Package org.openquark.gems.client.caleditor

Source Code of org.openquark.gems.client.caleditor.AdvancedCALEditor$UnderlineHighlightPainter

/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*     * Redistributions of source code must retain the above copyright notice,
*       this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of Business Objects nor the names of its contributors
*       may be used to endorse or promote products derived from this software
*       without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/


/*
* GemCodePanel.java
* Creation date: (March 10, 2004 1:17:15 PM)
* By: Iulian Radu
*/
package org.openquark.gems.client.caleditor;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.plaf.TextUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Element;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.Highlighter.Highlight;
import javax.swing.text.Highlighter.HighlightPainter;

import org.openquark.cal.compiler.CodeAnalyser;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.compiler.QualifiedName;
import org.openquark.cal.compiler.ScopedEntity;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.SourceIdentifier;
import org.openquark.cal.compiler.SourceRange;
import org.openquark.cal.compiler.CodeAnalyser.AnalysedIdentifier;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous;
import org.openquark.cal.services.CALWorkspace;
import org.openquark.gems.client.GemCutter;
import org.openquark.gems.client.ToolTipHelpers;
import org.openquark.util.Pair;



/**
* This is an extension of the CALEditor class, providing
* support for interaction with code elements such as
* supercombinators, class methods, data types, data constructors
* type classes and arguments. The editor has the
* following features:
*
*  - uses metadata to display descriptive tooltips for code elements
*  - facilitates identification and resolution of ambiguous elements
*    (these are unqualified elements which belong to multiple external modules)
*  - provides accessibility for custom menus
*
* @author Iulian Radu
*/
public class AdvancedCALEditor extends CALEditor {
   
    private static final long serialVersionUID = 1971239530937184912L;

    /** List of qualified and unqualified identifiers found in the code */
    private List <AnalysedIdentifier> analysedIdentifiers;
   
    private List <AmbiguityOffset> ambiguityOffsets = new ArrayList<AmbiguityOffset>();
   
    /** Type information about the module which the code belongs to */
    private ModuleTypeInfo workingModuleTypeInfo;
   
    /** Provider for popup menus */
    private IdentifierPopupMenuProvider popupMenuProvider;

    /** The workspace in which the editor exists. */
    private final CALWorkspace workspace;
   
    /**
     * Interface to define a provider for a popup menu for interacting with identifiers.
     * @author Iulian Radu
     */
    public interface IdentifierPopupMenuProvider {
        /**
         * Get the popup menu for a given identifier.
         * @param identifier clicked identifier
         * @return JPopupMenu the popup menu for this identifier
         */
        JPopupMenu getPopupMenu(PositionlessIdentifier identifier);
    }
   
    /**
     * Class representing an analysed identifier without position.
     * @author Iulian Radu
     */
    public static final class PositionlessIdentifier {
       
        /** Unqualified name of identifier */
        private final String unqualifiedName;
       
        /** Module name as appearing in the source text, if identifier is qualified. This is null if the identifier is unqualified. */
        private final ModuleName rawModuleName;
       
        /** Resolved module name, if identifier is qualified. This is null if the identifier is unqualified. */
        private final ModuleName resolvedModuleName;
       
        /**
         * If {@link #rawModuleName} is resolvable, then this is the minimally qualified module name that resolves to the same module.
         * Otherwise, this holds the same value as {@link #rawModuleName}.
         */
        private final ModuleName minimallyQualifiedModuleName;
       
        /** Category */
        private final SourceIdentifier.Category category;
       
        /** Qualification type */
        private final CodeAnalyser.AnalysedIdentifier.QualificationType qualificationType;
       
        /** The analysed identifier which is referenced, if any */
        private final CodeAnalyser.AnalysedIdentifier referenceIdentifier;
       
        /**
         * Constructor
         * @param unqualifiedName identifier name
         * @param rawModuleName identifier module (null if none)
         * @param resolvedModuleName resolved identifier module (null if none)
         * @param minimallyQualifiedModuleName
         *           if {@link #rawModuleName} is resolvable, then this is the minimally qualified module name that resolves to the same module.
         *           Otherwise, this should have the same value as {@link #rawModuleName}.
         * @param category identifier category
         * @param qualificationType identifier qualification type
         */
        public PositionlessIdentifier(
                String unqualifiedName,
                ModuleName rawModuleName,
                ModuleName resolvedModuleName,
                ModuleName minimallyQualifiedModuleName,
                SourceIdentifier.Category category, CodeAnalyser.AnalysedIdentifier.QualificationType qualificationType) {
           
            if ((unqualifiedName == null) || (category == null) || (qualificationType == null)) {
                throw new NullPointerException();
            }
           
            this.unqualifiedName     = unqualifiedName;
            this.rawModuleName       = rawModuleName;
            this.resolvedModuleName  = resolvedModuleName;
            this.minimallyQualifiedModuleName = minimallyQualifiedModuleName;
            this.category            = category;
            this.qualificationType   = qualificationType;
            this.referenceIdentifier = null;
        }
       
        /**
         * Constructor, specifying reference
         */
        public PositionlessIdentifier(CodeAnalyser.AnalysedIdentifier referenceIdentifier) {
           
            if (referenceIdentifier == null) {
                throw new NullPointerException();
            }
           
            this.unqualifiedName      = referenceIdentifier.getName();
            this.rawModuleName        = referenceIdentifier.getRawModuleName();
            this.resolvedModuleName   = referenceIdentifier.getResolvedModuleName();
            this.minimallyQualifiedModuleName = referenceIdentifier.getMinimallyQualifiedModuleName();
            this.category             = referenceIdentifier.getCategory();
            this.qualificationType    = referenceIdentifier.getQualificationType();
            this.referenceIdentifier  = referenceIdentifier;
        }
       
        /**
         * Checks whether this object's fields are the same to another
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object o) {
            if ((o == null) || !(o instanceof PositionlessIdentifier)) {
                return false;
            }
           
            return (this.unqualifiedName.equals(((PositionlessIdentifier)o).getName()))  &&
                   areMaybeModuleNamesEqual(this.rawModuleName, ((PositionlessIdentifier)o).getRawModuleName()) &&
                   areMaybeModuleNamesEqual(this.resolvedModuleName, ((PositionlessIdentifier)o).getResolvedModuleName()) &&
                   areMaybeModuleNamesEqual(this.minimallyQualifiedModuleName, ((PositionlessIdentifier)o).getMinimallyQualifiedModuleName()) &&
                   (this.category == (((PositionlessIdentifier)o).getCategory())) &&
                   (this.qualificationType == (((PositionlessIdentifier)o).getQualificationType()));
        }
       
        @Override
        public int hashCode() {
            int result = 17;
            result = 37*result + unqualifiedName.hashCode();
            result = 37*result + rawModuleName.hashCode();
            result = 37*result + resolvedModuleName.hashCode();
            result = 37*result + minimallyQualifiedModuleName.hashCode();
            result = 37*result + category.hashCode();
            result = 37*result + qualificationType.hashCode();
            return result;
        }
       
        // Simple accessors
       
        public ModuleName getRawModuleName() {
            return rawModuleName;
        }
       
        public ModuleName getResolvedModuleName() {
            return resolvedModuleName;
        }
       
        public ModuleName getMinimallyQualifiedModuleName() {
            return minimallyQualifiedModuleName;
        }
       
        public String getName() {
            return unqualifiedName;
        }
       
        public SourceIdentifier.Category getCategory() {
            return category;
        }
       
        public CodeAnalyser.AnalysedIdentifier.QualificationType getQualificationType() {
            return qualificationType;
        }
       
        public CodeAnalyser.AnalysedIdentifier getReference() {
            return referenceIdentifier;
        }
       
        private static boolean areMaybeModuleNamesEqual(ModuleName maybeModuleName1, ModuleName maybeModuleName2) {
            if (maybeModuleName1 == null) {
                return maybeModuleName2 == null;
            } else {
                return maybeModuleName1.equals(maybeModuleName2);
            }
        }
    }
   
   
    /**
     * Class to handle mouse events on text identifiers.
     *
     * @author Iulian Radu
     */
    private class MouseHandler extends org.openquark.gems.client.utilities.MouseClickDragAdapter {
       
        /**
         * Constructor for the Mouse Handler
         */
        private MouseHandler() {
        }
       
        /**
         * Surrogate method for mouseClicked.  Called only when our definition of click occurs.
         * @param e MouseEvent the relevant event
         * @return boolean true if the click was a double click
         */
        @Override
        public boolean mouseReallyClicked(MouseEvent e){

            boolean doubleClicked = super.mouseReallyClicked(e);
           
            return doubleClicked;
        }
       
        /**
         * If mouse event triggers popup, show it
         * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
         */
        @Override
        public void mousePressed(MouseEvent e) {
            super.mousePressed(e);
            maybeShowPopup(e);
        }
       
        /**
         * If mouse event triggers popup, show it
         * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
         */
        @Override
        public void mouseReleased(MouseEvent e) {
            super.mouseReleased(e);
            maybeShowPopup(e);
        }
       
        /**
         * Show the popup, if the given mouse event is the popup trigger.
         * @param e the mouse event.
         */
        private void maybeShowPopup(MouseEvent e) {

            if (e.isPopupTrigger() && popupMenuProvider != null) {

                // Get the popup menu for the current identifier.
                CodeAnalyser.AnalysedIdentifier identifier = getIdentifierAtPoint(e.getPoint());
                if (identifier == null) {
                    // not pointing to anything valid
                    return;
                }
               
                PositionlessIdentifier strippedIdentifier =
                    new PositionlessIdentifier(identifier);
               
                if (popupMenuProvider != null) {
                    JPopupMenu menu = popupMenuProvider.getPopupMenu(strippedIdentifier);
                    if (menu != null) {
                        menu.show(e.getComponent(), e.getX(), e.getY());
                    }
                }
            }
        }
    }
   
    /**
     * A convenience class for locating a portion of cal code within
     * the buffer of the text editor.
     */
    public static class EditorLocation {
       
        /** Start offset of the portion */
        private final int start;
       
        /** End offset of the portion */
        private final int end;
       
        /** Line number of the portion */
        private final int lineNumber;
       
        private EditorLocation(int start, int end, int lineNumber) {
           
            if (start < 0 || end < 0 || lineNumber < 0) {
                throw new IllegalArgumentException();
            }
           
            if (start > end) {
                throw new IllegalArgumentException();
            }
           
            this.start = start;
            this.end = end;
            this.lineNumber = lineNumber;
        }
       
        public int getStartOffset() {
            return start;
        }
       
        public int getEndOffset() {
            return end;
        }
       
        public int getLineNumber() {
            return lineNumber;
        }
    }
   
    /**
     * A convenience class for storing an ambiguous identifier and its position.
     * @author Iulian Radu
     */
    public static class AmbiguityOffset {
       
        private final CodeAnalyser.AnalysedIdentifier identifier;
        private final EditorLocation ambiguityLocation;
       
        private AmbiguityOffset(CodeAnalyser.AnalysedIdentifier identifier, EditorLocation offset) {
           
            if (identifier == null) {
                throw new NullPointerException();
            }
           
            this.identifier = identifier;
            this.ambiguityLocation = offset;
        }
       
        public int getStartOffset() {
            return ambiguityLocation.getStartOffset();
        }
       
        public int getEndOffset() {
            return ambiguityLocation.getEndOffset();
        }
       
        public int getLineNumber() {
            return ambiguityLocation.getLineNumber();
        }
       
        public CodeAnalyser.AnalysedIdentifier getIdentifier() {
            return identifier;
        }
    }
   
    /**
     * This class draws a little squiggly underline as the highlight. It is used
     * to indicate the location of code errors and ambiguities.
     *
     * NOTE: This class assumes that the highlight is on one line. It doesn't work
     * for hightlights that span multiple lines.
     * @author Frank Worsley
     */
    public abstract static class UnderlineHighlightPainter implements Highlighter.HighlightPainter {

        /** The color of the line to paint. */
        public abstract Color getLineColor();

        /**
         * @see javax.swing.text.Highlighter.HighlightPainter#paint(java.awt.Graphics, int, int, java.awt.Shape, javax.swing.text.JTextComponent)
         */
        public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {

            try {
               
                TextUI textUI = c.getUI();
                Rectangle start = textUI.modelToView(c, p0);
                Rectangle end = textUI.modelToView(c, p1);

                g.setColor(getLineColor());

                // This is the y coordinate of the bottom of the area we can draw in
                int y = start.y + start.height - 1;
               
                // Draws a squiggly line. We draw at least two squiggles.
                int width = 1;
                int height = 1;
                int x = start.x;
                int count = 0;
               
                do {
                    g.drawLine(x, y, x + width, y);
                    g.drawLine(x + width + 1, y - height, x + 2*width + 1, y - height);
                    x += 2*width + 2;
                    count++;
                } while (x < end.x - width || count < 2);
               
                // Draws a simple straight line
                //g.drawLine(start.x, y, end.x, y);

                // Draws a bounding box
                //g.drawRect(start.x - 2, start.y, end.x - start.x + 4, start.height - 1);

            } catch (BadLocationException e) {
                throw new IllegalStateException("invalid location drawing highlight");
            }
        }
    }
   
    /** Highlight painter for ambiguities */
    class AmbiguityUnderlineHighlightPainter extends UnderlineHighlightPainter {

        private final Color LINE_COLOR = Color.BLUE;
        @Override
        public Color getLineColor() {
            return LINE_COLOR;
        }
    }
   
    /**
     * Maintains highlights over the specified symbols in the current editor.
     * @author Iulian Radu
     */
    public class SymbolHighlighter {
       
        /** Default highlighting color */
        private Color highlightColor = new Color(255,255,70,150);
       
        /** Highlighter used to draw our highlights */
        private final Highlighter highlighter;
       
        /** References to highlights for specified identifiers */
        private final List<Object> highlights = new ArrayList<Object>();
       
        /** Identifiers to highlight */
        private final List<AnalysedIdentifier> identifiers;
       
       
        /** Constructor */
        public SymbolHighlighter(List<AnalysedIdentifier> identifiers) {
            if (identifiers == null) {
                throw new NullPointerException();
            }
           
            this.highlighter = getHighlighter();
            this.identifiers = identifiers;
        }
       
        /**
         * Set the highlight color as specified
         * Note: This does not update the color of existing applied highlights
         *
         * @param color new highlight bar color
         */
        public void setHighlightColor(Color color) {
            highlightColor = color;
        }
       
        /** Apply highlights to the specified identifiers */
        public void applyHighlights() {
            removeHighlights();
            try {
                for (final AnalysedIdentifier referenceIdentifier : identifiers) {
                    SourceRange referenceIdentifierOffsetRange = referenceIdentifier.getOffsetRange();
                    AdvancedCALEditor.EditorLocation editorLoc = getEditorTokenOffset(referenceIdentifierOffsetRange.getStartLine(), referenceIdentifierOffsetRange.getStartColumn(),
                            referenceIdentifier.getName().length());
                    int posStart = editorLoc.getStartOffset();
                    int posEnd = editorLoc.getEndOffset();
                    Object highlightRef = highlighter.addHighlight(posStart, posEnd, new DefaultHighlighter.DefaultHighlightPainter(highlightColor));
                    highlights.add(highlightRef);
                }
            } catch (BadLocationException ex) {
                throw new IllegalStateException("Cannot highlight variable because position is invalid");
            }
        }
       
        /** Remove all applied highlights */
        public void removeHighlights() {
            for (int i = 0; i < highlights.size(); i ++) {
                highlighter.removeHighlight(highlights.get(i));
            }
        }
       
        /**
         * Pads all highlights as specified.
         * The highlighted sections will constrain to fit the text buffer size.
         *
         * @param leftPad characters to pad on the left side
         * @param rightPad characters to pad on the right side
         */
        void padHighlights(int leftPad, int rightPad) {
           
            for (int i = 0; i < highlights.size(); i ++) {
                Highlight highlight = (Highlight)highlights.get(i);
                Highlight newHighlight = getPaddedHighlight(highlight, leftPad, rightPad);
                highlights.set(i, newHighlight);
            }
        }
       
        /**
         * Pads the highlight at the specified position
         * The highlighted section will constrain to fit the text buffer size.
         *
         * @param highlightPos position contained within the highlight
         * @param leftPad characters to pad on the left side
         * @param rightPad characters to pad on the right side
         */
        void padHighlight(int highlightPos, int leftPad, int rightPad) {
            for (int i = 0; i < highlights.size(); i ++) {
                Highlight highlight = (Highlight)highlights.get(i);
                if ((highlight.getStartOffset() <= highlightPos) && (highlightPos <= highlight.getEndOffset())) {
                    Highlight newHighlight = getPaddedHighlight(highlight, leftPad, rightPad);
                    highlights.set(i, newHighlight);
                }
            }
        }
       
        /**
         * Pads the highlight as specified.
         * The highlighted section will constrain to fit the text buffer size.
         *
         * @param highlight highlight to be padded
         * @param leftPad characters to pad on the left side
         * @param rightPad characters to add on the right side
         */
        private Highlight getPaddedHighlight(Highlight highlight, int leftPad, int rightPad) {
           
            int textBufferSize = getText().length();
           
            HighlightPainter painter = highlight.getPainter();
            int posStart = Math.max(0, highlight.getStartOffset() - leftPad);
            int posEnd = Math.min(textBufferSize, highlight.getEndOffset() + rightPad);
           
            try {
                highlighter.removeHighlight(highlight);
                highlight = (Highlight)highlighter.addHighlight(posStart, posEnd, painter);
                return highlight;
            } catch (BadLocationException ex) {
                throw new IllegalStateException("Cannot highlight variable because position is invalid");
            }
        }
    }
   
    /**
     * Listener notified when symbol renaming completes.
     * @author Iulian Radu
     */
    public interface SymbolRenamerListener {
        public void renameDone(String oldName, String newName);
        public void renameCanceled(String oldName);
    }
   
    /**
     * This class handles user interaction with the editor while a symbol is being renamed.
     * @author Iulian Radu
     */
    class SymbolRenamer {
       
        /** List of identifiers to be renamed */
        private final List<AnalysedIdentifier> symbolPositions;
       
        /** Old name of the symbols */
        private final String oldName;
       
        /** New name, updated as user types */
        private String newName;
       
        /** Caret position within the name */
        private int caretPos;
       
        /** The identifier where user is typing; updates to this name are handled by the editor itself */
        private final AnalysedIdentifier startIdentifier;
       
        /** Listener notified when renaming completes */
        private final SymbolRenamerListener renamerListener;
       
        // Listeners to the editor previous to entering renaming mode.
        // These are removed once renaming starts, and put back once it completes.
        private KeyListener[] oldKeyListeners;
        private MouseListener[] oldMouseListeners;
       
        /** Highlighters for the references and definition of the renamed identifiers */
        private final SymbolHighlighter referenceHighlighter;
        private final SymbolHighlighter definitionHighlighter;
       
        /**
         * Mouse listener; finishes editing if mouse is clicked.
         */
        private final MouseListener cancelMouseListener = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                done();
            }
        };
       
        /**
         * Focus listener; finishes editing if focus is switched somewhere else
         */
        private final FocusListener focusListener = new FocusListener() {
            public void focusGained(FocusEvent e) {
            }
            public void focusLost(FocusEvent e) {
                done();
            }
        };
       
        /**
         * Key listener for the editor while in rename mode.
         * As the user types, all identifier references are updated to match keystrokes. Caret
         * navigation is restriced to the editing section.
         *
         * Implementation note:
         *   Associated to the editor are at least 3 key listeners, invoked in order of appearance:
         *       - a top level document listener inserting typed alphanumeric characters into
         *         the editor
         *       - this listener
         *       - an editing listener, handling editor navigation (eg: arrow keys) and deletion
         */
        private final KeyListener renameKeyListener = new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
               
                int keyCode = e.getKeyCode();
               
                if (e.isControlDown() || e.isAltDown()) {
                    e.consume();
                    return;
                   
                } else if (keyCode == KeyEvent.VK_ESCAPE) {
                    // Exit cancelling
                    cancel();
                    e.consume();
                    return;
                   
                } else if (keyCode == KeyEvent.VK_ENTER) {
                    // Exit ok
                    done();
                    e.consume();
                    return;
                   
                } else if ( (keyCode == KeyEvent.VK_UP) ||
                            (keyCode == KeyEvent.VK_DOWN) ||
                            (keyCode == KeyEvent.VK_PAGE_UP) ||
                            (keyCode == KeyEvent.VK_PAGE_DOWN)) {
            
                    // Don't respond to these      
                    e.consume()
                    return;
               
                } else if (keyCode == KeyEvent.VK_HOME) {
                    setCaretPosition(getCaretPosition() - caretPos);
                    caretPos = 0;
                    e.consume();
                    return;
                   
                } else if (keyCode == KeyEvent.VK_END) {
                    setCaretPosition(getCaretPosition() + (newName.length() - caretPos));
                    caretPos = newName.length();
                    e.consume();
                    return;
                   
                } else if (keyCode == KeyEvent.VK_LEFT) {
                    if (getCaretPosition() > 0 && caretPos > 0) {
                        caretPos--;
                    } else {
                        e.consume();
                    }
                    return;
                   
                } else if (keyCode == KeyEvent.VK_RIGHT) {
                    if (getCaretPosition() < getText().length() && caretPos < newName.length()) {
                        caretPos++;
                    } else {
                        e.consume();
                    }
                    return;
                   
                } else if (keyCode == KeyEvent.VK_BACK_SPACE) {
                    if (getCaretPosition() <= 0 || caretPos <= 0) {
                        // If we are back-spacing beyond our identifier space,
                        // insert a character to consume so the editor is not changed
                       
                        select(getCaretPosition(), getCaretPosition());
                        replaceSelection("?");
                        return;
                    }
                } else if (keyCode == KeyEvent.VK_DELETE) {
                    if (!(getCaretPosition() < getText().length() && caretPos < newName.length())) {
                        // We are deleting at the edge of the identifier space
                       
                        e.consume();
                        return;
                    }
                } else if (keyCode == KeyEvent.VK_INSERT) {
                    e.consume();
                    return;
                   
                }
               
                // Now, insert the typed character (or delete characters) from each edited section
               
                if (e.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
                    // Not an insertable character, so stop.
                    // Note: backspace and delete characters do not fall under this category
                    return;
                }
               
                if (keyCode == KeyEvent.VK_BACK_SPACE) {
                    // For backspace, we remove the character to the left of the caret
                    newName = newName.substring(0, caretPos-1) + newName.substring(caretPos);
                   
                } else if (keyCode == KeyEvent.VK_DELETE) {
                    // For delete, we remove the character to the right of the caret
                    newName = newName.substring(0, caretPos) + newName.substring(caretPos + 1);
                   
                } else {
                   
                    // Insert character at the caret
                    if (caretPos == 0) {
                        // Highlight one character before the editing position, so that the inserted char
                        // will be highlighted
                        referenceHighlighter.padHighlight(getCaretPosition(), 1, 0);
                        definitionHighlighter.padHighlight(getCaretPosition(), 1, 0);
                        SwingUtilities.invokeLater(new Runnable() {
                            // And after insertion, remove highlight to original size
                            public void run() {
                                referenceHighlighter.padHighlight(getCaretPosition(), -1, 0);
                                definitionHighlighter.padHighlight(getCaretPosition(), -1, 0);
                            }
                        });
                        e.consume();
                    }
                    newName = newName.substring(0, caretPos) + e.getKeyChar() + newName.substring(caretPos);
                    caretPos++;
                }
               
                // Save position of the current editor caret
                int editorCaretPos = getCaretPosition();
               
                // Count the number of identifiers passed
                int identifiers = 0;
               
                // Keep track of which line we are scanning
                int currentLine = 0;
               
                // Add a padding to the highlights, in case we insert at the beginning of
                // the highlight (in this case, the highlight would not grow since characters
                // are inserted before the highlight start position). This will be reverted
                // after all identifiers are modified.
                referenceHighlighter.padHighlights(1, 0);
                definitionHighlighter.padHighlights(1, 0);
                   
                try {
                    for (final AnalysedIdentifier identifier : symbolPositions) {
                   
                        SourceRange identifierOffsetRange = identifier.getOffsetRange();
                       
                        // Get original position of each identifier
                        int pos = (convertPositionToEditorOffset(identifierOffsetRange.getStartLine(), identifierOffsetRange.getStartColumn()));
                        if (currentLine != identifierOffsetRange.getStartLine()) {
                            // The convertPositionToEditorOffset() uses the start offset of the identifier line
                            // so we only account for identifier on our line, and trust that the editor will
                            // offset position due to insertions at higher lines
                            currentLine = identifierOffsetRange.getStartLine();
                            identifiers = 0;
                        }
                        // Each position is offset due to modifications to the identifiers before it
                        int posOffset = identifiers*(newName.length() - oldName.length()) + caretPos;
                        pos = pos + posOffset;
                       
                        identifiers++;
                        if (identifier == startIdentifier) {
                            // Don't do anything since the editor will do editing for us here
                            continue;
                        }
                       
                        SourceRange startIdentifierOffsetRange = startIdentifier.getOffsetRange();
                       
                        if (keyCode == KeyEvent.VK_BACK_SPACE) {
                            if ((startIdentifierOffsetRange.getStartLine() == currentLine) && (startIdentifierOffsetRange.getStartColumn() < identifierOffsetRange.getStartColumn())) {
                                pos++;
                            }
                            // Backspace eats the character left of the caret
                            select(pos-1, pos);
                            replaceSelection("");
                            if ((startIdentifierOffsetRange.getStartLine() > currentLine) || ((startIdentifierOffsetRange.getStartLine() == currentLine) &&  (startIdentifierOffsetRange.getStartColumn() > identifierOffsetRange.getStartColumn()))) {
                                editorCaretPos--;
                            }
                           
                        } else if (keyCode == KeyEvent.VK_DELETE) {
                            if ((startIdentifierOffsetRange.getStartLine() == currentLine) && (startIdentifierOffsetRange.getStartColumn() < identifierOffsetRange.getStartColumn())) {
                                pos++;
                            }
                            // Delete eats the character right of the caret
                            select(pos, pos+1);
                            replaceSelection("");
                            if ((startIdentifierOffsetRange.getStartLine() > currentLine) || ((startIdentifierOffsetRange.getStartLine() == currentLine) &&  (startIdentifierOffsetRange.getStartColumn() > identifierOffsetRange.getStartColumn()))) {
                                editorCaretPos--;
                            }

                        } else {
                            pos -= 1;
                            if ((startIdentifierOffsetRange.getStartLine() == currentLine) && (startIdentifierOffsetRange.getStartColumn() < identifierOffsetRange.getStartColumn())) {
                                pos--;
                            }
                            // Insert at caret position
                            select(pos, pos);
                            replaceSelection(Character.toString(e.getKeyChar()));
                            if ((startIdentifierOffsetRange.getStartLine() > currentLine) || ((startIdentifierOffsetRange.getStartLine() == currentLine) &&  (startIdentifierOffsetRange.getStartColumn() > identifierOffsetRange.getStartColumn()))) {
                                editorCaretPos++;
                            }
                        }
                    }
                } catch (BadLocationException ex) {
                    throw new IllegalStateException("Identifier contains illegal position in editor.");
                }
               
                // Move caret back because of backspace
                if (keyCode == KeyEvent.VK_BACK_SPACE) {
                    caretPos--;
                }
               
                // Shrink the highlights
                referenceHighlighter.padHighlights(-1,0);
                definitionHighlighter.padHighlights(-1,0);
               
                // Put back the editor caret to the selected identifier
                setCaretPosition(editorCaretPos);
            }
        };

        /**
         * Constructor
         * @param startIdentifier identifier where editing starts
         * @param renamerListener listener for rename completion
         */
        SymbolRenamer(AnalysedIdentifier startIdentifier, SymbolRenamerListener renamerListener) {
            if (startIdentifier == null) {
                throw new NullPointerException();
            }
            if ((startIdentifier.getCategory() != SourceIdentifier.Category.LOCAL_VARIABLE) &&
                (startIdentifier.getCategory() != SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION)) {
                throw new UnsupportedOperationException();
            }
           
            this.startIdentifier = startIdentifier;
            this.renamerListener = renamerListener;
           
            oldName = startIdentifier.getName();
            newName = oldName;
            caretPos = oldName.length();
           
            // Calculate which identifiers will be modified and highlight
            Pair<SymbolHighlighter, SymbolHighlighter> highlighters = createLocalVariableHighlighters(startIdentifier);
            definitionHighlighter = highlighters.snd();
            referenceHighlighter = highlighters.fst();
            symbolPositions = findRelatedIdentifiers(startIdentifier, true);
        }
       
        /**
         * Enter rename mode.
         * After saving editor state, this method replaces existing listeners with our own.
         */
        void start() {
           
            // Put caret at the end of the identifier being edited
            try {
                SourceRange startIdentifierOffsetRange = startIdentifier.getOffsetRange();
                setCaretPosition(convertPositionToEditorOffset(startIdentifierOffsetRange.getStartLine(), startIdentifierOffsetRange.getStartColumn()) + caretPos);
            } catch (BadLocationException ex) {
                throw new IllegalStateException("Identifier contains illegal position in editor.");
            }
           
            // Remove listeners from the editor and add our own
           
            oldKeyListeners = getKeyListeners();
            for (final KeyListener oldKeyListener : oldKeyListeners) {
                removeKeyListener(oldKeyListener);
            }
            addKeyListener(renameKeyListener);
           
            oldMouseListeners = getMouseListeners();
            for (final MouseListener oldMouseListener : oldMouseListeners) {
                removeMouseListener(oldMouseListener);
            }
            addMouseListener(cancelMouseListener);
           
            addFocusListener(focusListener);
           
            // Highlight symbols
           
            referenceHighlighter.applyHighlights();
            definitionHighlighter.applyHighlights();
        }
       
        /**
         * Exit renaming mode.
         * This restores the editor listeners to their previous state.
         */
        void done() {
            finish();
           
            // Inform rename listener that we are finished
            if (renamerListener != null) {
                renamerListener.renameDone(oldName, newName);
            }
        }
       
        void finish() {
       
            // Put back editor listeners
           
            removeKeyListener(renameKeyListener);
            for (final KeyListener oldKeyListener : oldKeyListeners) {
                addKeyListener(oldKeyListener);
            }
           
            removeMouseListener(cancelMouseListener);
            for (final MouseListener oldMouseListener : oldMouseListeners) {
                addMouseListener(oldMouseListener);
            }
           
            removeFocusListener(focusListener);
           
            referenceHighlighter.removeHighlights();
            definitionHighlighter.removeHighlights();
        }
       
        /**
         * Cancel editing.
         * This traverses affected symbol positions and restores them to the original form.
         *
         * Note: This assumes that identifiers are ordered by source position
         */
        void cancel() {
           
            // Revert the identifiers to their original name
           
            for (final AnalysedIdentifier identifier : symbolPositions) {
                try {
                    SourceRange identifierOffsetRange = identifier.getOffsetRange();
                    int pos = (convertPositionToEditorOffset(identifierOffsetRange.getStartLine(), identifierOffsetRange.getStartColumn()));
                    select(pos, pos + newName.length());
                    replaceSelection(oldName);
                } catch (BadLocationException ex) {
                    throw new IllegalStateException("Identifier contains illegal position in editor.");
                }
            }
           
            finish();
           
            // Inform rename listener that we are finished
            if (renamerListener != null) {
                renamerListener.renameCanceled(oldName);
            }
        }
    }
   
   
    /** Constructor with module */
    public AdvancedCALEditor(ModuleTypeInfo workingModuleTypeInfo, CALWorkspace workspace) {
        this.ambiguityOffsets = new ArrayList<AmbiguityOffset>();
        this.analysedIdentifiers = new ArrayList<AnalysedIdentifier>();
        this.workingModuleTypeInfo = workingModuleTypeInfo;
        this.workspace = workspace;
       
        // Add mouse listener
        addMouseListener(new MouseHandler());
       
        setToolTipText("AdvancedCALEditor");
    }
   
    /** Retrieves tooltip text from the metadata of the identifier pointed to. */
    @Override
    public String getToolTipText(MouseEvent e) {

        // Get identifier and check that we have a tooltipable one
       
        CodeAnalyser.AnalysedIdentifier identifier = getIdentifierAtPoint(e.getPoint());
        if (identifier == null) {
            // Not pointing to an identifier
            return null;
        }
        if ((!identifierIsArgument(identifier)) &&
            (!identifier.getQualificationType().isResolvedTopLevelSymbol()) &&
            (!identifierIsLocalVariable(identifier)) &&
            (identifier.getQualificationType() != CodeAnalyser.AnalysedIdentifier.QualificationType.UnqualifiedAmbiguousTopLevelSymbol)) {
           
            // Not argument, not successfully qualified, and not ambiguity, don't display tip
            return null;
        }
       
        if (identifierIsLocalVariable(identifier)) {
           
            // Find definition of this local variable
           
            CodeAnalyser.AnalysedIdentifier definitionIdentifier;
            if (identifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION) {
                definitionIdentifier = identifier;
            } else {
                definitionIdentifier = identifier.getDefinitionIdentifier();
            }
           
            // Now find the code expression of the definition
           
            String expression = "";
            try {
                SourceRange definitionIdentifierOffsetRange = definitionIdentifier.getOffsetRange();
                expression = getExpressionFrom(definitionIdentifierOffsetRange.getStartLine(), definitionIdentifierOffsetRange.getStartColumn());
            } catch (BadLocationException ex) {
                throw new IllegalStateException("Expression gathering tried to go past end of buffer.");
            }
            return "<html><body>" +
                   ToolTipHelpers.wrapTextToHTMLLines(GemCutter.getResourceString("CEP_LocalVariable") + ": " + expression + " ..", this) +
                   "</body></html>";
        }
       
        if (identifierIsArgument(identifier)) {
            return GemCutter.getResourceString("CEP_Argument");
        }
       
        if (identifier.getQualificationType() == CodeAnalyser.AnalysedIdentifier.QualificationType.UnqualifiedAmbiguousTopLevelSymbol) {
            return GemCutter.getResourceString("CEP_Ambiguity");
        }

        // Properly qualified top level identifier; get its metadata
        return getMetadataToolTipText(identifier.getName(), identifier.getResolvedModuleName(), identifier.getCategory(), workingModuleTypeInfo);
    }
   
    /**
     * Retrieve formatted metadata tooltip for the specified qualification.
     *
     * @param unqualifiedName unqualified name of identifier
     * @param moduleName identifier module name. Can be null.
     * @param type identifier type
     * @param workingModuleTypeInfo type info for the current module.
     * @return metadata tooltip text
     */
    public String getMetadataToolTipText(String unqualifiedName, ModuleName moduleName, SourceIdentifier.Category type,
                                         ModuleTypeInfo workingModuleTypeInfo) {
       
        return getMetadataToolTipText(unqualifiedName, moduleName, type, workingModuleTypeInfo, workspace, this);
    }
   
    /**
     * Retrieve formatted metadata tooltip for the specified qualification.
     *
     * @param unqualifiedName unqualified name of identifier
     * @param moduleName identifier module name. Can be null.
     * @param type identifier type
     * @param workingModuleTypeInfo type info for the current module.
     * @param workspace the related workspace.
     * @param parent component displaying the tooltip
     * @return metadata tooltip text
     */
    public static String getMetadataToolTipText(String unqualifiedName, ModuleName moduleName, SourceIdentifier.Category type,
                                                ModuleTypeInfo workingModuleTypeInfo, CALWorkspace workspace, JComponent parent) {
       
        if (moduleName == null) {
            // Entity could not be found
            return GemCutter.getResourceString("CEP_UnknownSymbol");
        }
       
        QualifiedName qualifiedName = QualifiedName.make(moduleName, unqualifiedName);
       
        ScopedEntity entity = CodeAnalyser.getVisibleModuleEntity(qualifiedName, type, workingModuleTypeInfo);
        if (entity != null) {
            ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(workingModuleTypeInfo);
            return ToolTipHelpers.getEntityToolTip(entity, namingPolicy, workspace, parent);

        } else {
            // Entity could not be found
            return GemCutter.getResourceString("CEP_UnknownSymbol");
        }
    }
   
    /**
     * @return the tab size of used in this document
     */
    public int getTabSize() {
        return ((Integer) this.getDocument().getProperty(PlainDocument.tabSizeAttribute)).intValue();
    }
   
    /**
     * Retrieves the identifier that the mouse is pointing to.
     *
     * @param p location of identifier
     * @return source identifier that the point refers to; null if none
     */
    CodeAnalyser.AnalysedIdentifier getIdentifierAtPoint(Point p) {
        try {
           
            int textOffset = getUI().viewToModel(this, p);
           
            // See how closely the returned text offset actually matches the cursor position.
            // If the mouse hovers anywhere to the right of the last character on a line, then
            // the returned offset will be the offset of that character. This is because that
            // character is closest to the mouse position.
            Rectangle offsetRect = getUI().modelToView(this, textOffset);
           
            // The offset rectangle is very small, we need to make it a
            // little bigger so that it is not to difficult to get a tooltip.
            offsetRect.x -= 5;
            offsetRect.y -= 5;
            offsetRect.width += 10;
            offsetRect.height += 10;
           
            if (!offsetRect.contains(p)) {
                // If the mouse is just hovering on empty space to the right
                // of a line, then don't show a tooltip.
                return null;
            }
           
            return getIdentifierAtPosition(textOffset);
        } catch (BadLocationException ex) {
            throw new IllegalStateException("bad location converting point to identifier");
        }
    }
   
    /**
     * @param textOffset text offset of the item
     * @return analysed identifier that the offset refers to, or null if none is found.
     */
    public CodeAnalyser.AnalysedIdentifier getIdentifierAtPosition(int textOffset) {
        if (analysedIdentifiers == null) {
            // Code was not parsed properly
            return null;
        }
       
        try {
           
            // Try and see if we can resolve an identifier using the token we are hovering over.
           
            int indexLine = getDocument().getRootElements()[0].getElementIndex(textOffset);
            Element lineElement = getDocument().getRootElements()[0].getElement(indexLine);
           
            if (lineElement == null) {
                return null;
            }
           
            // Find column where mouse is pointing
            int mouseColumn = textOffset - lineElement.getStartOffset() + 1;
           
            // Adjust startColumn and endColumn for tabs
            String text = getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset());
            int tabSize = getTabSize();
            int actualColumn = 1;
            for (int i = 0; i < mouseColumn; i ++) {
                if (text.charAt(i) != '\t') {
                    actualColumn++;
                } else {
                    //tabs can consume from 1 to tabSize columns (a tab character moves the column to the next tab stop)
                    int jump = (((actualColumn-1)/tabSize) + 1) * tabSize + 1 - actualColumn;
                    actualColumn += jump;
                }
            }
            mouseColumn = actualColumn - 1;
           
            // Iterate through our known source identifiers, and find the
            // identifier we are pointing to. If the item is not contained
            // in the list, we do not have type information about it and ignore it.
           
            for (final AnalysedIdentifier identifier : analysedIdentifiers) {
                // Check position of identifier name
               
                // The positions are shifted by 1 because identifier positions
                // are 1-based, while Document positions are 0-based.
               
                SourceRange identifierOffsetRange = identifier.getOffsetRange();
                int column = identifierOffsetRange.getStartColumn() - 1;

                int identifierEndLine = identifierOffsetRange.getEndLine() - 1;

                if ((identifierEndLine == indexLine) &&
                    (column < mouseColumn) &&
                    (mouseColumn <= column + identifier.getName().length() + 1)) {
                   
                    return identifier;
                }
               
                // Check position of identifier module name
                // This is done separately because an identifier name
                // may be separated from its module name (eg: "Prelude    .    not")
               
                if (identifier.hasRawModuleSourceRange()) {
               
                    SourceRange identifierOffsetModuleNameRange = identifier.getOffsetModuleNameRange();
                   
                    int startLine = identifierOffsetModuleNameRange.getStartLine() - 1;
                    int startColumn = identifierOffsetModuleNameRange.getStartColumn() - 1;
                    int endLine = identifierOffsetModuleNameRange.getEndLine() - 1;
                    int endColumn = identifierOffsetModuleNameRange.getEndColumn() - 1;
                   
                    if ((startLine <= indexLine) && (indexLine <= endLine) &&
                        (startColumn < mouseColumn) && (mouseColumn <= endColumn)) {
                       
                        return identifier;
                    }
                }
            }
           
            // None of the identifiers matched our position
            return null;
           
        } catch (BadLocationException ex) {
            throw new IllegalStateException("bad location converting text offset to identifier");
        }
    }
   
    /** Indicates whether the identifier is an argument */
    private boolean identifierIsArgument(CodeAnalyser.AnalysedIdentifier identifier) {
        return identifier.getQualificationType() == CodeAnalyser.AnalysedIdentifier.QualificationType.UnqualifiedArgument;
    }
   
    /** Indicates whether the identifier is a local variable */
    private boolean identifierIsLocalVariable(CodeAnalyser.AnalysedIdentifier identifier) {
        return identifier.getQualificationType() == CodeAnalyser.AnalysedIdentifier.QualificationType.UnqualifiedLocalVariable;
    }
   
    /**
     * Sets the source identifiers
     * @param analysedIdentifiers
     */
    public void setSourceIdentifiers(List<AnalysedIdentifier> analysedIdentifiers) {
        this.analysedIdentifiers = analysedIdentifiers;
    }
   
    /**
     * Sets the module type info used
     * @param moduleTypeInfo
     */
    public void setModuleTypeInfo(ModuleTypeInfo moduleTypeInfo) {
        this.workingModuleTypeInfo = moduleTypeInfo;
    }
   
    /**
     * Adds an indicator for an ambiguous identifier
     * @param identifier
     */
    private void addAmbiguityIndicator(CodeAnalyser.AnalysedIdentifier identifier) {
       
        if (identifier.getQualificationType() != CodeAnalyser.AnalysedIdentifier.QualificationType.UnqualifiedAmbiguousTopLevelSymbol) {
            throw new IllegalArgumentException();
        }
       
        AmbiguityOffset offset = getAmbiguityOffset(identifier);
        if (offset == null) {
            return;
        }
       
        ambiguityOffsets.add(offset);
       
        try {
            Highlighter highlighter = getHighlighter();
            highlighter.addHighlight(offset.getStartOffset(), offset.getEndOffset(), new AmbiguityUnderlineHighlightPainter());
           
        } catch (BadLocationException ex) {
            throw new IllegalStateException("bad location adding highlight");
        }
    }
   
    /**
     * Clears all ambiguity indicators.
     */
    private void clearAmbiguityIndicators() {
       
        Highlighter highlighter = getHighlighter();
        Highlight[] highlights = highlighter.getHighlights();
       
        for (final Highlight highlight : highlights) {
            if (highlight.getPainter() instanceof AmbiguityUnderlineHighlightPainter) {
                highlighter.removeHighlight(highlight);
            }
        }

        ambiguityOffsets.clear();
    }   
   
    /**
     * Clears ambiguity identifiers, then repopulates the
     * list of ambiguities from the analyzed identifiers.
     */
    public void updateAmbiguityIndicators() {

        clearAmbiguityIndicators();
       
        if (analysedIdentifiers == null) {
            return;
        }
       
        for (final AnalysedIdentifier identifier : analysedIdentifiers) {
            if (identifier.getQualificationType() == CodeAnalyser.AnalysedIdentifier.QualificationType.UnqualifiedAmbiguousTopLevelSymbol) {
                addAmbiguityIndicator(identifier);
            }
        }
    }

    /**
     * Converts the source position of the identifier into a text offset, length, and line number.
     * @param identifier the analysed identifie rto convert the source position for.
     */
    private AmbiguityOffset getAmbiguityOffset(CodeAnalyser.AnalysedIdentifier identifier) {

        try {

            SourceRange identifierOffsetRange = identifier.getOffsetRange();
            EditorLocation offset = getEditorTokenOffset(identifierOffsetRange.getStartLine(), identifierOffsetRange.getStartColumn(), identifier.getName().length());
            return new AmbiguityOffset(identifier, offset);
           
        } catch (BadLocationException ex) {
            throw new IllegalArgumentException("invalid location trying to convert ambiguous identifier source position");
        }
    }
   
    /**
     * Retrieves the list of ambiguities highlighted in the editor
     * @return List of ambiguity offsets
     */
    public List<AmbiguityOffset> getAmbiguityOffsets() {
        return ambiguityOffsets;
    }
   
    /**
     * Returns list of identifiers contained by the
     * specified offsets of editor selection.
     *
     * Note: identifiers are not adjusted to match the selection offsets
     *
     * @param offsetStart
     * @param offsetEnd
     * @return identifiers within the selected text portion
     */
    public List<AnalysedIdentifier> getSelectedIdentifiers(int offsetStart, int offsetEnd) {
        List<AnalysedIdentifier> containedIdentifiers = new ArrayList<AnalysedIdentifier>();
        if (analysedIdentifiers == null) {
            return containedIdentifiers;
        }
        try {
           
            for (final AnalysedIdentifier identifier : analysedIdentifiers) {
               
                SourceRange identifierOffsetRange = identifier.getOffsetRange();
                int identifierStartOffset = convertPositionToEditorOffset(identifierOffsetRange.getStartLine(), identifierOffsetRange.getStartColumn());
                int identifierEndOffset = identifierStartOffset + identifier.getName().length();
                if (identifierStartOffset >= offsetStart && identifierEndOffset <= offsetEnd) {
                    containedIdentifiers.add(identifier);
                }
            }
           
        } catch (BadLocationException ex) {
            throw new IllegalArgumentException("invalid location trying to find identifiers in selection");
        }
        return containedIdentifiers;
    }
   
    /**
     * Returns the fully qualified text which can be produced from the specified
     * editor text.
     *
     * @param offsetStart
     * @param offsetEnd
     * @param codeAnalyser
     * @return fully qualified text (including local symbols)
     */
    public String getQualifiedCodeText(int offsetStart, int offsetEnd, CodeAnalyser codeAnalyser) {
       
        // Select the text from the start of the buffer to the selection end; this is so
        // that the code analyser can match identifier positions within code.
       
        String codeText;
        try {
            codeText = getDocument().getText(0, offsetEnd);
        } catch (BadLocationException e) {
            throw new IllegalArgumentException("invalid location trying to find identifiers in selection");
        }
       
        // Qualify the code
       
        List<AnalysedIdentifier> identifiers = getSelectedIdentifiers(offsetStart, offsetEnd);
        String qualifiedFullText = codeAnalyser.replaceUnqualifiedSymbols(codeText, identifiers, true);
       
        // Now Strip away the beginning, since we analyzed too much of the code text
       
        return qualifiedFullText.substring(offsetStart);
    }
   
    /**
     * Converts a code position into an editor buffer offset
     * Note: This method assumes the position is valid (ie: it does
     * not occur on a nonexistent line, and the column number exists
     * on the specified line)
     *
     * @param lineNumber identifier code line
     * @param columnNumber identifier code column
     * @return editor buffter offset corresponding to the specified position
     */
    private int convertPositionToEditorOffset(int lineNumber, int columnNumber) throws BadLocationException  {
        lineNumber--;
        Element lineElement = getDocument().getRootElements()[0].getElement(lineNumber);
        int offset = columnNumber;
       
        // Adjust offset for tabs in this line
        String wholeLine = getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset());
        int currentColumnPos = 1;
        int pos = -1;
        int newOffset = offset;
        int tabSize = getTabSize();
        while (++pos < wholeLine.length() && currentColumnPos < offset) { 
            if (wholeLine.charAt(pos) != '\t') {
                currentColumnPos++;
            } else {
                //tabs can consume from 1 to tabSize columns (a tab character moves the column to the next tab stop)
                int jump = (((currentColumnPos-1)/tabSize) + 1) * tabSize + 1 - currentColumnPos;
                newOffset = newOffset - jump + 1;
                currentColumnPos += jump;
            }                                                     
        }
        offset = newOffset + lineElement.getStartOffset();

        // Shift offset left by 1, since it was a column offset; now it will be treated
        // as buffer offset
        offset--;
       
        return offset;
    }
   
    /**
     * Retrieves a code expression starting from the specified position.
     *
     * This method gathers characters following the position until either a ";", or an unopened
     * close bracket ")" is reached. Contents of parentheses, string literals or comments are ignored.
     *
     * @param codeLine
     * @param codeColumn
     */
    private String getExpressionFrom(int codeLine, int codeColumn) throws BadLocationException {
        int start = convertPositionToEditorOffset(codeLine, codeColumn);
        int end = getText().length();
       
        String buffer = "";
       
        int i = start;
        char c;
       
        int bracketCount = 0;
        boolean inBigComment = false; /* .. */
        boolean inLineComment = false; // ..
        boolean inPreComment = false; //   "/"
        boolean inPreEndBigComment = false; //   "*"
        boolean inString = false; // "...
        boolean inStringSpecial = false; // " \...
        boolean inCharString = false; // '..
       
        boolean keepGoing = true;
        while (keepGoing && (i<end-1)) {
            c = getText(i, 1).charAt(0);
           
            if (inLineComment) {
                switch (c) {
                    case '\n':
                    {
                        inLineComment = false;
                        break;
                    }
                }
               
            } else if (inBigComment) {
               
                switch (c) {
                    case '*':
                    {
                        inPreEndBigComment = true;
                        break;
                    }
                    case '/':
                    {
                        if (inPreEndBigComment) {
                            inPreEndBigComment = false;
                            inBigComment = false;
                        }
                    }
                    default:
                    {
                        inPreEndBigComment = false;
                    }
                }
               
            } else if (inPreComment) {
               
                switch (c) {
                    case '*':
                    {
                        inBigComment = true;
                        break;
                    }
                    case '/':
                    {
                        inLineComment = true;
                        break;
                    }
                }
                inPreComment = false;
               
            } else if (inString) {
               
                if (!inStringSpecial) {
                    switch (c) {
                        case '\\':
                        {
                            inStringSpecial = true;
                            break;
                        }
                        case '"':
                        {
                            inString = false;
                            break;
                        }
                    }
                } else {
                    inStringSpecial = false;
                }
               
            } else if (inCharString) {
                if (!inStringSpecial) {
                    switch (c) {
                        case '\\':
                        {
                            inStringSpecial = true;
                            break;
                        }
                        case '\'':
                        {
                            inCharString = false;
                            break;
                        }
                    }
                } else {
                    inStringSpecial = false;
                }
            } else {
           
                switch (c) {
                    case '(':
                    {
                        bracketCount++;
                        break;
                    }
                       
                    case ')':
                    {
                        if (bracketCount == 0) {
                            keepGoing = false;
                        }
                        bracketCount--;
                        break;
                    }
                   
                    case '"':
                    {
                        inString = true;
                        break;
                    }
                   
                    case '/':
                    {
                        inPreComment = true;
                        break;
                    }
                    case ';':
                    {
                        if (bracketCount == 0) {
                            keepGoing = false;
                        }
                        break;
                    }
                    case '\'':
                    {
                        inCharString = true;
                        break;
                    }
                }
            }
           
            if (keepGoing) {
                buffer += c;
            }
            i++;
        }
       
        return buffer;
    }
   
    /**
     * Given a code line and column, calculates the position  of a code token
     * within the editor text buffer.
     *
     * @param codeLine
     * @param codeColumn
     * @param length length of text to select (-1 will auto-detect end bound)
     * @return buffer location of the selected token
     * @throws BadLocationException if supplied location is invalid
     */
    public EditorLocation getEditorTokenOffset(int codeLine, int codeColumn, int length) throws BadLocationException {

        int lineNumber = -1;
        int start = -1;
        int end = -1;
       
        lineNumber = codeLine;
       
        Element rootElement = getDocument().getRootElements()[0];
        Element lineElement = null;
        int numberOfLines = rootElement.getElementCount();
        int offset = -1;
       
        if (lineNumber > numberOfLines) {

            // Before we pass the source to the CodeGem to be type checked, we add a \n
            // at the bottom to make sure the parsing is done correctly. So, if the error
            // occurs on that new line, it really means the error is at the end of the
            // last line of the actual document. Here we check for that and correct for it.
            lineNumber -= 2;
           
            if (lineNumber == numberOfLines) {
               
                // When the CodeGem type checks the source it adds a semicolon and
                // additional newline at the bottom. If the user starts a multi-line comment,
                // but doesn't terminate it, then the compiler will include the terminating
                // semicolon in the line count. That means in the case of an unterminated
                // comment, it can happen that we need to decrease the line count by one more.
                lineNumber--;
            }

            lineElement = rootElement.getElement(lineNumber);
            offset = lineElement.getEndOffset() - lineElement.getStartOffset() - 1;
           
        } else {
            offset = convertPositionToEditorOffset(lineNumber, codeColumn);
            lineNumber--;
            lineElement = rootElement.getElement(lineNumber);
            offset -= lineElement.getStartOffset();
        }

        String wholeLine = getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset());
        if (offset == wholeLine.length()) {
            offset--;
        }
       
        if (length == -1) {
            // Get the starting index of the token. Add 1 to it since we don't want to
            // include the actual space or newline in the highlight.
            start = Math.max(wholeLine.lastIndexOf(" ", offset), wholeLine.lastIndexOf("\t", offset)) + 1;
            if (start == 0) {
                start = wholeLine.lastIndexOf("\n", offset) + 1;
            }
           
            // Now find the end index of the token.
       
            int endSpaceIndex = wholeLine.indexOf(" ",  offset);
            int endTabIndex   = wholeLine.indexOf("\t", offset);
            if (endSpaceIndex != -1) {
                end = (endTabIndex != -1 ? Math.min(endSpaceIndex, endTabIndex) : endSpaceIndex);
            } else {
                end = endTabIndex;
            }
           
            if (end == -1) {
               
                end = wholeLine.indexOf("\n", offset);
               
                if (end == -1) {
                    end = wholeLine.length() - 1;
                }
            }
        } else {
           
            start = offset;
           
            int eolIndex = wholeLine.indexOf("\n", offset);
            if (eolIndex == -1) {
                eolIndex = wholeLine.length() - 1;
            }
            end = start + length;
            if (end > eolIndex) {
                end = eolIndex;
            }
        }

        // Since we add 1 to the start position, it can happen that start > end,
        // if the error occurs on a space at the very end of the line. Check for that here.
        if (start == end + 1) {
            start--;
        }

        // Add the line offset to the positions.
        start += lineElement.getStartOffset();           
        end += lineElement.getStartOffset();
       
        return new EditorLocation(start, end, lineNumber);
    }
   
    /**
     * Set the provider for popup menus
     * @param popupProvider
     */
    public void setPopupMenuProvider(IdentifierPopupMenuProvider popupProvider) {
        this.popupMenuProvider = popupProvider;
    }
   
    /**
     * @return provider for popup menus
     */
    public IdentifierPopupMenuProvider getPopupMenuProvider() {
        return popupMenuProvider;
    }
   
   
    /**
     * Causes the editor to switch interaction mode to rename the specified identifier.
     * @param identifier identifier to rename
     */
    public void enterRenameMode(CodeAnalyser.AnalysedIdentifier identifier, SymbolRenamerListener renameListener) {
        if (identifier == null) {
            throw new NullPointerException();
        }
       
        if ((identifier.getCategory() != SourceIdentifier.Category.LOCAL_VARIABLE) && (identifier.getCategory() != SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION)) {
            throw new UnsupportedOperationException();
        }
       
        SymbolRenamer renamer = new SymbolRenamer(identifier, renameListener);
        renamer.start();
    }
   
    /**
     * Create highlighters for a local variable reference or definition.
     *
     * If the uniformHighlightAllRelated flag is True, then all references and definition of this variable
     * are highlighted with the same color. Otherwise, if the identifier is a reference then different colors
     * are used for highlighting the definition and references. 
     * 
     * @param identifier local variable to highlight
     * @return Pair of highlighters for references and definitions respectively
     */
    public Pair<SymbolHighlighter, SymbolHighlighter> createLocalVariableHighlighters(CodeAnalyser.AnalysedIdentifier identifier) {
       
        List<AnalysedIdentifier> variableReferences = findRelatedIdentifiers(identifier, true);
       
        // The list will contain references and a definition; extract the definition and highlight in a
        // different color
        CodeAnalyser.AnalysedIdentifier variableDefinition = null;
        for (int i = 0, n = variableReferences.size(); i < n; i++) {
            CodeAnalyser.AnalysedIdentifier var = variableReferences.get(i);
            if (var.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION) {
                variableDefinition = var;
                variableReferences.remove(i);
                break;
            }
        }
        if (variableDefinition == null) {
            throw new IllegalStateException("AdvancedCALEditor: Local variable does not have definition");
        }
        List<AnalysedIdentifier> variableDefinitions = new ArrayList<AnalysedIdentifier>();
        variableDefinitions.add(variableDefinition);
       
        SymbolHighlighter referenceHighlighter = new SymbolHighlighter(variableReferences);
        SymbolHighlighter definitionHighlighter = new SymbolHighlighter(variableDefinitions);
        definitionHighlighter.setHighlightColor(new Color(255,200,70,170));
           
        return new Pair<SymbolHighlighter, SymbolHighlighter>(referenceHighlighter, definitionHighlighter);
    }
   
    /**
     * Produce a list of identifiers which relate to the specified identifier (ie: these identifiers
     * either define or are defined by the identifier)
     *
     * If the selectAllRelated flag is True, then all references and definition of this identifier
     * are listed. Otherwise, if the identifier is a reference then only the definition is listed
     * if the identifier is a definition, only its references are listed.
     *
     * Limitation: Currently this method only handles local variable identifiers
     */
    private List<AnalysedIdentifier> findRelatedIdentifiers(AnalysedIdentifier identifier, boolean selectAllRelated) {

        List<AnalysedIdentifier> selectedIdentifiers = new ArrayList<AnalysedIdentifier>();
        if (identifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE) {
           
            // This is a local variable, retrieve its definition
           
            AnalysedIdentifier definitionIdentifier = identifier.getDefinitionIdentifier();
           
            if (!selectAllRelated) {
                // Highlight just its definition
                selectedIdentifiers.add(definitionIdentifier);
               
            } else {
               
                // Find the definition and all references to this definition and highlight
                for (final AnalysedIdentifier codeIdentifier : analysedIdentifiers) {
                    if ((codeIdentifier == definitionIdentifier) ||
                        (codeIdentifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE &&
                         codeIdentifier.getDefinitionIdentifier() == definitionIdentifier)) {
                        
                        selectedIdentifiers.add(codeIdentifier);
                    }
                }
            }
           
        } else if (identifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION) {
            // This is a variable definition; highlight its references
           
            for (final AnalysedIdentifier codeIdentifier : analysedIdentifiers) {
                if ( (selectAllRelated && codeIdentifier == identifier) ||
                     (codeIdentifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE &&
                      codeIdentifier.getDefinitionIdentifier() == identifier)) {
                 
                    selectedIdentifiers.add(codeIdentifier);
                }
            }
           
        } else {
           
           throw new UnsupportedOperationException("Identifier category not supported");
        }
       
        return selectedIdentifiers;
    }
}
TOP

Related Classes of org.openquark.gems.client.caleditor.AdvancedCALEditor$UnderlineHighlightPainter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.