Merge branch 'develop' into trialMerge
[jalview.git] / src / jalview / commands / EditCommand.java
index a8aa886..f2b3904 100644 (file)
@@ -1,25 +1,46 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
- * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
  * Jalview is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
- * 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
  * Jalview is distributed in the hope that it will be useful, but 
  * WITHOUT ANY WARRANTY; without even the implied warranty 
  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
  * PURPOSE.  See the GNU General Public License for more details.
  * 
- * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
 package jalview.commands;
 
-import java.util.*;
-
-import jalview.datamodel.*;
+import jalview.analysis.AlignSeq;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.Comparison;
+import jalview.util.ReverseListIterator;
+import jalview.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
 
 /**
  * 
@@ -45,19 +66,60 @@ import jalview.datamodel.*;
  */
 public class EditCommand implements CommandI
 {
-  public static final int INSERT_GAP = 0;
-
-  public static final int DELETE_GAP = 1;
-
-  public static final int CUT = 2;
-
-  public static final int PASTE = 3;
-
-  public static final int REPLACE = 4;
-  
-  public static final int INSERT_NUC=5;
+  public enum Action
+  {
+    INSERT_GAP
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return DELETE_GAP;
+      }
+    },
+    DELETE_GAP
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return INSERT_GAP;
+      }
+    },
+    CUT
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return PASTE;
+      }
+    },
+    PASTE
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return CUT;
+      }
+    },
+    REPLACE
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return REPLACE;
+      }
+    },
+    INSERT_NUC
+    {
+      @Override
+      public Action getUndoAction()
+      {
+        return null;
+      }
+    };
+    public abstract Action getUndoAction();
+  };
 
-  Edit[] edits;
+  private List<Edit> edits = new ArrayList<>();
 
   String description;
 
@@ -65,50 +127,177 @@ public class EditCommand implements CommandI
   {
   }
 
-  public EditCommand(String description)
+  public EditCommand(String desc)
   {
-    this.description = description;
+    this.description = desc;
   }
 
-  public EditCommand(String description, int command, SequenceI[] seqs,
+  public EditCommand(String desc, Action command, SequenceI[] seqs,
           int position, int number, AlignmentI al)
   {
-    this.description = description;
-    if (command == CUT || command == PASTE)
+    this.description = desc;
+    if (command == Action.CUT || command == Action.PASTE)
     {
-      edits = new Edit[]
-      { new Edit(command, seqs, position, number, al) };
+      setEdit(new Edit(command, seqs, position, number, al));
     }
 
     performEdit(0, null);
   }
 
-  public EditCommand(String description, int command, String replace,
+  public EditCommand(String desc, Action command, String replace,
           SequenceI[] seqs, int position, int number, AlignmentI al)
   {
-    this.description = description;
-    if (command == REPLACE)
+    this.description = desc;
+    if (command == Action.REPLACE)
     {
-      edits = new Edit[]
-      { new Edit(command, seqs, position, number, al, replace) };
+      setEdit(new Edit(command, seqs, position, number, al, replace));
     }
 
     performEdit(0, null);
   }
 
+  /**
+   * Set the list of edits to the specified item (only).
+   * 
+   * @param e
+   */
+  protected void setEdit(Edit e)
+  {
+    edits.clear();
+    edits.add(e);
+  }
+
+  /**
+   * Add the given edit command to the stored list of commands. If simply
+   * expanding the range of the last command added, then modify it instead of
+   * adding a new command.
+   * 
+   * @param e
+   */
+  public void addEdit(Edit e)
+  {
+    if (!expandEdit(edits, e))
+    {
+      edits.add(e);
+    }
+  }
+
+  /**
+   * Returns true if the new edit is incorporated by updating (expanding the
+   * range of) the last edit on the list, else false. We can 'expand' the last
+   * edit if the new one is the same action, on the same sequences, and acts on
+   * a contiguous range. This is the case where a mouse drag generates a series
+   * of contiguous gap insertions or deletions.
+   * 
+   * @param edits
+   * @param e
+   * @return
+   */
+  protected static boolean expandEdit(List<Edit> edits, Edit e)
+  {
+    if (edits == null || edits.isEmpty())
+    {
+      return false;
+    }
+    Edit lastEdit = edits.get(edits.size() - 1);
+    Action action = e.command;
+    if (lastEdit.command != action)
+    {
+      return false;
+    }
+
+    /*
+     * Both commands must act on the same sequences - compare the underlying
+     * dataset sequences, rather than the aligned sequences, which change as
+     * they are edited.
+     */
+    if (lastEdit.seqs.length != e.seqs.length)
+    {
+      return false;
+    }
+    for (int i = 0; i < e.seqs.length; i++)
+    {
+      if (lastEdit.seqs[i].getDatasetSequence() != e.seqs[i]
+              .getDatasetSequence())
+      {
+        return false;
+      }
+    }
+
+    /**
+     * Check a contiguous edit; either
+     * <ul>
+     * <li>a new Insert <n> positions to the right of the last <insert n>,
+     * or</li>
+     * <li>a new Delete <n> gaps which is <n> positions to the left of the last
+     * delete.</li>
+     * </ul>
+     */
+    boolean contiguous = (action == Action.INSERT_GAP
+            && e.position == lastEdit.position + lastEdit.number)
+            || (action == Action.DELETE_GAP
+                    && e.position + e.number == lastEdit.position);
+    if (contiguous)
+    {
+      /*
+       * We are just expanding the range of the last edit. For delete gap, also
+       * moving the start position left.
+       */
+      lastEdit.number += e.number;
+      lastEdit.seqs = e.seqs;
+      if (action == Action.DELETE_GAP)
+      {
+        lastEdit.position--;
+      }
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Clear the list of stored edit commands.
+   * 
+   */
+  protected void clearEdits()
+  {
+    edits.clear();
+  }
+
+  /**
+   * Returns the i'th stored Edit command.
+   * 
+   * @param i
+   * @return
+   */
+  protected Edit getEdit(int i)
+  {
+    if (i >= 0 && i < edits.size())
+    {
+      return edits.get(i);
+    }
+    return null;
+  }
+
+  @Override
   final public String getDescription()
   {
     return description;
   }
 
+  @Override
   public int getSize()
   {
-    return edits == null ? 0 : edits.length;
+    return edits.size();
   }
 
+  /**
+   * Return the alignment for the first edit (or null if no edit).
+   * 
+   * @return
+   */
   final public AlignmentI getAlignment()
   {
-    return edits[0].al;
+    return (edits.isEmpty() ? null : edits.get(0).al);
   }
 
   /**
@@ -124,8 +313,8 @@ public class EditCommand implements CommandI
    * @param al
    * @param performEdit
    */
-  final public void appendEdit(int command, SequenceI[] seqs, int position,
-          int number, AlignmentI al, boolean performEdit)
+  final public void appendEdit(Action command, SequenceI[] seqs,
+          int position, int number, AlignmentI al, boolean performEdit)
   {
     appendEdit(command, seqs, position, number, al, performEdit, null);
   }
@@ -142,182 +331,263 @@ public class EditCommand implements CommandI
    * @param performEdit
    * @param views
    */
-  final public void appendEdit(int command, SequenceI[] seqs, int position,
-          int number, AlignmentI al, boolean performEdit, AlignmentI[] views)
+  final public void appendEdit(Action command, SequenceI[] seqs,
+          int position, int number, AlignmentI al, boolean performEdit,
+          AlignmentI[] views)
   {
-    Edit edit = new Edit(command, seqs, position, number,
-            al.getGapCharacter());
-    if (al.getHeight() == seqs.length)
+    Edit edit = new Edit(command, seqs, position, number, al);
+    appendEdit(edit, al, performEdit, views);
+  }
+
+  /**
+   * Overloaded method that accepts an Edit object with additional parameters.
+   * 
+   * @param edit
+   * @param al
+   * @param performEdit
+   * @param views
+   */
+  final public void appendEdit(Edit edit, AlignmentI al,
+          boolean performEdit, AlignmentI[] views)
+  {
+    if (al.getHeight() == edit.seqs.length)
     {
       edit.al = al;
       edit.fullAlignmentHeight = true;
     }
 
-    if (edits != null)
-    {
-      Edit[] temp = new Edit[edits.length + 1];
-      System.arraycopy(edits, 0, temp, 0, edits.length);
-      edits = temp;
-      edits[edits.length - 1] = edit;
-    }
-    else
+    addEdit(edit);
+
+    if (performEdit)
     {
-      edits = new Edit[]
-      { edit };
+      performEdit(edit, views);
     }
+  }
 
-    if (performEdit)
+  /**
+   * Execute all the edit commands, starting at the given commandIndex
+   * 
+   * @param commandIndex
+   * @param views
+   */
+  public final void performEdit(int commandIndex, AlignmentI[] views)
+  {
+    ListIterator<Edit> iterator = edits.listIterator(commandIndex);
+    while (iterator.hasNext())
     {
-      performEdit(edits.length - 1, views);
+      Edit edit = iterator.next();
+      performEdit(edit, views);
     }
   }
 
-  final void performEdit(int commandIndex, AlignmentI[] views)
+  /**
+   * Execute one edit command in all the specified alignment views
+   * 
+   * @param edit
+   * @param views
+   */
+  protected static void performEdit(Edit edit, AlignmentI[] views)
   {
-    int eSize = edits.length;
-    for (int e = commandIndex; e < eSize; e++)
+    switch (edit.command)
     {
-      switch (edits[e].command)
-      {
-      case INSERT_GAP:
-        insertGap(edits[e]);
-        break;
-      case DELETE_GAP:
-        deleteGap(edits[e]);
-        break;
-      case CUT:
-        cut(edits[e], views);
-        break;
-      case PASTE:
-        paste(edits[e], views);
-        break;
-      case REPLACE:
-        replace(edits[e]);
-        break;
-        //TODO:add deleteNuc for UNDO
-//      case INSERT_NUC:
-//     insertNuc(edits[e]);
-//     break;
-      }
+    case INSERT_GAP:
+      insertGap(edit);
+      break;
+    case DELETE_GAP:
+      deleteGap(edit);
+      break;
+    case CUT:
+      cut(edit, views);
+      break;
+    case PASTE:
+      paste(edit, views);
+      break;
+    case REPLACE:
+      replace(edit);
+      break;
+    case INSERT_NUC:
+      // TODO:add deleteNuc for UNDO
+      // case INSERT_NUC:
+      // insertNuc(edits[e]);
+      break;
+    default:
+      break;
     }
   }
 
+  @Override
   final public void doCommand(AlignmentI[] views)
   {
     performEdit(0, views);
   }
 
+  /**
+   * Undo the stored list of commands, in reverse order.
+   */
+  @Override
   final public void undoCommand(AlignmentI[] views)
   {
-    int e = 0, eSize = edits.length;
-    for (e = eSize - 1; e > -1; e--)
+    ListIterator<Edit> iterator = edits.listIterator(edits.size());
+    while (iterator.hasPrevious())
     {
-      switch (edits[e].command)
+      Edit e = iterator.previous();
+      switch (e.command)
       {
       case INSERT_GAP:
-        deleteGap(edits[e]);
+        deleteGap(e);
         break;
       case DELETE_GAP:
-        insertGap(edits[e]);
+        insertGap(e);
         break;
       case CUT:
-        paste(edits[e], views);
+        paste(e, views);
         break;
       case PASTE:
-        cut(edits[e], views);
+        cut(e, views);
         break;
       case REPLACE:
-        replace(edits[e]);
+        replace(e);
+        break;
+      case INSERT_NUC:
+        // not implemented
+        break;
+      default:
         break;
-       }
+      }
     }
   }
 
-  final void insertGap(Edit command)
+  /**
+   * Insert gap(s) in sequences as specified by the command, and adjust
+   * annotations.
+   * 
+   * @param command
+   */
+  final private static void insertGap(Edit command)
   {
 
     for (int s = 0; s < command.seqs.length; s++)
     {
       command.seqs[s].insertCharAt(command.position, command.number,
               command.gapChar);
-//      System.out.println("pos: "+command.position+" number: "+command.number);
+      // System.out.println("pos: "+command.position+" number:
+      // "+command.number);
     }
 
     adjustAnnotations(command, true, false, null);
   }
-//  
-//  final void insertNuc(Edit command)
-//  {
-//
-//    for (int s = 0; s < command.seqs.length; s++)
-//    {
-//        System.out.println("pos: "+command.position+" number: "+command.number);
-//     command.seqs[s].insertCharAt(command.position, command.number,'A');
-//    }
-//
-//    adjustAnnotations(command, true, false, null);
-//  }
-
-  final void deleteGap(Edit command)
+
+  //
+  // final void insertNuc(Edit command)
+  // {
+  //
+  // for (int s = 0; s < command.seqs.length; s++)
+  // {
+  // System.out.println("pos: "+command.position+" number: "+command.number);
+  // command.seqs[s].insertCharAt(command.position, command.number,'A');
+  // }
+  //
+  // adjustAnnotations(command, true, false, null);
+  // }
+
+  /**
+   * Delete gap(s) in sequences as specified by the command, and adjust
+   * annotations.
+   * 
+   * @param command
+   */
+  final static private void deleteGap(Edit command)
   {
     for (int s = 0; s < command.seqs.length; s++)
     {
-      command.seqs[s].deleteChars(command.position, command.position
-              + command.number);
+      command.seqs[s].deleteChars(command.position,
+              command.position + command.number);
     }
 
     adjustAnnotations(command, false, false, null);
   }
 
-  void cut(Edit command, AlignmentI[] views)
+  /**
+   * Carry out a Cut action. The cut characters are saved in case Undo is
+   * requested.
+   * 
+   * @param command
+   * @param views
+   */
+  static void cut(Edit command, AlignmentI[] views)
   {
     boolean seqDeleted = false;
     command.string = new char[command.seqs.length][];
 
     for (int i = 0; i < command.seqs.length; i++)
     {
-      if (command.seqs[i].getLength() > command.position)
+      final SequenceI sequence = command.seqs[i];
+      if (sequence.getLength() > command.position)
       {
-        command.string[i] = command.seqs[i].getSequence(command.position,
+        command.string[i] = sequence.getSequence(command.position,
                 command.position + command.number);
-        SequenceI oldds = command.seqs[i].getDatasetSequence();
-        if (command.oldds != null && command.oldds[i] != null)
-        {
-          // we are redoing an undone cut.
-          command.seqs[i].setDatasetSequence(null);
-        }
-        command.seqs[i].deleteChars(command.position, command.position
+        SequenceI oldds = sequence.getDatasetSequence();
+        ContiguousI cutPositions = sequence.findPositions(
+                command.position + 1, command.position + command.number);
+        boolean cutIsInternal = cutPositions != null
+                && sequence.getStart() != cutPositions
+                .getBegin() && sequence.getEnd() != cutPositions.getEnd();
+
+        /*
+         * perform the cut; if this results in a new dataset sequence, add
+         * that to the alignment dataset
+         */
+        SequenceI ds = sequence.getDatasetSequence();
+        sequence.deleteChars(command.position, command.position
                 + command.number);
+
         if (command.oldds != null && command.oldds[i] != null)
         {
-          // oldds entry contains the cut dataset sequence.
-          command.seqs[i].setDatasetSequence(command.oldds[i]);
+          /*
+           * we are Redoing a Cut, or Undoing a Paste - so
+           * oldds entry contains the cut dataset sequence,
+           * with sequence features in expected place
+           */
+          sequence.setDatasetSequence(command.oldds[i]);
           command.oldds[i] = oldds;
         }
         else
         {
-          // modify the oldds if necessary
-          if (oldds != command.seqs[i].getDatasetSequence()
-                  || command.seqs[i].getSequenceFeatures() != null)
+          /* 
+           * new cut operation: save the dataset sequence 
+           * so it can be restored in an Undo
+           */
+          if (command.oldds == null)
           {
-            if (command.oldds == null)
+            command.oldds = new SequenceI[command.seqs.length];
+          }
+          command.oldds[i] = oldds;// todo not if !cutIsInternal?
+
+          // do we need to edit sequence features for new sequence ?
+          if (oldds != sequence.getDatasetSequence()
+                  || (cutIsInternal
+                          && sequence.getFeatures().hasFeatures()))
+          // todo or just test cutIsInternal && cutPositions != null ?
+          {
+            if (cutPositions != null)
             {
-              command.oldds = new SequenceI[command.seqs.length];
+              cutFeatures(command, sequence, cutPositions.getBegin(),
+                              cutPositions.getEnd(), cutIsInternal);
             }
-            command.oldds[i] = oldds;
-            adjustFeatures(
-                    command,
-                    i,
-                    command.seqs[i].findPosition(command.position),
-                    command.seqs[i].findPosition(command.position
-                            + command.number), false);
           }
         }
+        SequenceI newDs = sequence.getDatasetSequence();
+        if (newDs != ds && command.al != null
+                && command.al.getDataset() != null
+                && !command.al.getDataset().getSequences().contains(newDs))
+        {
+          command.al.getDataset().addSequence(newDs);
+        }
       }
 
-      if (command.seqs[i].getLength() < 1)
+      if (sequence.getLength() < 1)
       {
-        command.al.deleteSequence(command.seqs[i]);
+        command.al.deleteSequence(sequence);
         seqDeleted = true;
       }
     }
@@ -325,84 +595,107 @@ public class EditCommand implements CommandI
     adjustAnnotations(command, false, seqDeleted, views);
   }
 
-  void paste(Edit command, AlignmentI[] views)
+  /**
+   * Perform the given Paste command. This may be to add cut or copied sequences
+   * to an alignment, or to undo a 'Cut' action on a region of the alignment.
+   * 
+   * @param command
+   * @param views
+   */
+  static void paste(Edit command, AlignmentI[] views)
   {
-    StringBuffer tmp;
-    boolean newDSNeeded;
-    boolean newDSWasNeeded;
-    int newstart, newend;
     boolean seqWasDeleted = false;
-    int start = 0, end = 0;
 
     for (int i = 0; i < command.seqs.length; i++)
     {
-      newDSNeeded = false;
-      newDSWasNeeded = command.oldds != null && command.oldds[i] != null;
-      if (command.seqs[i].getLength() < 1)
+      boolean newDSNeeded = false;
+      boolean newDSWasNeeded = command.oldds != null
+              && command.oldds[i] != null;
+      SequenceI sequence = command.seqs[i];
+      if (sequence.getLength() < 1)
       {
-        // ie this sequence was deleted, we need to
-        // read it to the alignment
+        /*
+         * sequence was deleted; re-add it to the alignment
+         */
         if (command.alIndex[i] < command.al.getHeight())
         {
-          List<SequenceI> sequences;
-          synchronized (sequences=command.al.getSequences()) {
-            sequences.add(command.alIndex[i], command.seqs[i]);
+          List<SequenceI> sequences = command.al.getSequences();
+          synchronized (sequences)
+          {
+            if (!(command.alIndex[i] < 0))
+            {
+              sequences.add(command.alIndex[i], sequence);
+            }
           }
         }
         else
         {
-          command.al.addSequence(command.seqs[i]);
+          command.al.addSequence(sequence);
         }
         seqWasDeleted = true;
       }
-      newstart = command.seqs[i].getStart();
-      newend = command.seqs[i].getEnd();
+      int newStart = sequence.getStart();
+      int newEnd = sequence.getEnd();
 
-      tmp = new StringBuffer();
-      tmp.append(command.seqs[i].getSequence());
+      StringBuilder tmp = new StringBuilder();
+      tmp.append(sequence.getSequence());
       // Undo of a delete does not replace original dataset sequence on to
       // alignment sequence.
 
+      int start = 0;
+      int length = 0;
+
       if (command.string != null && command.string[i] != null)
       {
         if (command.position >= tmp.length())
         {
           // This occurs if padding is on, and residues
           // are removed from end of alignment
-          int length = command.position - tmp.length();
-          while (length > 0)
+          int len = command.position - tmp.length();
+          while (len > 0)
           {
             tmp.append(command.gapChar);
-            length--;
+            len--;
           }
         }
         tmp.insert(command.position, command.string[i]);
         for (int s = 0; s < command.string[i].length; s++)
         {
-          if (jalview.schemes.ResidueProperties.aaIndex[command.string[i][s]] != 23)
+          if (!Comparison.isGap(command.string[i][s]))
           {
+            length++;
             if (!newDSNeeded)
             {
               newDSNeeded = true;
-              start = command.seqs[i].findPosition(command.position);
-              end = command.seqs[i].findPosition(command.position
-                      + command.number);
+              start = sequence.findPosition(command.position);
+              // end = sequence
+              // .findPosition(command.position + command.number);
+            }
+            if (sequence.getStart() == start)
+            {
+              newStart--;
             }
-            if (command.seqs[i].getStart() == start)
-              newstart--;
             else
-              newend++;
+            {
+              newEnd++;
+            }
           }
         }
         command.string[i] = null;
       }
 
-      command.seqs[i].setSequence(tmp.toString());
-      command.seqs[i].setStart(newstart);
-      command.seqs[i].setEnd(newend);
+      sequence.setSequence(tmp.toString());
+      sequence.setStart(newStart);
+      sequence.setEnd(newEnd);
+
+      /*
+       * command and Undo share the same dataset sequence if cut was
+       * at start or end of sequence
+       */
+      boolean sameDatasetSequence = false;
       if (newDSNeeded)
       {
-        if (command.seqs[i].getDatasetSequence() != null)
+        if (sequence.getDatasetSequence() != null)
         {
           SequenceI ds;
           if (newDSWasNeeded)
@@ -413,21 +706,29 @@ public class EditCommand implements CommandI
           {
             // make a new DS sequence
             // use new ds mechanism here
-            ds = new Sequence(command.seqs[i].getName(),
-                    jalview.analysis.AlignSeq.extractGaps(
-                            jalview.util.Comparison.GapChars,
-                            command.seqs[i].getSequenceAsString()),
-                    command.seqs[i].getStart(), command.seqs[i].getEnd());
-            ds.setDescription(command.seqs[i].getDescription());
+            String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
+                    sequence.getSequenceAsString());
+            ds = new Sequence(sequence.getName(), ungapped,
+                    sequence.getStart(), sequence.getEnd());
+            ds.setDescription(sequence.getDescription());
           }
           if (command.oldds == null)
           {
             command.oldds = new SequenceI[command.seqs.length];
           }
-          command.oldds[i] = command.seqs[i].getDatasetSequence();
-          command.seqs[i].setDatasetSequence(ds);
+          command.oldds[i] = sequence.getDatasetSequence();
+          sameDatasetSequence = ds == sequence.getDatasetSequence();
+          ds.setSequenceFeatures(sequence.getSequenceFeatures());
+          if (!sameDatasetSequence && command.al.getDataset() != null)
+          {
+            // delete 'undone' sequence from alignment dataset
+            command.al.getDataset()
+                    .deleteSequence(sequence.getDatasetSequence());
+          }
+          sequence.setDatasetSequence(ds);
         }
-        adjustFeatures(command, i, start, end, true);
+        undoCutFeatures(command, command.seqs[i], start, length,
+                sameDatasetSequence);
       }
     }
     adjustAnnotations(command, true, seqWasDeleted, views);
@@ -435,9 +736,9 @@ public class EditCommand implements CommandI
     command.string = null;
   }
 
-  void replace(Edit command)
+  static void replace(Edit command)
   {
-    StringBuffer tmp;
+    StringBuilder tmp;
     String oldstring;
     int start = command.position;
     int end = command.number;
@@ -448,6 +749,10 @@ public class EditCommand implements CommandI
     command.number = start + command.string[0].length;
     for (int i = 0; i < command.seqs.length; i++)
     {
+      boolean newDSWasNeeded = command.oldds != null
+              && command.oldds[i] != null;
+      boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
+
       /**
        * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
        * cut, sg.getStartRes(), sg.getEndRes()-sg.getStartRes()+1,
@@ -459,18 +764,155 @@ public class EditCommand implements CommandI
        * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
        * 
        */
+      ContiguousI beforeEditedPositions = command.seqs[i].findPositions(1,
+              start);
+      ContiguousI afterEditedPositions = command.seqs[i]
+              .findPositions(end + 1, command.seqs[i].getLength());
+      
       oldstring = command.seqs[i].getSequenceAsString();
-      tmp = new StringBuffer(oldstring.substring(0, start));
+      tmp = new StringBuilder(oldstring.substring(0, start));
       tmp.append(command.string[i]);
-      tmp.append(oldstring.substring(end));
+      String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
+              new String(command.string[i]));
+      if (end < oldstring.length())
+      {
+        tmp.append(oldstring.substring(end));
+      }
+      // stash end prior to updating the sequence object so we can save it if
+      // need be.
+      Range oldstartend = new Range(command.seqs[i].getStart(),
+              command.seqs[i].getEnd());
       command.seqs[i].setSequence(tmp.toString());
-      command.string[i] = oldstring.substring(start, end).toCharArray();
+      command.string[i] = oldstring
+              .substring(start, Math.min(end, oldstring.length()))
+              .toCharArray();
+      String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
+              new String(command.string[i]));
+
+      if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
+      {
+        // we may already have dataset and limits stashed...
+        if (newDSWasNeeded || newStartEndWasNeeded)
+        {
+          if (newDSWasNeeded)
+          {
+          // then just switch the dataset sequence
+          SequenceI oldds = command.seqs[i].getDatasetSequence();
+          command.seqs[i].setDatasetSequence(command.oldds[i]);
+          command.oldds[i] = oldds;
+          }
+          if (newStartEndWasNeeded)
+          {
+            Range newStart = command.oldStartEnd[i];
+            command.oldStartEnd[i] = oldstartend;
+            command.seqs[i].setStart(newStart.getBegin());
+            command.seqs[i].setEnd(newStart.getEnd());
+          }
+        }
+        else         
+        {
+          // decide if we need a new dataset sequence or modify start/end
+          // first edit the original dataset sequence string
+          SequenceI oldds = command.seqs[i].getDatasetSequence();
+          String osp = oldds.getSequenceAsString();
+          int beforeStartOfEdit = -oldds.getStart() + 1
+                  + (beforeEditedPositions == null
+                          ? ((afterEditedPositions != null)
+                                  ? afterEditedPositions.getBegin() - 1
+                                  : oldstartend.getBegin()
+                                          + nogapold.length())
+                          : beforeEditedPositions.getEnd()
+                  );
+          int afterEndOfEdit = -oldds.getStart() + 1
+                  + ((afterEditedPositions == null)
+                  ? oldstartend.getEnd()
+                          : afterEditedPositions.getBegin() - 1);
+          String fullseq = osp.substring(0,
+                  beforeStartOfEdit)
+                  + nogaprep
+                  + osp.substring(afterEndOfEdit);
+
+          // and check if new sequence data is different..
+          if (!fullseq.equalsIgnoreCase(osp))
+          {
+            // old ds and edited ds are different, so
+            // create the new dataset sequence
+            SequenceI newds = new Sequence(oldds);
+            newds.setSequence(fullseq);
+
+            if (command.oldds == null)
+            {
+              command.oldds = new SequenceI[command.seqs.length];
+            }
+            command.oldds[i] = command.seqs[i].getDatasetSequence();
+
+            // And preserve start/end for good-measure
+
+            if (command.oldStartEnd == null)
+            {
+              command.oldStartEnd = new Range[command.seqs.length];
+            }
+            command.oldStartEnd[i] = oldstartend;
+            // TODO: JAL-1131 ensure newly created dataset sequence is added to
+            // the set of
+            // dataset sequences associated with the alignment.
+            // TODO: JAL-1131 fix up any annotation associated with new dataset
+            // sequence to ensure that original sequence/annotation
+            // relationships
+            // are preserved.
+            command.seqs[i].setDatasetSequence(newds);
+          }
+          else
+          {
+            if (command.oldStartEnd == null)
+            {
+              command.oldStartEnd = new Range[command.seqs.length];
+            }
+            command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
+                    command.seqs[i].getEnd());
+            if (beforeEditedPositions != null
+                    && afterEditedPositions == null)
+            {
+              // modification at end
+              command.seqs[i].setEnd(
+                      beforeEditedPositions.getEnd() + nogaprep.length()
+                              - nogapold.length());
+            }
+            else if (afterEditedPositions != null
+                    && beforeEditedPositions == null)
+            {
+              // modification at start
+              command.seqs[i].setStart(
+                      afterEditedPositions.getBegin() - nogaprep.length());
+            }
+            else
+            {
+              // edit covered both start and end. Here we can only guess the
+              // new
+              // start/end
+              String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
+                      command.seqs[i].getSequenceAsString().toUpperCase());
+              int newStart = command.seqs[i].getDatasetSequence()
+                      .getSequenceAsString().indexOf(nogapalseq);
+              if (newStart == -1)
+              {
+                throw new Error(
+                        "Implementation Error: could not locate start/end "
+                                + "in dataset sequence after an edit of the sequence string");
+              }
+              int newEnd = newStart + nogapalseq.length() - 1;
+              command.seqs[i].setStart(newStart);
+              command.seqs[i].setEnd(newEnd);
+            }
+          }
+        }
+      }
       tmp = null;
       oldstring = null;
     }
   }
 
-  final void adjustAnnotations(Edit command, boolean insert,
+  final static void adjustAnnotations(Edit command, boolean insert,
           boolean modifyVisibility, AlignmentI[] views)
   {
     AlignmentAnnotation[] annotations = null;
@@ -478,7 +920,7 @@ public class EditCommand implements CommandI
     if (modifyVisibility && !insert)
     {
       // only occurs if a sequence was added or deleted.
-      command.deletedAnnotationRows = new Hashtable();
+      command.deletedAnnotationRows = new Hashtable<>();
     }
     if (command.fullAlignmentHeight)
     {
@@ -490,6 +932,8 @@ public class EditCommand implements CommandI
       AlignmentAnnotation[] tmp;
       for (int s = 0; s < command.seqs.length; s++)
       {
+        command.seqs[s].sequenceChanged();
+
         if (modifyVisibility)
         {
           // Rows are only removed or added to sequence object.
@@ -526,7 +970,8 @@ public class EditCommand implements CommandI
                 tmp = saved;
                 command.deletedAnnotationRows.put(command.seqs[s], saved);
                 // and then remove any annotation in the other views
-                for (int alview = 0; views != null && alview < views.length; alview++)
+                for (int alview = 0; views != null
+                        && alview < views.length; alview++)
                 {
                   if (views[alview] != command.al)
                   {
@@ -562,7 +1007,7 @@ public class EditCommand implements CommandI
                     && command.deletedAnnotationRows
                             .containsKey(command.seqs[s]))
             {
-              AlignmentAnnotation[] revealed = (AlignmentAnnotation[]) command.deletedAnnotationRows
+              AlignmentAnnotation[] revealed = command.deletedAnnotationRows
                       .get(command.seqs[s]);
               command.seqs[s].setAlignmentAnnotation(revealed);
               if (revealed != null)
@@ -633,7 +1078,7 @@ public class EditCommand implements CommandI
 
     if (!insert)
     {
-      command.deletedAnnotations = new Hashtable();
+      command.deletedAnnotations = new Hashtable<>();
     }
 
     int aSize;
@@ -653,10 +1098,12 @@ public class EditCommand implements CommandI
       {
         temp = new Annotation[aSize + command.number];
         if (annotations[a].padGaps)
+        {
           for (int aa = 0; aa < temp.length; aa++)
           {
             temp[aa] = new Annotation(command.gapChar + "", null, ' ', 0);
           }
+        }
       }
       else
       {
@@ -694,7 +1141,7 @@ public class EditCommand implements CommandI
                   && command.deletedAnnotations
                           .containsKey(annotations[a].annotationId))
           {
-            Annotation[] restore = (Annotation[]) command.deletedAnnotations
+            Annotation[] restore = command.deletedAnnotations
                     .get(annotations[a].annotationId);
 
             System.arraycopy(restore, 0, temp, command.position,
@@ -703,8 +1150,8 @@ public class EditCommand implements CommandI
           }
 
           System.arraycopy(annotations[a].annotations, command.position,
-                  temp, command.position + command.number, aSize
-                          - command.position);
+                  temp, command.position + command.number,
+                  aSize - command.position);
         }
         else
         {
@@ -712,7 +1159,7 @@ public class EditCommand implements CommandI
                   && command.deletedAnnotations
                           .containsKey(annotations[a].annotationId))
           {
-            Annotation[] restore = (Annotation[]) command.deletedAnnotations
+            Annotation[] restore = command.deletedAnnotations
                     .get(annotations[a].annotationId);
 
             temp = new Annotation[annotations[a].annotations.length
@@ -735,8 +1182,10 @@ public class EditCommand implements CommandI
           int copylen = Math.min(command.position,
                   annotations[a].annotations.length);
           if (copylen > 0)
+          {
             System.arraycopy(annotations[a].annotations, 0, temp, 0,
                     copylen); // command.position);
+          }
 
           Annotation[] deleted = new Annotation[command.number];
           if (copylen >= command.position)
@@ -745,8 +1194,8 @@ public class EditCommand implements CommandI
                     annotations[a].annotations.length - command.position);
             if (copylen > 0)
             {
-              System.arraycopy(annotations[a].annotations,
-                      command.position, deleted, 0, copylen); // command.number);
+              System.arraycopy(annotations[a].annotations, command.position,
+                      deleted, 0, copylen); // command.number);
             }
           }
 
@@ -755,10 +1204,10 @@ public class EditCommand implements CommandI
           if (annotations[a].annotations.length > command.position
                   + command.number)
           {
-            System.arraycopy(annotations[a].annotations, command.position
-                    + command.number, temp, command.position,
-                    annotations[a].annotations.length - command.position
-                            - command.number); // aSize
+            System.arraycopy(annotations[a].annotations,
+                    command.position + command.number, temp,
+                    command.position, annotations[a].annotations.length
+                            - command.position - command.number); // aSize
           }
         }
         else
@@ -790,99 +1239,238 @@ public class EditCommand implements CommandI
     }
   }
 
-  final void adjustFeatures(Edit command, int index, int i, int j,
-          boolean insert)
+  /**
+   * Restores features to the state before a Cut.
+   * <ul>
+   * <li>re-add any features deleted by the cut</li>
+   * <li>remove any truncated features created by the cut</li>
+   * <li>shift right any features to the right of the cut</li>
+   * </ul>
+   * 
+   * @param command
+   *          the Cut command
+   * @param seq
+   *          the sequence the Cut applied to
+   * @param start
+   *          the start residue position of the cut
+   * @param length
+   *          the number of residues cut
+   * @param sameDatasetSequence
+   *          true if dataset sequence and frame of reference were left
+   *          unchanged by the Cut
+   */
+  final static void undoCutFeatures(Edit command, SequenceI seq,
+          final int start, final int length, boolean sameDatasetSequence)
   {
-    SequenceI seq = command.seqs[index];
     SequenceI sequence = seq.getDatasetSequence();
     if (sequence == null)
     {
       sequence = seq;
     }
 
-    if (insert)
+    /*
+     * shift right features that lie to the right of the restored cut (but not 
+     * if dataset sequence unchanged - so coordinates were changed by Cut)
+     */
+    if (!sameDatasetSequence)
     {
-      if (command.editedFeatures != null
-              && command.editedFeatures.containsKey(seq))
+      /*
+       * shift right all features right of and not 
+       * contiguous with the cut position
+       */
+      seq.getFeatures().shiftFeatures(start + 1, length);
+
+      /*
+       * shift right any features that start at the cut position,
+       * unless they were truncated
+       */
+      List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
+              start);
+      for (SequenceFeature sf : sfs)
       {
-        sequence.setSequenceFeatures((SequenceFeature[]) command.editedFeatures
-                .get(seq));
+        if (sf.getBegin() == start)
+        {
+          if (!command.truncatedFeatures.containsKey(seq)
+                  || !command.truncatedFeatures.get(seq).contains(sf))
+          {
+            /*
+             * feature was shifted left to cut position (not truncated),
+             * so shift it back right
+             */
+            SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
+                    + length, sf.getEnd() + length, sf.getFeatureGroup(),
+                    sf.getScore());
+            seq.addSequenceFeature(shifted);
+            seq.deleteFeature(sf);
+          }
+        }
       }
-
-      return;
     }
 
-    SequenceFeature[] sf = sequence.getSequenceFeatures();
-
-    if (sf == null)
-    {
-      return;
-    }
-
-    SequenceFeature[] oldsf = new SequenceFeature[sf.length];
-
-    int cSize = j - i;
-
-    for (int s = 0; s < sf.length; s++)
+    /*
+     * restore any features that were deleted or truncated
+     */
+    if (command.deletedFeatures != null
+            && command.deletedFeatures.containsKey(seq))
     {
-      SequenceFeature copy = new SequenceFeature(sf[s]);
-
-      oldsf[s] = copy;
-
-      if (sf[s].getEnd() < i)
-      {
-        continue;
-      }
-
-      if (sf[s].getBegin() > j)
-      {
-        sf[s].setBegin(copy.getBegin() - cSize);
-        sf[s].setEnd(copy.getEnd() - cSize);
-        continue;
-      }
-
-      if (sf[s].getBegin() >= i)
+      for (SequenceFeature deleted : command.deletedFeatures.get(seq))
       {
-        sf[s].setBegin(i);
+        sequence.addSequenceFeature(deleted);
       }
+    }
 
-      if (sf[s].getEnd() < j)
+    /*
+     * delete any truncated features
+     */
+    if (command.truncatedFeatures != null
+            && command.truncatedFeatures.containsKey(seq))
+    {
+      for (SequenceFeature amended : command.truncatedFeatures.get(seq))
       {
-        sf[s].setEnd(j - 1);
+        sequence.deleteFeature(amended);
       }
+    }
+  }
 
-      sf[s].setEnd(sf[s].getEnd() - (cSize));
+  /**
+   * Returns the list of edit commands wrapped by this object.
+   * 
+   * @return
+   */
+  public List<Edit> getEdits()
+  {
+    return this.edits;
+  }
 
-      if (sf[s].getBegin() > sf[s].getEnd())
+  /**
+   * Returns a map whose keys are the dataset sequences, and values their
+   * aligned sequences before the command edit list was applied. The aligned
+   * sequences are copies, which may be updated without affecting the originals.
+   * 
+   * The command holds references to the aligned sequences (after editing). If
+   * the command is an 'undo',then the prior state is simply the aligned state.
+   * Otherwise, we have to derive the prior state by working backwards through
+   * the edit list to infer the aligned sequences before editing.
+   * 
+   * Note: an alternative solution would be to cache the 'before' state of each
+   * edit, but this would be expensive in space in the common case that the
+   * original is never needed (edits are not mirrored).
+   * 
+   * @return
+   * @throws IllegalStateException
+   *           on detecting an edit command of a type that can't be unwound
+   */
+  public Map<SequenceI, SequenceI> priorState(boolean forUndo)
+  {
+    Map<SequenceI, SequenceI> result = new HashMap<>();
+    if (getEdits() == null)
+    {
+      return result;
+    }
+    if (forUndo)
+    {
+      for (Edit e : getEdits())
       {
-        sequence.deleteFeature(sf[s]);
+        for (SequenceI seq : e.getSequences())
+        {
+          SequenceI ds = seq.getDatasetSequence();
+          // SequenceI preEdit = result.get(ds);
+          if (!result.containsKey(ds))
+          {
+            /*
+             * copy sequence including start/end (but don't use copy constructor
+             * as we don't need annotations)
+             */
+            SequenceI preEdit = new Sequence("", seq.getSequenceAsString(),
+                    seq.getStart(), seq.getEnd());
+            preEdit.setDatasetSequence(ds);
+            result.put(ds, preEdit);
+          }
+        }
       }
+      return result;
     }
 
-    if (command.editedFeatures == null)
+    /*
+     * Work backwards through the edit list, deriving the sequences before each
+     * was applied. The final result is the sequence set before any edits.
+     */
+    Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
+    while (editList.hasNext())
     {
-      command.editedFeatures = new Hashtable();
+      Edit oldEdit = editList.next();
+      Action action = oldEdit.getAction();
+      int position = oldEdit.getPosition();
+      int number = oldEdit.getNumber();
+      final char gap = oldEdit.getGapCharacter();
+      for (SequenceI seq : oldEdit.getSequences())
+      {
+        SequenceI ds = seq.getDatasetSequence();
+        SequenceI preEdit = result.get(ds);
+        if (preEdit == null)
+        {
+          preEdit = new Sequence("", seq.getSequenceAsString(),
+                  seq.getStart(), seq.getEnd());
+          preEdit.setDatasetSequence(ds);
+          result.put(ds, preEdit);
+        }
+        /*
+         * 'Undo' this edit action on the sequence (updating the value in the
+         * map).
+         */
+        if (ds != null)
+        {
+          if (action == Action.DELETE_GAP)
+          {
+            preEdit.setSequence(new String(StringUtils.insertCharAt(
+                    preEdit.getSequence(), position, number, gap)));
+          }
+          else if (action == Action.INSERT_GAP)
+          {
+            preEdit.setSequence(new String(StringUtils.deleteChars(
+                    preEdit.getSequence(), position, position + number)));
+          }
+          else
+          {
+            System.err.println("Can't undo edit action " + action);
+            // throw new IllegalStateException("Can't undo edit action " +
+            // action);
+          }
+        }
+      }
     }
-
-    command.editedFeatures.put(seq, oldsf);
-
+    return result;
   }
 
-  class Edit
+  public class Edit
   {
-    public SequenceI[] oldds;
+    SequenceI[] oldds;
+
+    /**
+     * start and end of sequence prior to edit
+     */
+    Range[] oldStartEnd;
 
     boolean fullAlignmentHeight = false;
 
-    Hashtable deletedAnnotationRows;
+    Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
+
+    Map<String, Annotation[]> deletedAnnotations;
 
-    Hashtable deletedAnnotations;
+    /*
+     * features deleted by the cut (re-add on Undo)
+     * (including the original of any shortened features)
+     */
+    Map<SequenceI, List<SequenceFeature>> deletedFeatures;
 
-    Hashtable editedFeatures;
+    /*
+     * shortened features added by the cut (delete on Undo)
+     */
+    Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
 
     AlignmentI al;
 
-    int command;
+    final Action command;
 
     char[][] string;
 
@@ -890,55 +1478,274 @@ public class EditCommand implements CommandI
 
     int[] alIndex;
 
-    int position, number;
+    int position;
+
+    int number;
 
     char gapChar;
 
-    Edit(int command, SequenceI[] seqs, int position, int number,
-            char gapChar)
+    /*
+     * flag that identifies edits inserted to balance 
+     * user edits in a 'locked editing' region
+     */
+    private boolean systemGenerated;
+
+    public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            char gap)
     {
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.gapChar = gapChar;
+      this.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      this.gapChar = gap;
     }
 
-    Edit(int command, SequenceI[] seqs, int position, int number,
-            AlignmentI al)
+    Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            AlignmentI align)
     {
-      this.gapChar = al.getGapCharacter();
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.al = al;
+      this(cmd, sqs, pos, count, align.getGapCharacter());
+
+      this.al = align;
 
-      alIndex = new int[seqs.length];
-      for (int i = 0; i < seqs.length; i++)
+      alIndex = new int[sqs.length];
+      for (int i = 0; i < sqs.length; i++)
       {
-        alIndex[i] = al.findIndex(seqs[i]);
+        alIndex[i] = align.findIndex(sqs[i]);
       }
 
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
+      fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
-    Edit(int command, SequenceI[] seqs, int position, int number,
-            AlignmentI al, String replace)
+    /**
+     * Constructor given a REPLACE command and the replacement string
+     * 
+     * @param cmd
+     * @param sqs
+     * @param pos
+     * @param count
+     * @param align
+     * @param replace
+     */
+    Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            AlignmentI align, String replace)
     {
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.al = al;
-      this.gapChar = al.getGapCharacter();
-      string = new char[seqs.length][];
-      for (int i = 0; i < seqs.length; i++)
+      this(cmd, sqs, pos, count, align);
+
+      string = new char[sqs.length][];
+      for (int i = 0; i < sqs.length; i++)
       {
         string[i] = replace.toCharArray();
       }
+    }
+
+    public SequenceI[] getSequences()
+    {
+      return seqs;
+    }
+
+    public int getPosition()
+    {
+      return position;
+    }
+
+    public Action getAction()
+    {
+      return command;
+    }
+
+    public int getNumber()
+    {
+      return number;
+    }
+
+    public char getGapCharacter()
+    {
+      return gapChar;
+    }
+
+    public void setSystemGenerated(boolean b)
+    {
+      systemGenerated = b;
+    }
+
+    public boolean isSystemGenerated()
+    {
+      return systemGenerated;
+    }
+  }
+
+  /**
+   * Returns an iterator over the list of edit commands which traverses the list
+   * either forwards or backwards.
+   * 
+   * @param forwards
+   * @return
+   */
+  public Iterator<Edit> getEditIterator(boolean forwards)
+  {
+    if (forwards)
+    {
+      return getEdits().iterator();
+    }
+    else
+    {
+      return new ReverseListIterator<>(getEdits());
+    }
+  }
+
+  /**
+   * Adjusts features for Cut, and saves details of changes made to allow Undo
+   * <ul>
+   * <li>features left of the cut are unchanged</li>
+   * <li>features right of the cut are shifted left</li>
+   * <li>features internal to the cut region are deleted</li>
+   * <li>features that overlap or span the cut are shortened</li>
+   * <li>the originals of any deleted or shortened features are saved, to re-add
+   * on Undo</li>
+   * <li>any added (shortened) features are saved, to delete on Undo</li>
+   * </ul>
+   * 
+   * @param command
+   * @param seq
+   * @param fromPosition
+   * @param toPosition
+   * @param cutIsInternal
+   */
+  protected static void cutFeatures(Edit command, SequenceI seq,
+          int fromPosition, int toPosition, boolean cutIsInternal)
+  {
+    /* 
+     * if the cut is at start or end of sequence
+     * then we don't modify the sequence feature store
+     */
+    if (!cutIsInternal)
+    {
+      return;
+    }
+    List<SequenceFeature> added = new ArrayList<>();
+    List<SequenceFeature> removed = new ArrayList<>();
+  
+    SequenceFeaturesI featureStore = seq.getFeatures();
+    if (toPosition < fromPosition || featureStore == null)
+    {
+      return;
+    }
+  
+    int cutStartPos = fromPosition;
+    int cutEndPos = toPosition;
+    int cutWidth = cutEndPos - cutStartPos + 1;
+  
+    synchronized (featureStore)
+    {
+      /*
+       * get features that overlap the cut region
+       */
+      List<SequenceFeature> toAmend = featureStore.findFeatures(
+              cutStartPos, cutEndPos);
+  
+      /*
+       * add any contact features that span the cut region
+       * (not returned by findFeatures)
+       */
+      for (SequenceFeature contact : featureStore.getContactFeatures())
+      {
+        if (contact.getBegin() < cutStartPos
+                && contact.getEnd() > cutEndPos)
+        {
+          toAmend.add(contact);
+        }
+      }
 
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
+      /*
+       * adjust start-end of overlapping features;
+       * delete features enclosed by the cut;
+       * delete partially overlapping contact features
+       */
+      for (SequenceFeature sf : toAmend)
+      {
+        int sfBegin = sf.getBegin();
+        int sfEnd = sf.getEnd();
+        int newBegin = sfBegin;
+        int newEnd = sfEnd;
+        boolean toDelete = false;
+        boolean follows = false;
+        
+        if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
+        {
+          /*
+           * feature lies within cut region - delete it
+           */
+          toDelete = true;
+        }
+        else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
+        {
+          /*
+           * feature spans cut region - left-shift the end
+           */
+          newEnd -= cutWidth;
+        }
+        else if (sfEnd <= cutEndPos)
+        {
+          /*
+           * feature overlaps left of cut region - truncate right
+           */
+          newEnd = cutStartPos - 1;
+          if (sf.isContactFeature())
+          {
+            toDelete = true;
+          }
+        }
+        else if (sfBegin >= cutStartPos)
+        {
+          /*
+           * remaining case - feature overlaps right
+           * truncate left, adjust end of feature
+           */
+          newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
+          newEnd = newBegin + sfEnd - cutEndPos - 1;
+          if (sf.isContactFeature())
+          {
+            toDelete = true;
+          }
+        }
+  
+        seq.deleteFeature(sf);
+        if (!follows)
+        {
+          removed.add(sf);
+        }
+        if (!toDelete)
+        {
+          SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+                  sf.getFeatureGroup(), sf.getScore());
+          seq.addSequenceFeature(copy);
+          if (!follows)
+          {
+            added.add(copy);
+          }
+        }
+      }
+  
+      /*
+       * and left shift any features lying to the right of the cut region
+       */
+
+      featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
+    }
+
+    /*
+     * save deleted and amended features, so that Undo can 
+     * re-add or delete them respectively
+     */
+    if (command.deletedFeatures == null)
+    {
+      command.deletedFeatures = new HashMap<>();
+    }
+    if (command.truncatedFeatures == null)
+    {
+      command.truncatedFeatures = new HashMap<>();
     }
+    command.deletedFeatures.put(seq, removed);
+    command.truncatedFeatures.put(seq, added);
   }
 }