/* * 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; } }