/* * 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. */ /* * CodeGemEditor.java * Creation date: (1/26/01 11:16:20 AM) * By: Luke Evans */ package org.openquark.gems.client; import java.awt.CardLayout; import java.awt.Component; import java.awt.Frame; import java.awt.Point; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.ImageIcon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.Timer; import javax.swing.TransferHandler; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import javax.swing.undo.StateEdit; import javax.swing.undo.StateEditable; import javax.swing.undo.UndoableEdit; import org.openquark.cal.compiler.CodeAnalyser; import org.openquark.cal.compiler.CodeQualificationMap; import org.openquark.cal.compiler.ModuleName; 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.TypeExpr; import org.openquark.cal.compiler.CodeAnalyser.AnalysedIdentifier; import org.openquark.cal.compiler.CodeAnalyser.OffsetCompilerMessage; import org.openquark.cal.compiler.CodeAnalyser.AnalysedIdentifier.QualificationType; import org.openquark.cal.compiler.SourceIdentifier.Category; import org.openquark.cal.compiler.TypeChecker.TypeCheckInfo; import org.openquark.cal.services.GemEntity; import org.openquark.cal.services.Perspective; import org.openquark.gems.client.Argument.NameTypePair; import org.openquark.gems.client.Gem.PartConnectable; import org.openquark.gems.client.Gem.PartInput; import org.openquark.gems.client.browser.GemDrawerSelection; import org.openquark.gems.client.caleditor.AdvancedCALEditor; import org.openquark.gems.client.caleditor.AdvancedCALEditor.SymbolHighlighter; import org.openquark.gems.client.navigator.NavAddress; import org.openquark.gems.client.navigator.NavFrameOwner; import org.openquark.gems.client.valueentry.ValueEditorManager; import org.openquark.util.Pair; import org.openquark.util.UnsafeCast; /** * The CodeGemEditor is a JDialog which contains a GemCodePanel, plus some * smarts needed to update the CodeGem state based upon user actions such as * code editing and input reordering. Most of these involve running passes of * the parser over the code to ascertain parts of the syntax which allows us to * provide a graphical view of variables and some hinting. * * @author Luke Evans */ public class CodeGemEditor extends JDialog implements StateEditable, AutoCompleteManager.AutoCompleteEditor { private static final long serialVersionUID = -8650190926136188960L; /* * Keys for fields in map used with state editable interface */ private static final String LAST_ARGUMENTS_KEY = "LastArgumentsStateKey"; private static final String LAST_RESOLVED_QUALIFICATIONS_KEY = "LastResolvedQualificationsKey"; private static final String OLD_NAME_TO_INPUT_MAP_KEY = "OldNameToInputMapStateKey"; private static final String ARG_NAMED_VARS_KEY = "VarNamesWhichAreArgsStateKey"; private static final String PRESERVE_ORDER_KEY = "ShouldPreserveNaturalInputOrderStateKey"; /* * Icons used for popup menu items */ private static final ImageIcon POPUP_NAVEDIT_ICON = new ImageIcon(AdvancedCALEditor.class.getResource("/Resources/nav_edit.gif")); private static final ImageIcon POPUP_NAVVIEW_ICON = new ImageIcon(AdvancedCALEditor.class.getResource("/Resources/nav_edit.gif")); private static final ImageIcon POPUP_CONSTRUCTOR_ICON = new ImageIcon(AdvancedCALEditor.class.getResource("/Resources/Gem_Yellow.gif")); private static final ImageIcon POPUP_FUNCTION_ICON = new ImageIcon(AdvancedCALEditor.class.getResource("/Resources/nav_function.gif")); private static final ImageIcon POPUP_CLASS_ICON = new ImageIcon(AdvancedCALEditor.class.getResource("/Resources/nav_typeclass.gif")); private static final ImageIcon POPUP_TYPE_ICON = new ImageIcon(AdvancedCALEditor.class.getResource("/Resources/nav_typeconstructor.gif")); private static final ImageIcon POPUP_MAKE_ARGUMENT_ICON = new ImageIcon(CodeGemEditor.class.getResource("/Resources/argument.gif")); /** The CodeGem edited by this component. */ private final CodeGem codeGem; /** The code analyser object used to inspect the code */ private CodeAnalyser codeAnalyser; /** The type string provider used to convert types to strings. */ private final TypeStringProvider typeStringProvider; /** The perspective that keeps track of the current working module. */ private final Perspective perspective; /** The frame owner of the metadata viewer/editor */ private final NavFrameOwner navigatorOwner; /** Whether metadata editing is allowed */ private final boolean metadataEditAllowed; /** Transfer handler for the code text editor component */ private final EditorTextTransferHandler editorTransferHandler; /** * Popup menu provider for code editor and qualifications/variables * displays. */ private final PopupMenuProvider popupMenuProvider = new PopupMenuProvider(); /** * A variable to keep track of whether the syntax smarts are activated. For * example, we should set this to true whenever we are setting the text in * the code panel, so that a code edit-type event is not fired. */ private transient boolean smartsActivated = true; /** * Indicates whether the editor is recording edit (undo/redo) states. For * example, these states are not recorded when a local variable is renamed. */ private transient boolean recordingEditStates = true; // // Constituent components // private final JPanel dialogContentPane; private final GemCodePanel gemCodePanel; /** The handler for code gem edits. */ private final EditHandler editHandler; // // Timer stuff // /** * The parse timer triggers a code panel parse a given amount of time after * an edit. */ private final Timer parseTimer; /** The amount of time to wait (in ms) for user activity before parsing. */ private static final int PARSE_TIMER_PERIOD = 1000; /** The syntax listener that handles all the syntax coloring */ private final GemCodeSyntaxListener gemCodeSyntaxListener; // // Stuff to keep track of previous state. // /** * The list of arguments the last time the display had any */ private List<NameTypePair> lastArguments = new ArrayList<NameTypePair>(); /** * Map from name to input from the last time the codegem was not broken. */ private Map<String, PartInput> oldNameToInputMap = new HashMap<String, PartInput>(); /** * The whether to reorder the inputs upon, for instance, codeGem code * change. True means that inputs always appear in code order on code * change. False means that new inputs in changed code are added after * existing inputs. */ private boolean keepInputsInNaturalOrder = true; // // Various identifier-related state. // /** * Identifiers which have been constrained to be arguments * Unqualified names of arguments which appear in either qualified or * unqualified form are stored in this set. */ private Set<String> varNamesWhichAreArgs; /** * Qualification map of all identifiers which the user has assigned to * specific modules, regardless of whether they are still used within the * code. Resolved ambiguities and qualifications which have previously been * arguments are persistent in this map. * * Note: The map stored within the code gem contains only necessary entries * from this map, as well as entries representing unambiguous qualifications * (such entries do not appear in the userQualifiedIdentifiers map if the * module was not explicitly specified by the user). */ private CodeQualificationMap userQualifiedIdentifiers = new CodeQualificationMap(); /** * Map from incompatibly-connected part to its inferred type. */ private final Map<PartConnectable, TypeExpr> incompatiblePartToInferredTypeMap = new HashMap<PartConnectable, TypeExpr>(); /** The names of arguments which are unused in the code. */ private final Set<String> unusedArgNames = new HashSet<String>(); /** * The names of arguments which are used in unqualified form in the code. */ private final Set<String> unqualifiedArgNames = new HashSet<String>(); /** * Indicates whether there should be a delay before calling updateGemForTextChange(). See also setDelayUpdatingForTextChanges(). */ private boolean delayUpdatingForTextChanges = true; /** * A listener interface for edits to a code gem made by via code gem editor. * Possible changes include code changes, input reordering, and argument * qualification changes. Typically, the handler will make context-related * changes such as argument updating. * * @author Edward Lam */ public interface EditHandler { /** * Notify the handler that the gem definition has been edited * * @param codeGemEditor * the code gem editor used to edit the code gem. * @param oldInputs * the inputs from before the change. * @param codeGemEdit * an edit representing the pre and post states of the code * gem. */ public void definitionEdited(CodeGemEditor codeGemEditor, PartInput[] oldInputs, UndoableEdit codeGemEdit); } /** * Transferable object used by drag and drop between Qualification and * Variable display panels. * * This class represents an argument from a Variable Panel, and is created * by the Variable Display Transfer Handler. * * Its flavours are String (represented by argument name), and * VariableStructure (which holds details about this variable). * * @author Iulian Radu */ static final class VariablePanelTransferable implements Transferable { /** Structure which holds this transferable's details */ static class VariableStructure { private final String name; private final VariablesDisplay.VariablesDisplayList parentDisplayList; private final boolean transformationAllowed; /** * Constructor * * @param varName * name of the variable * @param parentDisplayList * the variables list object which created this structure * @param transformationAllowed * whether the variable can be transformed to a function * (this is not allowed for fully qualified or connected * arguments) */ private VariableStructure(String varName, VariablesDisplay.VariablesDisplayList parentDisplayList, boolean transformationAllowed) { this.name = varName; this.parentDisplayList = parentDisplayList; this.transformationAllowed = transformationAllowed; } // Accessors String getName() { return name; } VariablesDisplay.VariablesDisplayList getParentDisplayList() { return parentDisplayList; } boolean isTransformationAllowed() { return transformationAllowed; } } /** Transferable data */ private final VariableStructure variableStructure; // Transferable flavors protected static final DataFlavor variableStructureFlavor = new DataFlavor(VariableStructure.class, "Variable Structure"); private final DataFlavor[] flavors = {DataFlavor.stringFlavor, variableStructureFlavor}; /** * Creates a Transferable capable of transferring the specified * argument. */ public VariablePanelTransferable(String name, VariablesDisplay.VariablesDisplayList parentDisplayList, boolean transformationAllowed) { variableStructure = new VariableStructure(name, parentDisplayList, transformationAllowed); } /** * @see java.awt.datatransfer.Transferable#getTransferDataFlavors() */ public DataFlavor[] getTransferDataFlavors() { return flavors.clone(); } /** * @see java.awt.datatransfer.Transferable#isDataFlavorSupported(java.awt.datatransfer.DataFlavor) */ public boolean isDataFlavorSupported(DataFlavor flavor) { for (final DataFlavor dataFlavor : flavors) { if (flavor.equals(dataFlavor)) { return true; } } return false; } /** * @see java.awt.datatransfer.Transferable#getTransferData(java.awt.datatransfer.DataFlavor) */ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (flavor == null) { throw new NullPointerException(); } if (flavor == flavors[0]) { return " " + variableStructure.getName(); } else if (flavor == flavors[1]) { return variableStructure; } else { throw new UnsupportedFlavorException(flavor); } } } /** * Transferable object used by drag and drop between Qualification and * Variable display panels. * * This class represents a qualification from a Qualification Panel, and is * created by the Qualification Display Transfer Handler. * * Its flavours are String (represented by a fully qualified name), and * QualificationStructure (which holds details about this qualification). * * @author Iulian Radu */ static final class QualificationPanelTransferable implements Transferable { /** Structure which holds this transferable's details */ static class QualificationStructure { private final String name; private final ModuleName module; private final SourceIdentifier.Category type; private final QualificationsDisplay.QualificationsDisplayList parentDisplayList; /** * Constructor * * @param qualificationName * unqualified name of qualified entity * @param module * module it belongs to * @param type * entity type * @param parentDisplayList * the qualifications display list which created this * transferable */ private QualificationStructure( String qualificationName, ModuleName module, SourceIdentifier.Category type, QualificationsDisplay.QualificationsDisplayList parentDisplayList) { this.name = qualificationName; this.module = module; this.type = type; this.parentDisplayList = parentDisplayList; } String getName() { return name; } ModuleName getModule() { return module; } SourceIdentifier.Category getType() { return type; } QualificationsDisplay.QualificationsDisplayList getParentDisplayList() { return parentDisplayList; } } /** Transferable data */ private final QualificationStructure qualificationStructure; // Transferable flavors protected static final DataFlavor qualificationStructureFlavor = new DataFlavor(QualificationStructure.class, "Qualification Structure"); private final DataFlavor[] flavors = {DataFlavor.stringFlavor, qualificationStructureFlavor}; /** * Creates a Transferable capable of transferring the specified qualification */ public QualificationPanelTransferable( String name, ModuleName module, SourceIdentifier.Category type, QualificationsDisplay.QualificationsDisplayList parentDisplayList) { qualificationStructure = new QualificationStructure(name, module, type, parentDisplayList); } /** * @see java.awt.datatransfer.Transferable#getTransferDataFlavors() */ public DataFlavor[] getTransferDataFlavors() { return flavors.clone(); } /** * @see java.awt.datatransfer.Transferable#getTransferData(java.awt.datatransfer.DataFlavor) */ public boolean isDataFlavorSupported(DataFlavor flavor) { for (final DataFlavor dataFlavor : flavors) { if (flavor.equals(dataFlavor)) { return true; } } return false; } /** * @see java.awt.datatransfer.Transferable#getTransferData(java.awt.datatransfer.DataFlavor) */ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (flavor == null) { throw new NullPointerException(); } if (flavor == flavors[0]) { return " " + QualifiedName.make(qualificationStructure.getModule(), qualificationStructure.getName()).getQualifiedName(); } else if (flavor == flavors[1]) { return qualificationStructure; } else { throw new UnsupportedFlavorException(flavor); } } } /** * Drag and drop transfer handler for Qualification Display. Exports * functions as QualificationPanelTransferables on drag out. Imports * argument as VariablePanelTransferables, and converts to first available * function if possible. * * @author Iulian Radu */ private class QualificationsDisplayTransferHandler extends TransferHandler { private static final long serialVersionUID = -4050162641138759864L; /** * Creates a transferable for the component qualification panel. * * @see javax.swing.TransferHandler#createTransferable(javax.swing.JComponent) */ @Override protected Transferable createTransferable(JComponent c) { QualificationPanel panel = ((QualificationsDisplay.QualificationsDisplayList)c).getClickedPanel(); if (panel == null) { return null; } AdvancedCALEditor.PositionlessIdentifier identifier = panel.getIdentifier(); if ((!identifier.getQualificationType().isCodeQualified()) && (identifier.getCategory() == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD)) { // Export function as qualification transferable to variables display return new QualificationPanelTransferable(identifier.getName(), identifier.getResolvedModuleName(), identifier.getCategory(), (QualificationsDisplay.QualificationsDisplayList) c); } else { // Anything else is exported as a simple fully qualified string, // since it should not be draggable to variables display return new StringSelection(" " + QualifiedName.make(identifier.getResolvedModuleName(), identifier.getName()).getQualifiedName()); } } /** * @see javax.swing.TransferHandler#getSourceActions(javax.swing.JComponent) */ @Override public int getSourceActions(JComponent c) { return TransferHandler.MOVE; } /** * Imports variables from a variable display located in this editor. * * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, * java.awt.datatransfer.Transferable) */ @Override public boolean importData(JComponent c, Transferable t) { if (canImport(c, t.getTransferDataFlavors())) { try { // Can only import from variable panels VariablePanelTransferable.VariableStructure variable = (VariablePanelTransferable.VariableStructure)t.getTransferData(VariablePanelTransferable.variableStructureFlavor); if (variable.getParentDisplayList() != CodeGemEditor.this.getGemCodePanel().getVariablesDisplay().getListComponent()) { // Drag originated in a different code gem editor; // ignore it. return false; } if (variable.isTransformationAllowed()) { changeArgumentToDefaultFunction(variable.getName()); } return true; } catch (UnsupportedFlavorException ufe) { } catch (IOException ioe) { } } return false; } /** * @see javax.swing.TransferHandler#exportDone(javax.swing.JComponent, * java.awt.datatransfer.Transferable, int) */ @Override protected void exportDone(JComponent c, Transferable data, int action) { ((QualificationsDisplay.QualificationsDisplayList)c).externalDropComplete(); } /** * @see javax.swing.TransferHandler#canImport(javax.swing.JComponent, * java.awt.datatransfer.DataFlavor[]) */ @Override public boolean canImport(JComponent c, DataFlavor[] flavors) { for (final DataFlavor dataFlavor : flavors) { if (dataFlavor == VariablePanelTransferable.variableStructureFlavor) { return true; } } return false; } } /** * Drag and drop transfer handler for Variables Display. Exports arguments * as VariablePanelTransferables on drag out. Imports functions as * QualificationPanelTransferables, and converts them to arguments. * * @author Iulian Radu */ private class VariablesDisplayTransferHandler extends TransferHandler { private static final long serialVersionUID = -5327752640679311974L; /** * Creates a transferable with the data contained in the panel component * * @see javax.swing.TransferHandler#createTransferable(javax.swing.JComponent) */ @Override protected Transferable createTransferable(JComponent c) { VariablePanel panel = ((VariablesDisplay.VariablesDisplayList)c).getClickedPanel(); if (panel != null) { AdvancedCALEditor.PositionlessIdentifier identifier = panel.getIdentifier(); return new VariablePanelTransferable(identifier.getName(), (VariablesDisplay.VariablesDisplayList)c, !identifier.getQualificationType().isCodeQualified() && isArgumentFormChangeAllowed(identifier.getName())); } return null; } /** * @see javax.swing.TransferHandler#getSourceActions(javax.swing.JComponent) */ @Override public int getSourceActions(JComponent c) { return TransferHandler.MOVE; } /** * Imports a qualification from a qualification panel located in this editor * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, java.awt.datatransfer.Transferable) */ @Override public boolean importData(JComponent c, Transferable t) { if (canImport(c, t.getTransferDataFlavors())) { try { // Can only import from qualification panels QualificationPanelTransferable.QualificationStructure qualification = (QualificationPanelTransferable.QualificationStructure)t.getTransferData(QualificationPanelTransferable.qualificationStructureFlavor); if (qualification.getParentDisplayList() != CodeGemEditor.this.getGemCodePanel().getQualificationsDisplay().getListComponent()) { // Drag originated in a different code gem editor; ignore it. return false; } changeQualificationToArgument(qualification.getName(), qualification.getModule()); return true; } catch (UnsupportedFlavorException e) { } catch (IOException e) { } } return false; } /** * @see javax.swing.TransferHandler#exportDone(javax.swing.JComponent, java.awt.datatransfer.Transferable, int) */ @Override protected void exportDone(JComponent c, Transferable data, int action) { ((VariablesDisplay.VariablesDisplayList) c).externalDropComplete(); } /** * @see javax.swing.TransferHandler#canImport(javax.swing.JComponent, * java.awt.datatransfer.DataFlavor[]) */ @Override public boolean canImport(JComponent c, DataFlavor[] flavors) { for (final DataFlavor dataFlavor : flavors) { if (dataFlavor == QualificationPanelTransferable.qualificationStructureFlavor) { return true; } } return false; } } /** * Class providing custom popup menus for the CAL editor, qualification and variables displays. * @author Iulian Radu */ public class PopupMenuProvider implements AdvancedCALEditor.IdentifierPopupMenuProvider { /** * Focus listener for editor popup menus. On focus lost, display * selections are cleared */ private class EditorMenuFocusListener implements PopupMenuListener { public void popupMenuCanceled(PopupMenuEvent e) { gemCodePanel.getQualificationsDisplay().clearSelection(); gemCodePanel.getVariablesDisplay().clearSelection(); } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { gemCodePanel.getQualificationsDisplay().clearSelection(); gemCodePanel.getVariablesDisplay().clearSelection(); } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } } private final EditorMenuFocusListener editorMenuFocusListener = new EditorMenuFocusListener(); /** * Menu item for converting an argument or ambiguity to a proper qualification * * @author Iulian Radu */ private class ToQualificationMenuItem extends JCheckBoxMenuItem implements ActionListener { private static final long serialVersionUID = -8016370644205989220L; private final String unqualifiedName; private final ModuleName moduleName; private final SourceIdentifier.Category type; private final boolean isArgument; /** * Constructor * * @param unqualifiedName * unqualified name of the identifier * @param moduleName * module which the identifier will belong to if menu * clicked * @param type * type of identifier * @param isArgument * whether the identifier is an argument */ ToQualificationMenuItem(String unqualifiedName, ModuleName moduleName, SourceIdentifier.Category type, boolean isArgument) { super(QualifiedName.make(moduleName, unqualifiedName) .getQualifiedName()); this.unqualifiedName = unqualifiedName; this.moduleName = moduleName; this.setIcon(getTypeIcon(type)); this.type = type; this.isArgument = isArgument; this.addActionListener(this); } /** * Converts the identifier to the proper qualification * * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) */ public void actionPerformed(ActionEvent evt) { Object eventSource = evt.getSource(); if (eventSource == this) { if (isArgument) { changeArgumentToQualification(unqualifiedName, moduleName); } else { changeAmbiguityToQualification(unqualifiedName, moduleName, type); } } } /** * Overwrite tooltip location to always be on the right side of the * menu. * * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent) */ @Override public Point getToolTipLocation(MouseEvent e) { return new Point(this.getWidth(), 0); } } /** * Menu item for converting a function to an argument * * @author Iulian Radu */ private class ToArgumentMenuItem extends JCheckBoxMenuItem implements ActionListener { private static final long serialVersionUID = 1891597337246051104L; private final String unqualifiedName; private final ModuleName moduleName; /** * Constructor * * @param unqualifiedName * name of the identifier * @param moduleName * module name of the identifier. Can be null if the identifier is unqualified. * @param active * whether this menu item represents the current form of * the identifier */ ToArgumentMenuItem(String unqualifiedName, ModuleName moduleName, boolean active) { super(GemCutter.getResourceString("CGE_Argument")); this.unqualifiedName = unqualifiedName; this.moduleName = moduleName; this.setIcon(POPUP_MAKE_ARGUMENT_ICON); this.setState(active); if (!active) { // Only act if not active this.addActionListener(this); } } /** * Converts the qualification to an argument * * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) */ public void actionPerformed(ActionEvent evt) { Object eventSource = evt.getSource(); if (eventSource == this) { changeQualificationToArgument(unqualifiedName, moduleName); } } /** * Overwrite tooltip location to always be on the right side of the menu. * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent) */ @Override public Point getToolTipLocation(MouseEvent e) { return new Point(this.getWidth(), 0); } } /** * Menu item for renaming a local variable * * @author Iulian Radu */ private class RenameLocalVarMenuItem extends JMenuItem implements ActionListener { private static final long serialVersionUID = 4520386950737427876L; private final CodeAnalyser.AnalysedIdentifier identifier; RenameLocalVarMenuItem(CodeAnalyser.AnalysedIdentifier identifier) { super(GemCutter.getResourceString("CGE_RenameVariable")); this.identifier = identifier; addActionListener(this); } public void actionPerformed(ActionEvent evt) { // Get the state of the recording edits flag final boolean recordingEdits = CodeGemEditor.this.recordingEditStates; // The following objects will hold the current editor state final StateEdit stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_CodeChange")); final PartInput[] oldInputs = codeGem.getInputParts(); // We will disable the recording edits flag while the renaming occurs. // Once renaming completes, we commit an edit if the renaming is // not canceled, then reenable the recording edits flag. // Because the editor has a parseTimer checking code every PARSE_TIMER_PERIOD ms, // we cannot enable recording edits flag before this time (since an edit state may be // recorded). Thus we will enable the recording edit states flag only after the next // possible parseTimer tick, by use of the following timer. final Timer editEnableTimer = new Timer(PARSE_TIMER_PERIOD + 1, new ActionListener() { public void actionPerformed(ActionEvent e) { CodeGemEditor.this.recordingEditStates = recordingEdits; } }); editEnableTimer.setRepeats(false); // Prepare commit/cancel listener for the symbol renamer AdvancedCALEditor.SymbolRenamerListener renameListener = new AdvancedCALEditor.SymbolRenamerListener() { public void renameCanceled(String oldName) { // Abort the state edit stateEdit.die(); // Enable recording edits after the next parse tick editEnableTimer.start(); } public void renameDone(String oldName, String newName) { // Post the edit to the edit handler. stateEdit.end(); if (CodeGemEditor.this.editHandler != null) { CodeGemEditor.this.editHandler.definitionEdited(CodeGemEditor.this, oldInputs, stateEdit); } // Enable recording edits after the next parse tick editEnableTimer.start(); } }; CodeGemEditor.this.recordingEditStates = false; gemCodePanel.getCALEditorPane().enterRenameMode(identifier, renameListener); } } /** * Menu item for changing module of the identifier * * @author Iulian Radu */ private class ModuleChangeMenuItem extends JCheckBoxMenuItem implements ActionListener { private static final long serialVersionUID = -4045805968225327825L; private final String unqualifiedName; private final ModuleName newModuleName; private final SourceIdentifier.Category type; /** * Constructor * * @param unqualifiedName * unqualified name of the identifier * @param newModuleName * module to which the identifier will be switched by the * menu item. <em>Cannot</em> be null. * @param type * type of identifier * @param active * whether this menu item represents the current form of * the identifier */ ModuleChangeMenuItem(String unqualifiedName, ModuleName newModuleName, SourceIdentifier.Category type, boolean active) { super(QualifiedName.make(newModuleName, unqualifiedName) .getQualifiedName()); this.unqualifiedName = unqualifiedName; this.newModuleName = newModuleName; this.type = type; this.setIcon(getTypeIcon(type)); this.setState(active); if (!active) { // Only act if not active this.addActionListener(this); } } /** * Informs all the listeners that a module change is requested * * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) */ public void actionPerformed(ActionEvent evt) { Object eventSource = evt.getSource(); if (eventSource == this) { changeQualificationModule(unqualifiedName, newModuleName, type); } } /** * Overwrite tooltip location to always be on the right side of the * menu. * * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent) */ @Override public Point getToolTipLocation(MouseEvent e) { return new Point(this.getWidth(), 0); } } /** * Menu item for editing metadata of the identifier * * @author Iulian Radu */ private class EditMetadataMenuItem extends JMenuItem implements ActionListener { private static final long serialVersionUID = -8256211170440664282L; private final QualifiedName identifierName; private final SourceIdentifier.Category identifierType; EditMetadataMenuItem(QualifiedName identifierName, SourceIdentifier.Category identifierType) { super(GemCutter.getResourceString("PopItem_EditGemProperties")); this.identifierName = identifierName; this.identifierType = identifierType; this.addActionListener(this); this.setIcon(POPUP_NAVEDIT_ICON); } public void actionPerformed(ActionEvent evt) { Object eventSource = evt.getSource(); if (eventSource == this) { ScopedEntity entity = CodeAnalyser.getVisibleModuleEntity(identifierName, identifierType, CodeGemEditor.this.perspective.getWorkingModuleTypeInfo()); NavAddress address = NavAddress.getAddress(entity); navigatorOwner.editMetadata(address); } } } /** * Menu item for viewing metadata of the identifier * * @author Iulian Radu */ private class ViewMetadataMenuItem extends JMenuItem implements ActionListener { private static final long serialVersionUID = 6152554485823491115L; private final QualifiedName identifierName; private final SourceIdentifier.Category identifierType; ViewMetadataMenuItem(QualifiedName identifierName, SourceIdentifier.Category identifierType) { super(GemCutter.getResourceString("PopItem_ViewGemProperties")); this.identifierName = identifierName; this.identifierType = identifierType; this.addActionListener(this); this.setIcon(POPUP_NAVVIEW_ICON); } public void actionPerformed(ActionEvent evt) { Object eventSource = evt.getSource(); if (eventSource == this) { ScopedEntity entity = CodeAnalyser.getVisibleModuleEntity(identifierName, identifierType, CodeGemEditor.this.perspective.getWorkingModuleTypeInfo()); NavAddress address = NavAddress.getAddress(entity); navigatorOwner.displayMetadata(address, true); } } } /** * Create menu for a qualification. This is the menu for a qualification * panel, or for a CAL editor identifier which is not an argument. * * @param identifier * @return qualification popup menu */ private JPopupMenu getQualificationPopupMenu(AdvancedCALEditor.PositionlessIdentifier identifier) { String unqualifiedName = identifier.getName(); ModuleName moduleName = identifier.getResolvedModuleName(); SourceIdentifier.Category type = identifier.getCategory(); boolean locallyResolved = (moduleName.equals(perspective.getWorkingModuleName())); boolean isCodeQualified = (identifier.getQualificationType() != QualificationType.UnqualifiedResolvedTopLevelSymbol); JPopupMenu menu = new JPopupMenu(); // Add metadata view/edit items if (navigatorOwner != null) { addMetadataChangeMenuItems(menu, QualifiedName.make(moduleName, unqualifiedName), type); menu.addSeparator(); } // Add module change items AdvancedCALEditor calEditor = gemCodePanel.getCALEditorPane(); if (isCodeQualified) { if (locallyResolved) { // We can switch local qualifications to arguments if (isQualificationFormChangeAllowed(type)) { JMenuItem toArgumentItem = new ToArgumentMenuItem(unqualifiedName, moduleName, false); toArgumentItem.setToolTipText(GemCutter.getResourceString("CGE_To_Argument")); menu.add(toArgumentItem); } // Or keep in the current form { JCheckBoxMenuItem newItem = new ModuleChangeMenuItem(unqualifiedName, moduleName, type, true); newItem.setToolTipText(calEditor.getMetadataToolTipText(unqualifiedName, moduleName, type, perspective.getWorkingModuleTypeInfo())); menu.add(newItem); } } else { // Cannot change form; just display grayed current form JCheckBoxMenuItem newItem = new ModuleChangeMenuItem(unqualifiedName, moduleName, type, true); newItem.setToolTipText(calEditor.getMetadataToolTipText(unqualifiedName, moduleName, type, perspective.getWorkingModuleTypeInfo())); newItem.setEnabled(false); menu.add(newItem); } } else { // If this is an unqualified symbol // Add to-argument change item if (isQualificationFormChangeAllowed(type)) { JMenuItem toArgumentItem = new ToArgumentMenuItem(unqualifiedName, moduleName, false); toArgumentItem.setToolTipText(GemCutter.getResourceString("CGE_To_Argument")); menu.add(toArgumentItem); } // Add module change items List<ModuleName> candidateModules = CodeAnalyser.getModulesContainingIdentifier(unqualifiedName, type, perspective.getWorkingModuleTypeInfo()); for (final ModuleName newModule : candidateModules) { JCheckBoxMenuItem newItem = new ModuleChangeMenuItem( unqualifiedName, newModule, type, (newModule .equals(moduleName))); newItem.setToolTipText(calEditor.getMetadataToolTipText( unqualifiedName, newModule, type, perspective .getWorkingModuleTypeInfo())); menu.add(newItem); } } return menu; } /** * Create menu for an argument. This is the menu for a variable panel, * or for a CAL editor identifier which is an argument. * * @param identifier * @return argument popup menu */ private JPopupMenu getArgumentPopupMenu(AdvancedCALEditor.PositionlessIdentifier identifier) { String unqualifiedName = identifier.getName(); boolean isCodeQualified = false; JPopupMenu menu = new JPopupMenu(); // If argument to function change is not allowed, we will still display // possible changes to modules, but disable them. boolean changeAllowed = isArgumentFormChangeAllowed(unqualifiedName); JMenuItem toArgumentItem = new ToArgumentMenuItem(unqualifiedName, null, true); if (!changeAllowed) { toArgumentItem.setEnabled(false); toArgumentItem.setToolTipText(GemCutter.getResourceString("CGE_Cannot_Transform_Argument")); } else { toArgumentItem.setToolTipText(GemCutter.getResourceString("CGE_Is_Argument")); } menu.add(toArgumentItem); List<ModuleName> candidateModules = CodeAnalyser .getModulesContainingIdentifier( unqualifiedName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, perspective.getWorkingModuleTypeInfo()); AdvancedCALEditor calEditor = gemCodePanel.getCALEditorPane(); if (isCodeQualified && (candidateModules.size() > 0)) { // If code qualified, can only be switched to the current module ModuleName moduleName = perspective.getWorkingModuleName(); if ((candidateModules.iterator().next()).equals(moduleName)) { JMenuItem newItem = new ToQualificationMenuItem( unqualifiedName, moduleName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, true); if (!changeAllowed) { newItem.setToolTipText(GemCutter.getResourceString("CGE_Cannot_Transform_Argument")); newItem.setEnabled(false); } else { newItem.setToolTipText(calEditor.getMetadataToolTipText(unqualifiedName, moduleName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, perspective.getWorkingModuleTypeInfo())); } menu.add(newItem); } } else { // Can be switched to any module for (final ModuleName moduleName : candidateModules) { JMenuItem newItem = new ToQualificationMenuItem(unqualifiedName, moduleName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, true); if (!changeAllowed) { newItem.setToolTipText(GemCutter.getResourceString("CGE_Cannot_Transform_Argument")); newItem.setEnabled(false); } else { newItem.setToolTipText(calEditor.getMetadataToolTipText(unqualifiedName, moduleName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, perspective.getWorkingModuleTypeInfo())); } menu.add(newItem); } } return menu; } /** * Create menu for an ambiguity. This is the menu for a text identifier * which could not be qualified; the options are to turn it into an * argument or qualify to a function. * * @param identifier * @return ambiguity popup menu */ private JPopupMenu getAmbiguityPopupMenu(AdvancedCALEditor.PositionlessIdentifier identifier) { String unqualifiedName = identifier.getName(); SourceIdentifier.Category type = identifier.getCategory(); JPopupMenu menu = new JPopupMenu(); // If argument to function change is not allowed, we will still // display // possible changes to modules, but disable them. if (type == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) { JMenuItem toArgumentItem = new ToArgumentMenuItem( unqualifiedName, null, false); toArgumentItem.setToolTipText(GemCutter .getResourceString("CGE_To_Argument")); menu.add(toArgumentItem); } AdvancedCALEditor calEditor = gemCodePanel.getCALEditorPane(); List<ModuleName> candidateModules = CodeAnalyser .getModulesContainingIdentifier(unqualifiedName, type, perspective.getWorkingModuleTypeInfo()); for (final ModuleName moduleName : candidateModules) { JMenuItem newItem = new ToQualificationMenuItem( unqualifiedName, moduleName, type, false); newItem.setToolTipText(calEditor.getMetadataToolTipText( unqualifiedName, moduleName, type, perspective .getWorkingModuleTypeInfo())); menu.add(newItem); } return menu; } /** * Popup menu listener used for highlighting a specified local variable * identifier when the menu is active. * * @author Iulian Radu */ private class HighlightVariablePopupListener implements PopupMenuListener { private final AdvancedCALEditor.SymbolHighlighter referenceHighlighter; private final AdvancedCALEditor.SymbolHighlighter definitionHighlighter; public HighlightVariablePopupListener(CodeAnalyser.AnalysedIdentifier identifier) { Pair<SymbolHighlighter, SymbolHighlighter> highlighters = gemCodePanel.getCALEditorPane().createLocalVariableHighlighters(identifier); this.referenceHighlighter = highlighters.fst(); this.definitionHighlighter = highlighters.snd(); } public void popupMenuCanceled(PopupMenuEvent e) { referenceHighlighter.removeHighlights(); definitionHighlighter.removeHighlights(); } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { referenceHighlighter.removeHighlights(); definitionHighlighter.removeHighlights(); } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { referenceHighlighter.applyHighlights(); definitionHighlighter.applyHighlights(); } } /** * Get the popup menu for a clicked identifier * * @param identifier * the identifier selected * @return JPopupMenu the popup menu to be displayed; null if no menu * for this item */ public JPopupMenu getPopupMenu(AdvancedCALEditor.PositionlessIdentifier identifier) { // Determine if identifier is an argument, ambiguity or // qualification QualificationType qualificationType = identifier.getQualificationType(); // Argument ? if (qualificationType == QualificationType.UnqualifiedArgument) { gemCodePanel.getVariablesDisplay().selectPanelForArgument(identifier.getName()); gemCodePanel.getQualificationsDisplay().clearSelection(); JPopupMenu menu = getArgumentPopupMenu(identifier); menu.addPopupMenuListener(editorMenuFocusListener); return menu; } // Qualified symbol ? if (qualificationType.isResolvedTopLevelSymbol()) { gemCodePanel.getQualificationsDisplay().selectPanelForIdentifier(identifier); gemCodePanel.getVariablesDisplay().clearSelection(); JPopupMenu menu = getQualificationPopupMenu(identifier); menu.addPopupMenuListener(editorMenuFocusListener); return menu; } // Ambiguity ? if (qualificationType == QualificationType.UnqualifiedAmbiguousTopLevelSymbol) { gemCodePanel.getQualificationsDisplay().selectPanelForIdentifier(identifier); gemCodePanel.getVariablesDisplay().clearSelection(); JPopupMenu menu = getAmbiguityPopupMenu(identifier); menu.addPopupMenuListener(editorMenuFocusListener); return menu; } // Local Variable ? if (qualificationType == QualificationType.UnqualifiedLocalVariable) { CodeAnalyser.AnalysedIdentifier analysedIdentifier = identifier.getReference(); if (analysedIdentifier != null) { JPopupMenu menu = new JPopupMenu(); menu.add(new RenameLocalVarMenuItem(analysedIdentifier)); menu.addPopupMenuListener(new HighlightVariablePopupListener(analysedIdentifier)); return menu; } } // This menu only handles ambiguities, arguments, or qualified // identifiers gemCodePanel.getQualificationsDisplay().clearSelection(); gemCodePanel.getVariablesDisplay().clearSelection(); return null; } /** * Displays a popup menu for interacting with the specified identifier. * * @param identifier * clicked identifier * @param p * point where the menu should be displayed (relative to * invoker) * @param invoker * component in which to display the menu */ public void showMenu(AdvancedCALEditor.PositionlessIdentifier identifier, Point p, Component invoker) { JPopupMenu menu = getPopupMenu(identifier); if (menu != null) { menu.show(invoker, p.x, p.y); } } /** * Retrieves the icon associated to a given type * * @param type * @return icon associated to type */ public ImageIcon getTypeIcon(SourceIdentifier.Category type) { if (type == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) { return POPUP_FUNCTION_ICON; } else if (type == SourceIdentifier.Category.DATA_CONSTRUCTOR) { return POPUP_CONSTRUCTOR_ICON; } else if (type == SourceIdentifier.Category.TYPE_CLASS) { return POPUP_CLASS_ICON; } else if (type == SourceIdentifier.Category.TYPE_CONSTRUCTOR) { return POPUP_TYPE_ICON; } else { throw new IllegalArgumentException(); } } /** * Generate menu for viewing/editing properties of an identifier * * @param menu * items will be added to this menu * @param qualifiedName * qualified name of identifier * @param type * identifier category */ public void addMetadataChangeMenuItems(JPopupMenu menu, QualifiedName qualifiedName, SourceIdentifier.Category type) { if (metadataEditAllowed) { menu.add(new EditMetadataMenuItem(qualifiedName, type)); } menu.add(new ViewMetadataMenuItem(qualifiedName, type)); } } /** * This is a transfer handler for the code gem editor component. * * It is able of importing: - simple text, as a regular text component - gem * entities and gem drawers, inserting the respective names in the code text - * code text from another code editor, along with qualifications and * arguments * * It exports: - code gem text, mappings and arguments for other code gem * editors - simple text flavour of the code, in the form of fully qualified * code * * Note: The class assumes that it is attached to an AdvancedCALEditor. * * @author Iulian Radu */ public static class EditorTextTransferHandler extends TransferHandler { private static final long serialVersionUID = -2949337896560340803L; /** Analyser of editor code */ private final CodeAnalyser codeAnalyser; /** * Tracks names of identifiers which have been constrained to * arguments If this is left null, the code is assumed to not have * arguments, and import/export of arguments is not done. */ private Set<String> varNamesWhichAreArgs = null; /** * Qualification map of identifiers which have been explicitly resolved * by the user If this is left null, the code is assumed to not manage * qualification mappings, thus import/export handles fully qualified * code */ private CodeQualificationMap userQualifiedIdentifiers = null; /** Transfer handler for basic text */ private final TransferHandler textTransferHandler; /** * Constructor * * @param textTransferHandler * @param analyser */ public EditorTextTransferHandler(TransferHandler textTransferHandler, CodeAnalyser analyser) { this.textTransferHandler = textTransferHandler; this.codeAnalyser = analyser; } /** * Sets the set of argument names to use * @param varNamesWhichAreArgs */ public void setArgumentNames(Set<String> varNamesWhichAreArgs) { this.varNamesWhichAreArgs = varNamesWhichAreArgs; } /** * Sets the qualification map to use * @param userQualifiedIdentifiers */ public void setUserQualifiedIdentifiers(CodeQualificationMap userQualifiedIdentifiers) { this.userQualifiedIdentifiers = userQualifiedIdentifiers; } /** * Creates transferable holding the selected text and a qualification * map filled with qualifications of the selected identifiers. * * @see javax.swing.TransferHandler#createTransferable(javax.swing.JComponent) */ @Override protected Transferable createTransferable(JComponent c) { AdvancedCALEditor editor = (AdvancedCALEditor) c; String visibleText = editor.getSelectedText(); if (visibleText == null) { return null; } // Qualify the selected portion of text String fullyQualifiedText = editor.getQualifiedCodeText(editor.getSelectionStart(), editor.getSelectionEnd(), codeAnalyser); // Run through the selected identifiers, and build up a map of the // used qualifications and arguments. Set<String> argumentNames = new LinkedHashSet<String>(); CodeQualificationMap qualificationMap = new CodeQualificationMap(); List<AnalysedIdentifier> selectedIdentifiers = editor.getSelectedIdentifiers(editor.getSelectionStart(), editor.getSelectionEnd()); for (final CodeAnalyser.AnalysedIdentifier identifier : selectedIdentifiers) { if (identifier.getQualificationType() == QualificationType.UnqualifiedResolvedTopLevelSymbol) { qualificationMap.putQualification(identifier.getName(), identifier.getResolvedModuleName(), identifier.getCategory()); } else if ((varNamesWhichAreArgs != null) && (identifier.getQualificationType() == QualificationType.UnqualifiedArgument) && (varNamesWhichAreArgs.contains(identifier.getName()))) { argumentNames.add(identifier.getName()); } } return new CodeTextTransferable(visibleText, fullyQualifiedText, qualificationMap, argumentNames, editor, editor.getSelectionStart(), editor.getSelectionEnd()); } /** * @see javax.swing.TransferHandler#getSourceActions(javax.swing.JComponent) */ @Override public int getSourceActions(JComponent c) { return TransferHandler.COPY_OR_MOVE; } /** * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, * java.awt.datatransfer.Transferable) */ @Override public boolean importData(JComponent c, Transferable t) { DataFlavor[] flavors = t.getTransferDataFlavors(); if (canImport(c, flavors)) { try { // Handle custom flavors for (final DataFlavor dataFlavor : flavors) { if (dataFlavor == CodeTextTransferable.codeStructureFlavor) { // Code text from a code editor return importCodeText(c, t); } else if (SingleGemEntityDataFlavor.getSingleGemEntityDataFlavor().equals(dataFlavor)) { // entity from the browser view return importGemEntity(c, t); } else if (GemDrawerSelection.getGemDrawerDataFlavor().equals(dataFlavor)) { return importGemDrawer(c, t); } } // Not one of our flavours, but can be imported via the original handler return textTransferHandler.importData(c, t); } catch (UnsupportedFlavorException ufe) { throw new IllegalStateException("editor transfer handler trying to import invalid flavor"); } catch (IOException ioe) { throw new IllegalStateException("editor transfer handler encountered exception: " + ioe); } } // Cannot import data return false; } /** * Imports data from a transferable containing pasted text from a code gem. * @param c editor which has produced this * @param t transferable * @throws UnsupportedFlavorException * @throws IOException */ private boolean importCodeText(JComponent c, Transferable t) throws UnsupportedFlavorException, IOException { // Get the code text structure CodeTextTransferable.CodeTextStructure codeTextStructure = (CodeTextTransferable.CodeTextStructure)t.getTransferData(CodeTextTransferable.codeStructureFlavor); // Combine the new map entries into our own if (userQualifiedIdentifiers != null) { CodeQualificationMap codeQualificationMap = codeTextStructure.getQualificationMap(); appendToEditorMap(codeQualificationMap, SourceIdentifier.Category.DATA_CONSTRUCTOR); appendToEditorMap(codeQualificationMap, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); appendToEditorMap(codeQualificationMap, SourceIdentifier.Category.TYPE_CLASS); appendToEditorMap(codeQualificationMap, SourceIdentifier.Category.TYPE_CONSTRUCTOR); } // Combine the argument names into our own if (varNamesWhichAreArgs != null) { for (final String argument : codeTextStructure.getArgumentNames()) { // Only add arguments if they are not already mapped if (userQualifiedIdentifiers.getQualifiedName(argument, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) == null) { varNamesWhichAreArgs.add(argument); } } } // Insert the text into the editor AdvancedCALEditor editor = (AdvancedCALEditor)c; if (userQualifiedIdentifiers != null) { editor.replaceSelection(codeTextStructure.getVisibleCode()); } else { editor.replaceSelection(codeTextStructure.getFullyQualifiedCode()); } return true; } /** * Imports data from a transferable containing a gem entity * * @param c * @param t * @return whether the import succeeded * @throws UnsupportedFlavorException * @throws IOException * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, * java.awt.datatransfer.Transferable) */ private boolean importGemEntity(JComponent c, Transferable t) throws UnsupportedFlavorException, IOException { GemEntity entity = (GemEntity)t.getTransferData(SingleGemEntityDataFlavor.getSingleGemEntityDataFlavor()); AdvancedCALEditor editor = (AdvancedCALEditor)c; editor.getInputContext().endComposition(); // TODO: If Gem Entities ever contain more than functional agents, // category needs to be updated SourceIdentifier.Category category = (entity.isDataConstructor() ? SourceIdentifier.Category.DATA_CONSTRUCTOR : SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); editor.replaceSelection(" "); insertEditorQualification(editor, entity.getName(), category, userQualifiedIdentifiers, false); return true; } /** * Inserts the qualification into the editor text at the specified location. * * If the qualification does not exist in the qualification map, or is * mapped to the same module, it can be inserted as unqualified and the * qualification map is updated with the corresponding module. * Otherwise, a fullly qualified name is inserted. * * @param editor * @param completedName * @param identifierCategory */ public static void insertEditorQualification(AdvancedCALEditor editor, QualifiedName completedName, SourceIdentifier.Category identifierCategory, CodeQualificationMap userQualifiedIdentifiers, boolean insertQualified) { // Do we have this in our map ? QualifiedName userMappedName = userQualifiedIdentifiers.getQualifiedName(completedName.getUnqualifiedName(), identifierCategory); if (userMappedName == null) { // No; add it to the map and insert unqualified name into code userQualifiedIdentifiers.putQualification(completedName.getUnqualifiedName(), completedName.getModuleName(), identifierCategory); if (insertQualified){ editor.replaceSelection(completedName.toString()); } else{ editor.replaceSelection(completedName.getUnqualifiedName()); } } else { // We have this identifier mapped already // If it's to the same module, enter as above; if not, enter // fully qualified name. if (!insertQualified && userMappedName.getModuleName().equals(completedName.getModuleName())) { editor.replaceSelection(completedName.getUnqualifiedName()); } else { editor.replaceSelection(completedName.getQualifiedName()); } } } /** * Imports data from a transferable containing a gem drawer * * @param c * @param t * @return whether the import succeeded * @throws UnsupportedFlavorException * @throws IOException * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, * java.awt.datatransfer.Transferable) */ private boolean importGemDrawer(JComponent c, Transferable t) throws UnsupportedFlavorException, IOException { String drawer = (String)t.getTransferData(GemDrawerSelection.getGemDrawerDataFlavor()); // Add to code gem editor via the default transfer handler return textTransferHandler.importData(c, new StringSelection(" " + drawer + ".")); } /** * @see javax.swing.TransferHandler#exportDone(javax.swing.JComponent, * java.awt.datatransfer.Transferable, int) */ @Override protected void exportDone(JComponent c, Transferable data, int action) { if (action == MOVE) { CodeTextTransferable.EditorInfo editorInfo = ((CodeTextTransferable)data).getEditorInfo(); try { editorInfo.getEditor().getDocument().remove(editorInfo.getOffsetStart(), editorInfo.getOffsetEnd() - editorInfo.getOffsetStart()); } catch (BadLocationException e) { throw new IllegalStateException("Cannot remove selected text because selection is invalid"); } } } /** * @see javax.swing.TransferHandler#canImport(javax.swing.JComponent, * java.awt.datatransfer.DataFlavor[]) */ @Override public boolean canImport(JComponent c, DataFlavor[] flavors) { for (final DataFlavor dataFlavor : flavors) { if (dataFlavor == CodeTextTransferable.codeStructureFlavor) { return true; } else if (dataFlavor == SingleGemEntityDataFlavor.getSingleGemEntityDataFlavor()) { return true; } } return textTransferHandler.canImport(c, flavors); } /** * Adds the entries from the specified map into the editor qualification * map, if these mappings do not already exists. * * @param insertedMap * map which is imported into our own * @param type * type of entries to transfer */ private void appendToEditorMap(CodeQualificationMap insertedMap, SourceIdentifier.Category type) { Set<String> names = insertedMap.getUnqualifiedNames(type); for (final String entityName : names) { if (userQualifiedIdentifiers.getQualifiedName(entityName, type) == null) { userQualifiedIdentifiers.putQualification(entityName, insertedMap.getQualifiedName(entityName, type).getModuleName(), type); } } } } /** * Structure which holds this transferable's details */ public static class CodeTextTransferable implements Transferable { /** * Structure holding the transfered data */ static class CodeTextStructure { /** Visible code to transfer */ private final String visibleCode; /** Fully qualified code */ private final String fullyQualifiedCode; /** Qualification map for the visible code */ private final CodeQualificationMap codeQualificationMap; /** Set of argument names appearing in code */ private final Set<String> argumentNames; /** Constructor */ CodeTextStructure(String visibleCode, String fullyQualifiedCode, CodeQualificationMap qualificationMap, Set<String> argumentNames) { this.fullyQualifiedCode = fullyQualifiedCode; this.visibleCode = visibleCode; this.codeQualificationMap = qualificationMap.makeCopy(); this.argumentNames = new LinkedHashSet<String>(argumentNames); } // Accessors String getVisibleCode() { return visibleCode; } CodeQualificationMap getQualificationMap() { return codeQualificationMap; } Set<String> getArgumentNames() { return argumentNames; } String getFullyQualifiedCode() { return fullyQualifiedCode; } } /** * Information about the editor component which owns the text. This is * needed because in MOVE (ex: clipboard cut) operations, the transfer * handler must erase the transfered text from the editor * * @author Iulian Radu */ private static class EditorInfo { /** Actual component */ private final JTextComponent editor; // Start and end offsets of text being transfered copied private final int offsetStart; private final int offsetEnd; /** * Constructor * * @param editor * @param offsetStart * @param offsetEnd */ public EditorInfo(JTextComponent editor, int offsetStart, int offsetEnd) { this.editor = editor; this.offsetStart = offsetStart; this.offsetEnd = offsetEnd; } /** * @return Returns the editor. */ public JTextComponent getEditor() { return editor; } /** * @return Returns the offsetEnd. */ public int getOffsetEnd() { return offsetEnd; } /** * @return Returns the offsetStart. */ public int getOffsetStart() { return offsetStart; } } /** Transferable data */ private final CodeTextStructure codeTextStructure; /** Editor information */ private final EditorInfo editorInfoStructure; // Transferable flavors protected static final DataFlavor codeStructureFlavor = new DataFlavor(CodeTextStructure.class, "Code Text Structure"); private final DataFlavor[] flavors = {DataFlavor.stringFlavor, codeStructureFlavor}; /** * Creates a Transferable capable of transferring the specified code */ public CodeTextTransferable(String codeText, String qualifiedText, CodeQualificationMap qualificationMap, Set<String> argumentNames, JTextComponent editor, int offsetStart, int offsetEnd) { this.codeTextStructure = new CodeTextStructure(codeText, qualifiedText, qualificationMap, argumentNames); this.editorInfoStructure = new EditorInfo(editor, offsetStart, offsetEnd); } /** * @see java.awt.datatransfer.Transferable#getTransferDataFlavors() */ public DataFlavor[] getTransferDataFlavors() { return flavors.clone(); } /** * @see java.awt.datatransfer.Transferable#isDataFlavorSupported(java.awt.datatransfer.DataFlavor) */ public boolean isDataFlavorSupported(DataFlavor flavor) { for (final DataFlavor dataFlavor : flavors) { if (flavor.equals(dataFlavor)) { return true; } } return false; } /** * @see java.awt.datatransfer.Transferable#getTransferData(java.awt.datatransfer.DataFlavor) */ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (flavor == null) { throw new NullPointerException(); } if (flavor == flavors[0]) { return codeTextStructure.getFullyQualifiedCode(); } else if (flavor == flavors[1]) { return codeTextStructure; } else { throw new UnsupportedFlavorException(flavor); } } EditorInfo getEditorInfo() { return editorInfoStructure; } } /** * CodeGemEditor constructor. * * @param parent * the parent of this code editor * @param codeGem * the code gem to edit. * @param editHandler * the handler for edits on the code gem. * @param perspective * the current perspective. * @param typeStringProvider * @param navigatorOwner * owner of metadata editor / viewer * @param metadataEditAllowed * whether metadata editing is allowed * @param codeAnalyser * object used to analyse the code */ public CodeGemEditor(Frame parent, final CodeGem codeGem, EditHandler editHandler, Perspective perspective, TypeStringProvider typeStringProvider, NavFrameOwner navigatorOwner, boolean metadataEditAllowed, CodeAnalyser codeAnalyser) { super(parent); this.codeGem = codeGem; this.perspective = perspective; this.codeAnalyser = codeAnalyser; this.typeStringProvider = typeStringProvider; this.editHandler = editHandler; Argument.NameTypePair[] args = codeGem.getArguments(); if (args.length > 0) { keepInputsInNaturalOrder = false; } varNamesWhichAreArgs = new LinkedHashSet<String>(); for (final NameTypePair arg : args) { varNamesWhichAreArgs.add(arg.getName()); } userQualifiedIdentifiers = codeGem.getQualificationMap().makeCopy(); // Add metadata view/edit support this.navigatorOwner = navigatorOwner; this.metadataEditAllowed = metadataEditAllowed; // Set up the gem code panel gemCodeSyntaxListener = new GemCodeSyntaxListener(perspective); gemCodePanel = new GemCodePanel(codeGem, gemCodeSyntaxListener, perspective); gemCodePanel.setName("GemCodePanel"); // Set up the content pane dialogContentPane = new JPanel(); dialogContentPane.setName("JDialogContentPane"); dialogContentPane.setLayout(new CardLayout()); dialogContentPane.add(gemCodePanel, gemCodePanel.getName()); // Now set up this editor setName("CodeGemEditor"); setSize(600, 380); setContentPane(dialogContentPane); final AutoCompleteManager autoCompleteManager = new AutoCompleteManager(this, perspective); // Update the variables display area, so that the type change listener // update of this area doesn't screw up on preconditions. updateVariablesDisplay(); // Add a listener to the CAL editor that will close this code editor // when ESC is pressed // and invoke autocomplete on ctrl-space. gemCodePanel.getCALEditorPane().addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { processWindowEvent(new WindowEvent(CodeGemEditor.this, WindowEvent.WINDOW_CLOSING)); } // The gesture is control - space - a la Eclipse et al. if ((e.isControlDown()) && (e.getKeyCode() == KeyEvent.VK_SPACE)) { // Show the autocomplete popup just underneath the cursor try { autoCompleteManager.showCodeEditorPopupMenu(); } catch (AutoCompletePopupMenu.AutoCompleteException ex) { gemCodePanel.setErrorMessage("No Autocomplete Available"); } } } }); // Add a listener to activate the gem code panel when we activate this // frame addWindowListener(new WindowAdapter() { @Override public void windowActivated(WindowEvent e) { // Pass focus to the GemCodePanel gemCodePanel.requestFocus(); } }); // add listeners for burn and definition events on the gem codeGem.addBurnListener(new BurnListener() { public void burntStateChanged(BurnEvent e) { // make sure the correct variable panel types show up if (smartsActivated) { doSyntaxSmarts(); } } }); // Add a listener to update the type display when the code gem types change. codeGem.addTypeChangeListener(new TypeChangeListener() { public void typeChanged(TypeChangeEvent e) { Gem.PartConnectable partChanged = e.getPartChanged(); if (partChanged instanceof Gem.PartInput) { updateVariablesDisplay(((Gem.PartInput)partChanged).getInputNum()); } else if (partChanged instanceof Gem.PartOutput) { updateOutputTypePanel(); } } }); // Set up event listeners for Variable Display gemCodePanel.getVariablesDisplay().addPanelEventListener(new VariablesDisplay.PanelEventListener() { public void panelShifted(int argIndex, int shiftAmount) { StateEdit stateEdit = null; PartInput[] oldInputs = null; if (recordingEditStates) { stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_ReorderInputs")); oldInputs = codeGem.getInputParts(); } keepInputsInNaturalOrder = false; shiftInput(argIndex, shiftAmount); updateVariablesDisplay(); if (recordingEditStates) { stateEdit.end(); if (CodeGemEditor.this.editHandler != null) { CodeGemEditor.this.editHandler.definitionEdited(CodeGemEditor.this, oldInputs, stateEdit); } } } public void panelTypeIconDoubleClicked(VariablePanel variablePanel) { AdvancedCALEditor.PositionlessIdentifier identifier = variablePanel.getIdentifier(); if (!identifier.getQualificationType().isCodeQualified() && isArgumentFormChangeAllowed(identifier.getName())) { // Switch to default module changeArgumentToDefaultFunction(identifier.getName()); } } }); // Set up listeners for Qualifications Display gemCodePanel.getQualificationsDisplay().addPanelEventListener(new QualificationsDisplay.PanelEventListener() { public void panelTypeIconDoubleClicked(QualificationPanel panel) { AdvancedCALEditor.PositionlessIdentifier identifier = panel.getIdentifier(); if (!identifier.getQualificationType().isCodeQualified() && isQualificationFormChangeAllowed(identifier.getCategory())) { changeQualificationToArgument(identifier.getName(), identifier.getResolvedModuleName()); } } public void panelModuleLabelDoubleClicked(QualificationPanel panel, Point mousePoint) { popupMenuProvider.showMenu(panel.getIdentifier(), mousePoint, gemCodePanel.getQualificationsDisplay().getListComponent()); } }); // Add popup menu providers PopupMenuProvider popupProvider = new PopupMenuProvider(); gemCodePanel.getQualificationsDisplay().setPopupMenuProvider(popupProvider); gemCodePanel.getVariablesDisplay().setPopupMenuProvider(popupProvider); gemCodePanel.getCALEditorPane().setPopupMenuProvider(popupProvider); // Enable dragging between displays gemCodePanel.getQualificationsDisplay().setDragEnabled(true); gemCodePanel.getQualificationsDisplay().setListTransferHandler(new QualificationsDisplayTransferHandler()); gemCodePanel.getVariablesDisplay().setDragEnabled(true); gemCodePanel.getVariablesDisplay().setListTransferHandler(new VariablesDisplayTransferHandler()); // Enable special copy/paste editorTransferHandler = new EditorTextTransferHandler(gemCodePanel.getCALEditorPane().getTransferHandler(), codeAnalyser); gemCodePanel.getCALEditorPane().setTransferHandler(editorTransferHandler); editorTransferHandler.setArgumentNames(varNamesWhichAreArgs); editorTransferHandler.setUserQualifiedIdentifiers(userQualifiedIdentifiers); // Create a timer for the periodic parse, but only once when the timer elapses parseTimer = new Timer(PARSE_TIMER_PERIOD, new ActionListener() { public void actionPerformed(ActionEvent e) { // Check it's from our friendly timer object if (e.getSource() == parseTimer) { updateGemForTextChange(); } } }); parseTimer.setRepeats(false); // Add the listener for document change events in the Editor gemCodePanel.getCALEditorPane().getDocument().addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent e) { textChanged(); } public void removeUpdate(DocumentEvent e) { textChanged(); } public void changedUpdate(DocumentEvent e) { textChanged(); } }); // Ignore the fact that the code gem isn't good, to update the "last good" state. updateLastGoodState(); } /** * To be called when the text of the code gem changes. Records the change, performs * syntax smarts, and posts the edit to the edit handler. */ private void updateGemForTextChange() { // Get the code gem change. StateEdit stateEdit = null; PartInput[] oldInputs = null; if (recordingEditStates) { stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_CodeChange")); oldInputs = codeGem.getInputParts(); } // Update the code gem. doSyntaxSmarts(); // Post edit state. if (recordingEditStates) { stateEdit.end(); // Post the edit to the edit handler. if (CodeGemEditor.this.editHandler != null) { CodeGemEditor.this.editHandler.definitionEdited(CodeGemEditor.this, oldInputs, stateEdit); } } } /** * Sets whether there should be a delay after the text changes before updateGemForTextChange() * is called. Under normal conditions this should be true so that the method is not called for every keystroke * as a user interactively updates the text. However, this should be to false if the call to updateGemForTextChange() * needs to be called synchronously (ie. if the change is part of a programmatic compound edit). * * @param shouldDelay True if there should be a delay, false otherwise */ void setDelayUpdatingForTextChanges(boolean shouldDelay) { this.delayUpdatingForTextChanges = shouldDelay; } /** * What to do when the text is changed. If not restoring, reset the timer * which will result in us checking everything when the user rests for a * while */ private void textChanged() { if (smartsActivated) { if (delayUpdatingForTextChanges) { parseTimer.restart(); } else { updateGemForTextChange(); } } } /** * Return the code gem for this editor. * * @return CodeGem the code gem for this editor. */ public final CodeGem getCodeGem() { return codeGem; } void setCodeAnalyser(CodeAnalyser codeAnalyser) { this.codeAnalyser = codeAnalyser; } /** * returns the code gem panel * * @return GemCodePanel */ GemCodePanel getGemCodePanel() { return gemCodePanel; } /** * Find out what the new arguments should be to reflect a definition change. * * @param argNamesFromCode * the argument names from the code, in code order * @param newCodeGemType * the new type of the sc defined by the code gem * @return Argument[] the new arguments after the definition change. */ private Argument.NameTypePair[] getNewCodeGemArgs(String[] argNamesFromCode, TypeExpr newCodeGemType) { // Make an argument list int argCount = argNamesFromCode.length; Argument.NameTypePair[] argsFromCode = new Argument.NameTypePair[argCount]; Map <String, NameTypePair>newNameToArgMap = new HashMap<String, NameTypePair>(); if (newCodeGemType != null) { TypeExpr[] typePieces = newCodeGemType.getTypePieces(); for (int i = 0; i < argCount; i++) { argsFromCode[i] = new Argument.NameTypePair(argNamesFromCode[i], typePieces[i]); newNameToArgMap.put(argNamesFromCode[i], argsFromCode[i]); } } else { // No type - broken gem. for (int i = 0; i < argCount; i++) { argsFromCode[i] = new Argument.NameTypePair(argNamesFromCode[i], null); newNameToArgMap.put(argNamesFromCode[i], argsFromCode[i]); } } Argument.NameTypePair[] currentArgs = codeGem.getArguments(); int numCurrentArgs = currentArgs.length; // Create a set of arg names final List<NameTypePair>argsInOrderList; if (keepInputsInNaturalOrder) { argsInOrderList = new ArrayList<NameTypePair>(Arrays.asList(argsFromCode)); // Add connected arguments not in the code. The nth arg is // associated with the nth input for (int i = 0; i < numCurrentArgs; i++) { String inputArgName = currentArgs[i].getName(); PartInput input = oldNameToInputMap.get(inputArgName); if (input != null && input.isConnected() && !newNameToArgMap.containsKey(inputArgName)) { argsInOrderList.add(new Argument.NameTypePair(inputArgName, TypeExpr.makeParametricType())); } } } else { // Try to preserve the last order as much as possible, and add new inputs to the end. // Args in order = args present in new args + new args not present in old args argsInOrderList = new ArrayList<NameTypePair>(); // We also want the new name to arg map to include names for connected args not in the code // so that we don't consider it a new arg and add it to the end. for (int i = 0; i < numCurrentArgs; i++) { String inputArgName = currentArgs[i].getName(); PartInput input = oldNameToInputMap.get(inputArgName); if (input != null && input.isConnected() && !newNameToArgMap.containsKey(inputArgName)) { newNameToArgMap.put(inputArgName, new Argument.NameTypePair(inputArgName, TypeExpr.makeParametricType())); } } // The code args which are new. We'll remove old args from this. List<NameTypePair> newArgs = new ArrayList<NameTypePair>(Arrays.asList(argsFromCode)); // Add args present from lastArgs. Also remove old args from the new args list. for (final NameTypePair lastArg : lastArguments) { Argument.NameTypePair newLastArg = newNameToArgMap.get(lastArg.getName()); if (newLastArg != null) { argsInOrderList.add(newLastArg); // Search through the last args for the one to remove. for (final Argument.NameTypePair newArg : newArgs){ if (newArg.getName().equals(lastArg.getName())) { newArgs.remove(newArg); // not a new arg - remove from new args break; // ConcurrentModificationException is avoided since the iterate } } } } // Now add new args to the end argsInOrderList.addAll(newArgs); } // Convert to an array Argument.NameTypePair[] argsInOrder = new Argument.NameTypePair[argsInOrderList.size()]; argsInOrderList.toArray(argsInOrder); return argsInOrder; } /** * Check whether the current output type is compatible with its connection. * * @param typeCheckInfo * the info to use to check the connectivity of the output. * @return if non-null, the current output type is incompatible with the * inferred type of the output. The inferred type is returned. */ private TypeExpr checkOutputForConnectivity(TypeCheckInfo typeCheckInfo) { // Determine whether the output is compatibly-connected. if (!codeGem.isRootGem() && codeGem.getOutputPart().isConnected()) { TypeExpr inferredOutputType = codeGem.getOutputPart().inferType(typeCheckInfo); if (!GemGraph.typesWillUnify(inferredOutputType, getOutputType(), typeCheckInfo)) { return inferredOutputType; } } return null; } /** * Update the given code gem editor's state according to the validity of its * connections. * * TODOEL: this type of bookkeeping should be handled by the code gem * (editor) owner. The next best call by clients should be to * updateIncompatibleParts(). * * @param typeCheckInfo * the type check info object used to check connectivity of * connections. * @param valueEditorManager * the value editor manager used to check providability of * arguments. */ public void updateForConnectivity(TypeCheckInfo typeCheckInfo, ValueEditorManager valueEditorManager) { Map<PartConnectable, TypeExpr> incompatiblyConnectedPartToInferredTypeMap = new HashMap<PartConnectable, TypeExpr>(); // Determine whether the output is compatibly-connected. TypeExpr incompatibleInferredOutputType = checkOutputForConnectivity(typeCheckInfo); if (incompatibleInferredOutputType != null) { incompatiblyConnectedPartToInferredTypeMap.put(codeGem.getOutputPart(), incompatibleInferredOutputType); } // Find incompatibly-connected inputs. Argument.NameTypePair[] args = codeGem.getArguments(); for (int i = 0; i < args.length; i++) { // Skip if not connected. PartInput input = codeGem.getInputPart(i); if (!input.isConnected()) { continue; } // If there's no arg type, this must be an "orphaned" arg (ie. // doesn't appear in the source). TypeExpr argType = args[i].getType(); if (argType == null) { continue; } // Broken if the arg type doesn't unify with its inferred type, or // if the attached gem // is a value gem and the value system is unable to handle the type // of the argument. TypeExpr inferredInputType = input.inferType(typeCheckInfo); if (!GemGraph.typesWillUnify(argType, inferredInputType, typeCheckInfo) || (input.getConnectedGem() instanceof ValueGem && !valueEditorManager.canInputDefaultValue(argType))) { incompatiblyConnectedPartToInferredTypeMap.put(input, inferredInputType); } } // Update the code gem editor. updateIncompatibleParts(incompatiblyConnectedPartToInferredTypeMap); } /** * Update the internal set of connectable parts on the code gem whose * connections are incompatible with their definitions. TODOEL: remove. This * is requires a call back from the code gem editor's owner after an edit. * External brokenness info should really be retained/handled externally! * * @param incompatiblePartToInferredTypeMap * Map from incompatibly-connected part to its inferred type. */ void updateIncompatibleParts(Map<PartConnectable, TypeExpr> incompatiblePartToInferredTypeMap) { // (slow) check for validity for (final Map.Entry<PartConnectable, TypeExpr> mapEntry : incompatiblePartToInferredTypeMap.entrySet()) { Gem.PartConnectable codeGemPart = mapEntry.getKey(); if (codeGemPart.getGem() != codeGem) { throw new IllegalArgumentException("Incompatibly-connected parts must come from the edited code gem."); } if (!codeGem.isConnected()) { throw new IllegalArgumentException("Attempt to add an unconnected part to the set of incompatibly-connected parts ."); } if (!(mapEntry.getValue() instanceof TypeExpr)) { throw new IllegalArgumentException("Parts must map to types."); } } // Update the set of parts. this.incompatiblePartToInferredTypeMap.clear(); this.incompatiblePartToInferredTypeMap.putAll(incompatiblePartToInferredTypeMap); // Update whether the code gem should be broken, based on its current // brokenness and its connections. boolean shouldBreak = codeGem.isBroken() || !incompatiblePartToInferredTypeMap.isEmpty(); codeGem.setBroken(shouldBreak); // Update the display. updateVariablesDisplay(); updateOutputTypePanel(); } /** * Return whether a given code gem input is unused. * * @param codeGemInput * the input on the code gem edited by this editor. * @return whether a given code gem input is unused. */ public boolean isUnusedArg(Gem.PartInput codeGemInput) { if (codeGemInput.getGem() != codeGem || !codeGemInput.isValid()) { throw new IllegalArgumentException("Input must be from the edited code gem."); } return unusedArgNames.contains(codeGem.getArguments()[codeGemInput.getInputNum()].getName()); } /** * Update the state of the code gem and this editor after a burn action * takes place. * * @param typeCheckInfo */ public void updateForBurn(TypeCheckInfo typeCheckInfo) { // We have to: // recalculate whether an output connection is broken. // refresh input and output panels. // Calculate the new output type. TypeExpr outputType = getOutputType(); if (outputType == null) { // This is a broken code gem. // Burning should be disallowed on the connected gem subtrees, and so shouldn't affect this gem. return; } // Create a new incompatibility map. Map<PartConnectable, TypeExpr> newIncompatiblePartToInferredTypeMap = new HashMap<PartConnectable, TypeExpr>(incompatiblePartToInferredTypeMap); TypeExpr incompatibleInferredOutputType = checkOutputForConnectivity(typeCheckInfo); if (incompatibleInferredOutputType != null) { newIncompatiblePartToInferredTypeMap.put(codeGem.getOutputPart(), incompatibleInferredOutputType); } else { newIncompatiblePartToInferredTypeMap.remove(codeGem.getOutputPart()); } // Update for incompatibility. This call will also refresh the input panels. updateIncompatibleParts(newIncompatiblePartToInferredTypeMap); } /** * Get the output type from the code gem. This adds burned types to the code result type expression. * * @return TypeExpr the derived output type */ private TypeExpr getOutputType() { TypeExpr codeResultType = codeGem.getCodeResultType(); // if there is no type, the output type is null! if (codeResultType == null) { return null; } // The result type is (burnt args + arrows) -> codeResultType TypeExpr outputType = codeResultType; // calculate which params are burnt Argument.NameTypePair[] args = codeGem.getArguments(); for (int i = args.length - 1; i > -1; i--) { Gem.PartInput input = codeGem.getInputPart(i); if (input.isBurnt()) { TypeExpr inputType = args[i].getType(); outputType = TypeExpr.makeFunType(inputType, outputType); } } // Return the output type return outputType; } /** * Perform all the syntax directed smarts. */ public void doSyntaxSmarts() { // Get the body text, which will be our RHS SC expression String codeGemBodyText = gemCodePanel.getCode(); // Analyze the code. // We analyze using the userQualifiedIdentifiers since symbols // which we have previously selected a module for may appear in the code. // The map in the qualification results will have unnecessary entries removed, // and this is what we store in the codegem. CodeQualificationMap currentQualificationMap = userQualifiedIdentifiers; CodeAnalyser.AnalysisResults results = codeAnalyser.analyseCode(codeGemBodyText, varNamesWhichAreArgs, currentQualificationMap); // Add any new arguments to the set of existing argument names varNamesWhichAreArgs.addAll(Arrays.asList(results.getAllArgumentNames())); // Note: If the analysis is not successful (ie: parsing failed), the // code gem will be broken and appropriate errors will be displayed. TypeExpr codeType = results.getTypeExpr(); // Find the new arguments in the appropriate order. Argument.NameTypePair[] newCodeGemArgs = getNewCodeGemArgs(results.getAllArgumentNames(), codeType); // Arguments which are unused in the analyzed code have been added; so // update the type expression accordingly. if (codeType != null) { for (int i = results.getAllArgumentNames().length; i < newCodeGemArgs.length; i++) { codeType = TypeExpr.makeFunType(TypeExpr.makeParametricType(), codeType); } } // Get the output type TypeExpr codeResultType = codeType == null ? null : codeType.dropFirstNArgs(newCodeGemArgs.length); // Update the code gem codeGem.definitionUpdate(results.getQualifiedCode(), newCodeGemArgs, codeResultType, oldNameToInputMap, results.getQualificationMap(), codeGemBodyText); // Update "last good" state if the code type is ok. if (codeType != null) { updateLastGoodState(); } // Update internal argument info (arg names which unused, or appear in // unqualified form). updateArgumentInfo(results); // Update the qualifications display area updateQualificationsDisplay(results); // Update the editor panel updateEditorPanel(results); // Update the error message. updateErrorMessage(results); // Update the variable and output display areas. updateVariablesDisplay(); updateOutputTypePanel(); } /** * A helper function that displays a compiler message. * * @param results */ private void updateErrorMessage(CodeAnalyser.AnalysisResults results) { List<OffsetCompilerMessage> messages = results.getCompilerMessages(); gemCodePanel.clearErrorIndicators(); for (final OffsetCompilerMessage message : messages) { gemCodePanel.addErrorIndicator(message); } } /** * Update the code gem panel to show the output type. */ private void updateOutputTypePanel() { TypeExpr outputType = getOutputType(); // Do we have a code result type? if (outputType != null) { ScopedEntityNamingPolicy namingPolicy = new ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous( perspective.getWorkingModuleTypeInfo()); if (incompatiblePartToInferredTypeMap.containsKey(codeGem.getOutputPart())) { // Doesn't match with connected output String outputTypeString = typeStringProvider.getTypeString(outputType, namingPolicy); displayOutputType(outputTypeString, false); } else { // No constraint, or output matches // Update the type string of the output to use the new TypeVar // map generated during definitionUpdate TypeExpr outputPartType = codeGem.getOutputPart().getType(); String outputTypeString = (outputPartType == null) ? "" : "<i>" + typeStringProvider.getTypeString(outputPartType, namingPolicy) + "</i>"; displayOutputType(outputTypeString, true); } } else { // no type displayOutputType("", true); } } /** * Display the type for the output. * * @param typeOrMessage the text to display in the type label. This can contain html tags. * @param notBadOutputConnection * false if there is an output type which doesn't match * the connected output (else the connection breaks the gem) */ private void displayOutputType(String typeOrMessage, boolean notBadOutputConnection) { // Is the output connected? boolean outputLocked = (codeGem.getOutputPart().isConnected()); String typeText; String toolTipText; // Depends whether we're setting or unsetting if (notBadOutputConnection) { // for now this just means that the output is connected if (outputLocked && !codeGem.isRootGem()) { typeText = GemCutter.getResourceString("CGE_Connected"); } else { if (typeOrMessage.length() == 0) { typeOrMessage = GemCutter.getResourceString("CGE_Undefined_Type"); } typeText = "-> " + typeOrMessage; } // Clear tooltip toolTipText = null; } else { // Set the label and its background colour typeText = "-> " + GemCutter.getResourceString("CGE_Type_Clash"); // Set a tooltip to indicate the exact problem TypeExpr inferredType = incompatiblePartToInferredTypeMap.get(codeGem.getOutputPart()); if (inferredType == null) { toolTipText = GemCutterMessages.getString("CGE_BrokenSubtreeToolTip"); } else if (typeOrMessage.length() > 0) { toolTipText = GemCutterMessages.getString("CGE_WrongTypeToolTip", inferredType.toString(), typeOrMessage); } else { toolTipText = GemCutterMessages.getString("CGE_UndefinedTypeToolTip", inferredType.toString()); } } // Wrap in html formatting tags. // Use a slightly smaller bold monospace font. typeText = "<html><body><TT><b><FONT SIZE=\"-1\">" + typeText + "</FONT></b></TT></body></html>"; gemCodePanel.updateOutputTypeLabel(typeText, toolTipText, outputLocked, notBadOutputConnection); } /** * Update the internal argument info for the new code gem state. * * @param results * results of code analysis, for displaying fully qualified * arguments. */ private void updateArgumentInfo(CodeAnalyser.AnalysisResults results) { // Update unused args. this.unusedArgNames.clear(); Set<String> usedArgNames = new HashSet<String>(Arrays.asList(results.getAllArgumentNames())); for (final String argName : varNamesWhichAreArgs) { if (!usedArgNames.contains(argName)) { unusedArgNames.add(argName); } } // Update the names of arguments which appear in unqualified form in code this.unqualifiedArgNames.clear(); for (final AnalysedIdentifier identifier : results.getAnalysedIdentifiers()) { if (identifier.getQualificationType() == QualificationType.UnqualifiedArgument) { unqualifiedArgNames.add(identifier.getName()); } } } /** * Update the variables display area according to the current state. */ private void updateVariablesDisplay() { Argument.NameTypePair[] args = codeGem.getArguments(); // Create argument variable panels int numArgVarPanels = args.length; VariablePanel[] argVarPanels = new VariablePanel[numArgVarPanels]; for (int i = 0; i < numArgVarPanels; i++) { argVarPanels[i] = getVariablePanel(i); } // Reordering is allowed if the code gem's result type is ok. // Limiting reordering to instances where the code gem is not broken is // too constraining.. boolean reorderingAllowed = (codeGem.getCodeResultType() != null); gemCodePanel.getVariablesDisplay().updateVariablePanels(argVarPanels, reorderingAllowed); } /** * Update the variable display to show the updated panel at the given panel index. * @param panelIndex the index of the panel to update. */ private void updateVariablesDisplay(int panelIndex) { VariablePanel updatedVariablePanel = getVariablePanel(panelIndex); gemCodePanel.getVariablesDisplay().updateVariablePanel(panelIndex, updatedVariablePanel); } /** * Get the variable panel corresponding to a given variable index. * * @param argIndex * the index of the panel to return * @return the corresponding panel. */ private VariablePanel getVariablePanel(int argIndex) { String varName = codeGem.getArguments()[argIndex].getName(); QualificationType qualificationType = QualificationType.UnqualifiedArgument; Argument.Status argStatus = getArgStatus(argIndex); String typeText = getArgTypeText(codeGem.getInputPart(argIndex).getType(), argStatus); String toolTipText = getArgToolTipText(argIndex, argStatus); VariablePanel varPan = new VariablePanel( new AdvancedCALEditor.PositionlessIdentifier( varName, null, null, null, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, qualificationType), typeText, toolTipText, argStatus); return varPan; } /** * Update the qualifications display area for the new code gem state. * @param results results of code analysis (null if none was performed) */ private void updateQualificationsDisplay( CodeAnalyser.AnalysisResults results) { // Retrieve all mapped names from the code gem qualification map, sort the names // alphabetically in each category, and build the qualification panels. gemCodePanel.getQualificationsDisplay().generateQualificationPanels( codeGem.getQualificationMap(), (results == null ? null : results.getAnalysedIdentifiers()), perspective.getWorkingModuleTypeInfo()); } /** * Update the editor area for the new code gem state. This updates the * qualification map used in the panel so that tooltips are displayed * properly, and updates the chromacoder if analysis was successful * * @param results */ private void updateEditorPanel(CodeAnalyser.AnalysisResults results) { gemCodePanel.setSourceIdentifiers(results.getAnalysedIdentifiers()); gemCodePanel.updateAmbiguityIndicators(); if (results.analysisSuccessful()) { updateChromacoding(results); } // Update the text editor colors after the analysis by repainting gemCodePanel.getCALEditorPane().repaint(); } /** * Updates the chromacoder to color the proper arguments and local variables * * @param analysisResults */ private void updateChromacoding(CodeAnalyser.AnalysisResults analysisResults) { List<String> argumentNames = new ArrayList<String>(Arrays.asList(analysisResults.getAllArgumentNames())); Collections.sort(argumentNames); String[] sortedArgumentNamesArray = new String[argumentNames.size()]; argumentNames.toArray(sortedArgumentNamesArray); gemCodeSyntaxListener.setArgumentNames(sortedArgumentNamesArray); gemCodeSyntaxListener.setLocalVariableNames(getLocalVariableNames(analysisResults)); } /** * Scan the analysed identifiers and retrieve the names of all declared * local variables The resulting local variable names do not contain * duplicates and are sorted alphabetically. * * @param analysisResults * @return list of variable names */ private String[] getLocalVariableNames( CodeAnalyser.AnalysisResults analysisResults) { List<String> variableNames = new ArrayList<String>(); for (final AnalysedIdentifier identifier : analysisResults.getAnalysedIdentifiers()) { if (identifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION && !variableNames.contains(identifier.getName())) { variableNames.add(identifier.getName()); } } Collections.sort(variableNames); String[] variableNamesArray = new String[variableNames.size()]; variableNames.toArray(variableNamesArray); return variableNamesArray; } /** * Get the status of a given argument. * * @param argNum * the index of the argument * @return Argument.Status the status of the given argument. */ private Argument.Status getArgStatus(int argNum) { Argument.NameTypePair[] arguments = codeGem.getArguments(); PartInput input = codeGem.getInputPart(argNum); if (incompatiblePartToInferredTypeMap.containsKey(input)) { // Check if it's an incompatible arg. return Argument.Status.TYPE_CLASH; } else if (input.isConnected()) { // Check whether the arg is used (appears in the code). if (unusedArgNames.contains(arguments[argNum].getName())) { return Argument.Status.CONNECTED_UNUSED; } return Argument.Status.CONNECTED; } else if (input.isBurnt()) { // also no type if the input is burnt return Argument.Status.BURNT; } else if (input.getType() != null) { // Return the type from the input return Argument.Status.NATURAL; } else { // Unconnected, unburnt input has no type. return Argument.Status.TYPE_UNDEFINED; } } /** * Get the type text for a given argument. This takes the state of the code * gem (eg. connection, burn info) into account. * * @param argType * the type of the argument * @param argStatus * the status of the argument * @return the type text for the given argument. */ private String getArgTypeText(TypeExpr argType, Argument.Status argStatus) { if (argStatus == Argument.Status.TYPE_UNDEFINED) { return GemCutter.getResourceString("CGE_Undefined_Type"); } else if (argStatus == Argument.Status.BURNT) { return GemCutter.getResourceString("CGE_Burnt_Input"); } else if (argStatus == Argument.Status.CONNECTED) { return GemCutter.getResourceString("CGE_Connected"); } else if (argStatus == Argument.Status.CONNECTED_UNUSED) { return GemCutter.getResourceString("CGE_Unused_Connected"); } else if (argStatus == Argument.Status.TYPE_CLASH) { return GemCutter.getResourceString("CGE_Type_Clash"); } ScopedEntityNamingPolicy namingPolicy = new ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous(perspective.getWorkingModuleTypeInfo()); // Must be in natural state. Check for (transiently) null type.. if (argType == null) { return "null"; } // Return the type string. return typeStringProvider.getTypeString(argType, namingPolicy); } /** * Get the tooltip text for a given argument. This takes the state of the * code gem (eg. connection, burn info) into account. * * @param argNum * the index of the argument * @param argStatus * the status of the argument * @return the tooltip text for the given argument. */ private String getArgToolTipText(int argNum, Argument.Status argStatus) { Argument.NameTypePair[] arguments = codeGem.getArguments(); String argName = arguments[argNum].getName(); StringBuilder text = new StringBuilder(); text.append("<html><body>"); if (argStatus != Argument.Status.TYPE_CLASH) { TypeExpr argType = codeGem.getInputPart(argNum).getType(); String argTypeText = getArgTypeText(argType, argStatus); boolean separateText = false; // Check if argument name / type lines must be separated { String completeText = argName + " :: " + argTypeText; if (ToolTipHelpers.wrapTextToHTMLLines(completeText, gemCodePanel).length() != completeText.length()) { // Text cut because name or type is over the tooltip limit; // so display each on separate lines separateText = true; } } text.append("<b>" + ToolTipHelpers.wrapTextToHTMLLines(argName, gemCodePanel) + "</b> :: "); if (separateText) { text.append("<br>"); } text.append("<i>" + ToolTipHelpers.wrapTextToHTMLLines(argTypeText, gemCodePanel) + "</i>"); } else { PartInput input = codeGem.getInputPart(argNum); TypeExpr argType = arguments[argNum].getType(); TypeExpr inferredInputType = incompatiblePartToInferredTypeMap.get(input); text.append(GemCutterMessages.getString("CGE_WrongArgTypeToolTip", ToolTipHelpers.wrapTextToHTMLLines(inferredInputType.toString(), gemCodePanel), ToolTipHelpers.wrapTextToHTMLLines(argType.toString(), gemCodePanel))); } text.append("</body></html>"); return text.toString(); } /** * Update "last good" state if the code gem is now good. This updates * internal variables needed to keep track of input reorder info across * intermediate broken codegem states. */ private void updateLastGoodState() { lastArguments = new ArrayList<NameTypePair>(Arrays.asList(codeGem.getArguments())); oldNameToInputMap = codeGem.getArgNameToInputMap(); } /** * Return whether changing the form of an argument to a qualification is * allowed * * @param name * the name of the variable * @return True if argument change form is allowed; False if not */ private boolean isArgumentFormChangeAllowed(String name) { // Disallow if the variable is connected. PartInput input = oldNameToInputMap.get(name); if (input != null && input.isConnected()) { return false; } else { return true; } } /** * Returns whether changing the form of a qualification to an argument is allowed * * @param currentForm * current form of the qualification panel * @return True if form change is allowed; False if not */ private boolean isQualificationFormChangeAllowed(SourceIdentifier.Category currentForm) { return (currentForm == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); } /** * Change the form of an argument to a default function. * * The argument form: - if it had been previously qualified to a function, * will become this function - if can be resolved to a function from am * imported module, will become the first such resolving function - if * cannot be resolved, will remain the same * * This action occurs on double click of argument panel icons, or argument * drag into qualification panel. * * @param argumentName * @return whether argument was transformed to function */ boolean changeArgumentToDefaultFunction(String argumentName) { // Check if this was already mapped QualifiedName qualifiedName = userQualifiedIdentifiers.getQualifiedName(argumentName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); if (qualifiedName != null) { // Yes, use previous qualification changeArgumentToQualification(argumentName, qualifiedName.getModuleName()); return true; } // Put variable in first candidate module List<ModuleName> candidateModules = CodeAnalyser.getModulesContainingIdentifier( argumentName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, perspective.getWorkingModuleTypeInfo()); if (candidateModules.size() > 0) { changeArgumentToQualification(argumentName, candidateModules.get(0)); return true; } // Argument cannot resolve to a function return false; } /** * Change the form of an argument to a qualification. If the argument is * fully qualified in code, this means it is changed to a function in the * current module. If the argument is not fully qualified in code, then it * is changed to a function from an external module, but will not be removed * from the list of arguments. * * @param argumentName * @param moduleName */ void changeArgumentToQualification(String argumentName, ModuleName moduleName) { StateEdit stateEdit = null; PartInput[] oldInputs = null; if (recordingEditStates) { stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_CodeGemArgFormChange")); oldInputs = codeGem.getInputParts(); } if (moduleName.equals(perspective.getWorkingModuleName())) { // Remove argument from free variables, and add to map varNamesWhichAreArgs.remove(argumentName); } userQualifiedIdentifiers.putQualification(argumentName, moduleName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); // re-evaluate the gem based on the new variable form doSyntaxSmarts(); if (recordingEditStates) { stateEdit.end(); if (editHandler != null) { editHandler.definitionEdited(this, oldInputs, stateEdit); } } } /** * Change the form of an ambiguity to a qualification. * * @param argumentName * @param moduleName * @param type */ void changeAmbiguityToQualification(String argumentName, ModuleName moduleName, SourceIdentifier.Category type) { StateEdit stateEdit = null; PartInput[] oldInputs = null; if (recordingEditStates) { stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_CodeGemArgFormChange")); oldInputs = codeGem.getInputParts(); } userQualifiedIdentifiers.putQualification(argumentName, moduleName, type); // re-evaluate the gem based on the new variable form doSyntaxSmarts(); if (recordingEditStates) { stateEdit.end(); if (editHandler != null) { editHandler.definitionEdited(this, oldInputs, stateEdit); } } } /** * Change the form of a qualification (ie: an identifier which was not * qualified in code) to an argument of the codegem. * * @param qualificationName * the unqualified name of the qualification * @param qualificationModuleOrNull * the module name, or null */ void changeQualificationToArgument(String qualificationName, ModuleName qualificationModuleOrNull) { StateEdit stateEdit = null; PartInput[] oldInputs = null; if (recordingEditStates) { stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_CodeGemArgFormChange")); oldInputs = codeGem.getInputParts(); } // Add qualification to free variables varNamesWhichAreArgs.add(qualificationName); if (qualificationModuleOrNull == null || !qualificationModuleOrNull.equals(perspective.getWorkingModuleName())) { userQualifiedIdentifiers.removeQualification(qualificationName, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); } // re-evaluate the gem based on the new variable form doSyntaxSmarts(); if (recordingEditStates) { stateEdit.end(); if (editHandler != null) { editHandler.definitionEdited(this, oldInputs, stateEdit); } } } /** * Change the module of a qualification represented by a qualification panel * * @param unqualifiedName * unqualified name of identifier * @param newModule * new module name. <em>Cannot</em> be null. * @param qualificationForm * identifier form */ void changeQualificationModule(String unqualifiedName, ModuleName newModule, SourceIdentifier.Category qualificationForm) { StateEdit stateEdit = null; PartInput[] oldInputs = null; if (recordingEditStates) { stateEdit = new StateEdit(CodeGemEditor.this, GemCutter.getResourceString("UndoText_CodeGemQualificationModuleChange")); oldInputs = codeGem.getInputParts(); } // If we are switching to the current module, make sure all arguments // are switched also if (newModule.equals(perspective.getWorkingModuleName())) { varNamesWhichAreArgs.remove(unqualifiedName); } // Change the module mapping CodeQualificationMap qualificationMap = userQualifiedIdentifiers; qualificationMap.removeQualification(unqualifiedName, qualificationForm); qualificationMap.putQualification(unqualifiedName, newModule, qualificationForm); // re-evaluate the gem based on the new variable form doSyntaxSmarts(); if (recordingEditStates) { stateEdit.end(); if (editHandler != null) { editHandler.definitionEdited(this, oldInputs, stateEdit); } } } /** * Update the user qualification map to reflect the renaming of a (non module) entity. * If the qualification map contained the old name, it will be replaced by this new name. * @param oldName * @param newName * @param category */ void updateQualificationsForEntityRename(QualifiedName oldName, QualifiedName newName, SourceIdentifier.Category category) { QualifiedName storedQualification = userQualifiedIdentifiers.getQualifiedName(oldName.getUnqualifiedName(), category); if (oldName.equals(storedQualification)) { userQualifiedIdentifiers.removeQualification(oldName.getUnqualifiedName(), category); userQualifiedIdentifiers.putQualification(newName.getUnqualifiedName(), newName.getModuleName(), category); } } /** * Update the user qualification map to reflect the renaming of a module. * All qualifications that referred to the old module name will be replaced with a new * qualification referring to the new module name. * @param oldModuleName * @param newModuleName */ void updateQualificationsForModuleRename(ModuleName oldModuleName, ModuleName newModuleName) { // We need to handle each of the following four categories separately, so keep them in an an array // and loop over them SourceIdentifier.Category[] categories = new SourceIdentifier.Category[] { SourceIdentifier.Category.DATA_CONSTRUCTOR, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD, SourceIdentifier.Category.TYPE_CLASS, SourceIdentifier.Category.TYPE_CONSTRUCTOR }; for (final Category category : categories) { // Get the set of unqualifiedNames associated with this category, and look up their respective qualifications. // If the module name associated with them is the old module name, replace the qualification with a new one // referring to the new module name. Set<String> unqualifiedNames = userQualifiedIdentifiers.getUnqualifiedNames(category); for (final String unqualifiedName : unqualifiedNames) { QualifiedName qualifiedName = userQualifiedIdentifiers.getQualifiedName(unqualifiedName, category); if (qualifiedName.getModuleName().equals(oldModuleName)) { userQualifiedIdentifiers.removeQualification(unqualifiedName, category); userQualifiedIdentifiers.putQualification(unqualifiedName, newModuleName, category); } } } } /** * Shift an argument to another position. * * @param inputIndexToShift * the index of the input to shift * @param shiftAmount * the amount by which to shift the input. +ve numbers increase * its index, -ve numbers decrease it. */ private void shiftInput(int inputIndexToShift, int shiftAmount) { int nArgs = codeGem.getNInputs(); int newArgPos = inputIndexToShift + shiftAmount; if (newArgPos < 0 || newArgPos >= nArgs) { throw new IllegalArgumentException("The new argument index must lie within 0 and " + (nArgs - 1) + " inclusive."); } // Fill in the new input nums array. int[] newInputNums = new int[nArgs]; for (int i = 0; i < nArgs; i++) { // If we're looking at the new arg position, insert the new index. if (i == inputIndexToShift) { newInputNums[i] = newArgPos; } else { // Otherwise, adjust the value from i. int newArgIndex = i; if (inputIndexToShift < newArgPos) { // shift down everything in between if (i > inputIndexToShift && i <= newArgPos) { newArgIndex--; } } else { // shift up everything in between if (i < inputIndexToShift && i >= newArgPos) { newArgIndex++; } } newInputNums[i] = newArgIndex; } } reorderInputs(newInputNums); } /** * Reorder the inputs. Only call if this code gem's result type is ok (ie. * it's either not broken, or broken because of its connections). * * @param newInputNums * int[] An array of new input numbers. The number at index i is * the new index of input i. */ private void reorderInputs(int[] newInputNums) { Argument.NameTypePair[] currentArgs = codeGem.getArguments(); if (codeGem.getCodeResultType() == null) { throw new IllegalStateException("Attempt to reorder inputs on a broken gem."); } // fill in an array that tracks which inputs are included in newInputNums boolean[] inputsGiven = new boolean[newInputNums.length]; for (final int inputNum : newInputNums) { if (inputNum < 0 || inputNum >= newInputNums.length) { throw new IllegalArgumentException("Input nums must be in the range 0 to (numInputs-1) inclusive."); } inputsGiven[inputNum] = true; } // now check that all the inputs are accounted for for (final boolean inputGiven : inputsGiven) { if (inputGiven == false) { throw new IllegalArgumentException("Attempt to reorder inputs to a state without all the inputs."); } } // declare the new args array int numArgs = newInputNums.length; Argument.NameTypePair[] newArgs = new Argument.NameTypePair[numArgs]; // fill in the new args array for (int i = 0; i < numArgs; i++) { // the number at index i is the new index of arg i int newInputNum = newInputNums[i]; newArgs[newInputNum] = currentArgs[i]; } // Everything else should be the same.. boolean wasBroken = codeGem.isBroken(); codeGem.definitionUpdate(codeGem.getCode(), newArgs, codeGem.getCodeResultType(), codeGem.getArgNameToInputMap(), codeGem.getQualificationMap(), codeGem.getVisibleCode()); codeGem.setBroken(wasBroken); if (!wasBroken) { updateLastGoodState(); } } /** * Carry out actions needed to insert the specified string into the editor * from the auto-complete manager. * * @param backtrackLength * @param insertion */ public void insertAutoCompleteString(int backtrackLength, String insertion) { try { // Remove text from editor AdvancedCALEditor editor = gemCodePanel.getCALEditorPane(); int caretPosition = editor.getCaretPosition(); CodeAnalyser.AnalysedIdentifier identifier = editor.getIdentifierAtPosition(caretPosition); editor.getDocument().remove(caretPosition - backtrackLength, backtrackLength); if ((identifier == null) || (insertion.indexOf('.') < 0)) { // Not a qualified (ie: ambiguous) insertion, or cannot locate identifier // Do regular insert editor.getDocument().insertString(caretPosition - backtrackLength, insertion, null); return; } else { // Qualified completion, and type of the identifier is known // So do a smart insert and update the qualification map SourceIdentifier.Category identifierCategory = identifier.getCategory(); QualifiedName completedName = QualifiedName.makeFromCompoundName(insertion); EditorTextTransferHandler.insertEditorQualification(gemCodePanel.getCALEditorPane(), completedName, identifierCategory, userQualifiedIdentifiers, true); } } catch (BadLocationException e) { throw new IllegalStateException("bad location on auto-complete insert"); } } /** * @see org.openquark.gems.client.AutoCompleteManager.AutoCompleteEditor#getEditorComponent() */ public JTextComponent getEditorComponent() { return gemCodePanel.getCALEditorPane(); } /* * Methods supporting javax.swing.undo.StateEditable * ******************************************** */ /** * Restore the stored editor state. * * @param state * Hashtable the stored state */ public void restoreState(Hashtable<?, ?> state) { boolean oldSmartsActivated = smartsActivated; smartsActivated = false; codeGem.restoreDefinitionState(state); gemCodePanel.restoreState(state); gemCodePanel.getCALEditorPane().setText(codeGem.getVisibleCode()); Object stateValue = state.get(new Pair<CodeGemEditor, String>(this, LAST_ARGUMENTS_KEY)); if (stateValue != null) { lastArguments = new ArrayList<NameTypePair>(UnsafeCast.<ArrayList<NameTypePair>>unsafeCast(stateValue)); } stateValue = state.get(new Pair<CodeGemEditor, String>(this, LAST_RESOLVED_QUALIFICATIONS_KEY)); if (stateValue != null) { userQualifiedIdentifiers = ((CodeQualificationMap)stateValue).makeCopy(); editorTransferHandler.setUserQualifiedIdentifiers(userQualifiedIdentifiers); } stateValue = state.get(new Pair<CodeGemEditor, String>(this, OLD_NAME_TO_INPUT_MAP_KEY)); if (stateValue != null) { oldNameToInputMap = new HashMap<String, PartInput>(UnsafeCast.<Map<String, PartInput>>unsafeCast(stateValue)); } stateValue = state.get(new Pair<CodeGemEditor, String>(this, ARG_NAMED_VARS_KEY)); if (stateValue != null) { varNamesWhichAreArgs = new LinkedHashSet<String>(UnsafeCast.<Set<String>>unsafeCast(stateValue)); editorTransferHandler.setArgumentNames(varNamesWhichAreArgs); } stateValue = state.get(new Pair<CodeGemEditor, String>(this, PRESERVE_ORDER_KEY)); if (stateValue != null) { keepInputsInNaturalOrder = ((Boolean) stateValue).booleanValue(); } smartsActivated = oldSmartsActivated; } /** * Save the editor state. * * @param state * Hashtable the table in which to store the editor state */ public void storeState(Hashtable<Object, Object> state) { codeGem.storeDefinitionState(state); gemCodePanel.storeState(state); state.put(new Pair<CodeGemEditor, String>(this, LAST_ARGUMENTS_KEY), new ArrayList<NameTypePair>(lastArguments)); state.put(new Pair<CodeGemEditor, String>(this, LAST_RESOLVED_QUALIFICATIONS_KEY), userQualifiedIdentifiers.makeCopy()); state.put(new Pair<CodeGemEditor, String>(this, OLD_NAME_TO_INPUT_MAP_KEY), new HashMap<String, PartInput>(oldNameToInputMap)); state.put(new Pair<CodeGemEditor, String>(this, ARG_NAMED_VARS_KEY), new LinkedHashSet<String>(varNamesWhichAreArgs)); state.put(new Pair<CodeGemEditor, String>(this, PRESERVE_ORDER_KEY), Boolean.valueOf(keepInputsInNaturalOrder)); } }