1 /******************************************************************************* 2 * Copyright (c) 2000, 2018 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.jface.text; 15 16 import java.util.ArrayList; 17 import java.util.List; 18 19 import org.eclipse.swt.SWT; 20 import org.eclipse.swt.custom.StyledText; 21 import org.eclipse.swt.events.KeyEvent; 22 import org.eclipse.swt.events.KeyListener; 23 import org.eclipse.swt.events.MouseEvent; 24 import org.eclipse.swt.events.MouseListener; 25 import org.eclipse.swt.widgets.Display; 26 import org.eclipse.swt.widgets.Shell; 27 28 import org.eclipse.core.commands.ExecutionException; 29 import org.eclipse.core.commands.operations.AbstractOperation; 30 import org.eclipse.core.commands.operations.IOperationHistory; 31 import org.eclipse.core.commands.operations.IOperationHistoryListener; 32 import org.eclipse.core.commands.operations.IUndoContext; 33 import org.eclipse.core.commands.operations.IUndoableOperation; 34 import org.eclipse.core.commands.operations.ObjectUndoContext; 35 import org.eclipse.core.commands.operations.OperationHistoryEvent; 36 import org.eclipse.core.commands.operations.OperationHistoryFactory; 37 38 import org.eclipse.core.runtime.IAdaptable; 39 import org.eclipse.core.runtime.IProgressMonitor; 40 import org.eclipse.core.runtime.IStatus; 41 import org.eclipse.core.runtime.Status; 42 43 import org.eclipse.jface.dialogs.MessageDialog; 44 45 46 /** 47 * Standard implementation of {@link org.eclipse.jface.text.IUndoManager}. 48 * <p> 49 * It registers with the connected text viewer as text input listener and 50 * document listener and logs all changes. It also monitors mouse and keyboard 51 * activities in order to partition the stream of text changes into undo-able 52 * edit commands. 53 * </p> 54 * <p> 55 * Since 3.1 this undo manager is a facade to the global operation history. 56 * </p> 57 * <p> 58 * The usage of {@link org.eclipse.core.runtime.IAdaptable} in the JFace 59 * layer has been approved by Platform UI, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=87669#c9 60 * </p> 61 * <p> 62 * This class is not intended to be subclassed. 63 * </p> 64 * 65 * @see org.eclipse.jface.text.ITextViewer 66 * @see org.eclipse.jface.text.ITextInputListener 67 * @see org.eclipse.jface.text.IDocumentListener 68 * @see org.eclipse.core.commands.operations.IUndoableOperation 69 * @see org.eclipse.core.commands.operations.IOperationHistory 70 * @see MouseListener 71 * @see KeyListener 72 * @deprecated As of 3.2, replaced by {@link TextViewerUndoManager} 73 * @noextend This class is not intended to be subclassed by clients. 74 */ 75 @Deprecated 76 public class DefaultUndoManager implements IUndoManager, IUndoManagerExtension { 77 78 /** 79 * Represents an undo-able edit command. 80 * <p> 81 * Since 3.1 this implements the interface for IUndoableOperation. 82 * </p> 83 */ 84 class TextCommand extends AbstractOperation { 85 86 /** The start index of the replaced text. */ 87 protected int fStart= -1; 88 /** The end index of the replaced text. */ 89 protected int fEnd= -1; 90 /** The newly inserted text. */ 91 protected String fText; 92 /** The replaced text. */ 93 protected String fPreservedText; 94 95 /** The undo modification stamp. */ 96 protected long fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 97 /** The redo modification stamp. */ 98 protected long fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 99 100 /** 101 * Creates a new text command. 102 * 103 * @param context the undo context for this command 104 * @since 3.1 105 */ TextCommand(IUndoContext context)106 TextCommand(IUndoContext context) { 107 super(JFaceTextMessages.getString("DefaultUndoManager.operationLabel")); //$NON-NLS-1$ 108 addContext(context); 109 } 110 111 /** 112 * Re-initializes this text command. 113 */ reinitialize()114 protected void reinitialize() { 115 fStart= fEnd= -1; 116 fText= fPreservedText= null; 117 fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 118 fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 119 } 120 121 /** 122 * Sets the start and the end index of this command. 123 * 124 * @param start the start index 125 * @param end the end index 126 */ set(int start, int end)127 protected void set(int start, int end) { 128 fStart= start; 129 fEnd= end; 130 fText= null; 131 fPreservedText= null; 132 } 133 134 @Override dispose()135 public void dispose() { 136 reinitialize(); 137 } 138 139 /** 140 * Undo the change described by this command. 141 * 142 * @since 2.0 143 */ undoTextChange()144 protected void undoTextChange() { 145 try { 146 IDocument document= fTextViewer.getDocument(); 147 if (document instanceof IDocumentExtension4) 148 ((IDocumentExtension4)document).replace(fStart, fText.length(), fPreservedText, fUndoModificationStamp); 149 else 150 document.replace(fStart, fText.length(), fPreservedText); 151 } catch (BadLocationException x) { 152 } 153 } 154 155 @Override canUndo()156 public boolean canUndo() { 157 158 if (isConnected() && isValid()) { 159 IDocument doc= fTextViewer.getDocument(); 160 if (doc instanceof IDocumentExtension4) { 161 long docStamp= ((IDocumentExtension4)doc).getModificationStamp(); 162 163 // Normal case: an undo is valid if its redo will restore document 164 // to its current modification stamp 165 boolean canUndo= docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP || 166 docStamp == getRedoModificationStamp(); 167 168 /* Special case to check if the answer is false. 169 * If the last document change was empty, then the document's 170 * modification stamp was incremented but nothing was committed. 171 * The operation being queried has an older stamp. In this case only, 172 * the comparison is different. A sequence of document changes that 173 * include an empty change is handled correctly when a valid commit 174 * follows the empty change, but when #canUndo() is queried just after 175 * an empty change, we must special case the check. The check is very 176 * specific to prevent false positives. 177 * see https://bugs.eclipse.org/bugs/show_bug.cgi?id=98245 178 */ 179 if (!canUndo && 180 this == fHistory.getUndoOperation(fUndoContext) && // this is the latest operation 181 this != fCurrent && // there is a more current operation not on the stack 182 !fCurrent.isValid() && // the current operation is not a valid document modification 183 fCurrent.fUndoModificationStamp != // the invalid current operation has a document stamp 184 IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { 185 canUndo= fCurrent.fRedoModificationStamp == docStamp; 186 } 187 /* 188 * When the composite is the current command, it may hold the timestamp 189 * of a no-op change. We check this here rather than in an override of 190 * canUndo() in CompoundTextCommand simply to keep all the special case checks 191 * in one place. 192 */ 193 if (!canUndo && 194 this == fHistory.getUndoOperation(fUndoContext) && // this is the latest operation 195 this instanceof CompoundTextCommand && 196 this == fCurrent && // this is the current operation 197 this.fStart == -1 && // the current operation text is not valid 198 fCurrent.fRedoModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { // but it has a redo stamp 199 canUndo= fCurrent.fRedoModificationStamp == docStamp; 200 } 201 202 } 203 // if there is no timestamp to check, simply return true per the 3.0.1 behavior 204 return true; 205 } 206 return false; 207 } 208 209 @Override canRedo()210 public boolean canRedo() { 211 if (isConnected() && isValid()) { 212 IDocument doc= fTextViewer.getDocument(); 213 if (doc instanceof IDocumentExtension4) { 214 long docStamp= ((IDocumentExtension4)doc).getModificationStamp(); 215 return docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP || 216 docStamp == getUndoModificationStamp(); 217 } 218 // if there is no timestamp to check, simply return true per the 3.0.1 behavior 219 return true; 220 } 221 return false; 222 } 223 224 @Override canExecute()225 public boolean canExecute() { 226 return isConnected(); 227 } 228 229 @Override execute(IProgressMonitor monitor, IAdaptable uiInfo)230 public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) { 231 // Text commands execute as they are typed, so executing one has no effect. 232 return Status.OK_STATUS; 233 } 234 235 /* 236 * Undo the change described by this command. Also selects and 237 * reveals the change. 238 */ 239 240 /** 241 * Undo the change described by this command. Also selects and 242 * reveals the change. 243 * 244 * @param monitor the progress monitor to use if necessary 245 * @param uiInfo an adaptable that can provide UI info if needed 246 * @return the status 247 */ 248 @Override undo(IProgressMonitor monitor, IAdaptable uiInfo)249 public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) { 250 if (isValid()) { 251 undoTextChange(); 252 selectAndReveal(fStart, fPreservedText == null ? 0 : fPreservedText.length()); 253 resetProcessChangeSate(); 254 return Status.OK_STATUS; 255 } 256 return IOperationHistory.OPERATION_INVALID_STATUS; 257 } 258 259 /** 260 * Re-applies the change described by this command. 261 * 262 * @since 2.0 263 */ redoTextChange()264 protected void redoTextChange() { 265 try { 266 IDocument document= fTextViewer.getDocument(); 267 if (document instanceof IDocumentExtension4) 268 ((IDocumentExtension4)document).replace(fStart, fEnd - fStart, fText, fRedoModificationStamp); 269 else 270 fTextViewer.getDocument().replace(fStart, fEnd - fStart, fText); 271 } catch (BadLocationException x) { 272 } 273 } 274 275 /** 276 * Re-applies the change described by this command that previously been 277 * rolled back. Also selects and reveals the change. 278 * 279 * @param monitor the progress monitor to use if necessary 280 * @param uiInfo an adaptable that can provide UI info if needed 281 * @return the status 282 */ 283 @Override redo(IProgressMonitor monitor, IAdaptable uiInfo)284 public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) { 285 if (isValid()) { 286 redoTextChange(); 287 resetProcessChangeSate(); 288 selectAndReveal(fStart, fText == null ? 0 : fText.length()); 289 return Status.OK_STATUS; 290 } 291 return IOperationHistory.OPERATION_INVALID_STATUS; 292 } 293 294 /** 295 * Update the command in response to a commit. 296 * 297 * @since 3.1 298 */ 299 updateCommand()300 protected void updateCommand() { 301 fText= fTextBuffer.toString(); 302 fTextBuffer.setLength(0); 303 fPreservedText= fPreservedTextBuffer.toString(); 304 fPreservedTextBuffer.setLength(0); 305 } 306 307 /** 308 * Creates a new uncommitted text command depending on whether 309 * a compound change is currently being executed. 310 * 311 * @return a new, uncommitted text command or a compound text command 312 */ createCurrent()313 protected TextCommand createCurrent() { 314 return fFoldingIntoCompoundChange ? new CompoundTextCommand(fUndoContext) : new TextCommand(fUndoContext); 315 } 316 317 /** 318 * Commits the current change into this command. 319 */ commit()320 protected void commit() { 321 if (fStart < 0) { 322 if (fFoldingIntoCompoundChange) { 323 fCurrent= createCurrent(); 324 } else { 325 reinitialize(); 326 } 327 } else { 328 updateCommand(); 329 fCurrent= createCurrent(); 330 } 331 resetProcessChangeSate(); 332 } 333 334 /** 335 * Updates the text from the buffers without resetting 336 * the buffers or adding anything to the stack. 337 * 338 * @since 3.1 339 */ pretendCommit()340 protected void pretendCommit() { 341 if (fStart > -1) { 342 fText= fTextBuffer.toString(); 343 fPreservedText= fPreservedTextBuffer.toString(); 344 } 345 } 346 347 /** 348 * Attempt a commit of this command and answer true if a new 349 * fCurrent was created as a result of the commit. 350 * 351 * @return true if the command was committed and created a 352 * new fCurrent, false if not. 353 * @since 3.1 354 */ attemptCommit()355 protected boolean attemptCommit() { 356 pretendCommit(); 357 if (isValid()) { 358 DefaultUndoManager.this.commit(); 359 return true; 360 } 361 return false; 362 } 363 364 /** 365 * Checks whether this text command is valid for undo or redo. 366 * 367 * @return <code>true</code> if the command is valid for undo or redo 368 * @since 3.1 369 */ isValid()370 protected boolean isValid() { 371 return fStart > -1 && 372 fEnd > -1 && 373 fText != null; 374 } 375 376 @Override toString()377 public String toString() { 378 String delimiter= ", "; //$NON-NLS-1$ 379 StringBuilder text= new StringBuilder(super.toString()); 380 text.append("\n"); //$NON-NLS-1$ 381 text.append(this.getClass().getName()); 382 text.append(" undo modification stamp: "); //$NON-NLS-1$ 383 text.append(fUndoModificationStamp); 384 text.append(" redo modification stamp: "); //$NON-NLS-1$ 385 text.append(fRedoModificationStamp); 386 text.append(" start: "); //$NON-NLS-1$ 387 text.append(fStart); 388 text.append(delimiter); 389 text.append("end: "); //$NON-NLS-1$ 390 text.append(fEnd); 391 text.append(delimiter); 392 text.append("text: '"); //$NON-NLS-1$ 393 text.append(fText); 394 text.append('\''); 395 text.append(delimiter); 396 text.append("preservedText: '"); //$NON-NLS-1$ 397 text.append(fPreservedText); 398 text.append('\''); 399 return text.toString(); 400 } 401 402 /** 403 * Return the undo modification stamp 404 * 405 * @return the undo modification stamp for this command 406 * @since 3.1 407 */ getUndoModificationStamp()408 protected long getUndoModificationStamp() { 409 return fUndoModificationStamp; 410 } 411 412 /** 413 * Return the redo modification stamp 414 * 415 * @return the redo modification stamp for this command 416 * @since 3.1 417 */ getRedoModificationStamp()418 protected long getRedoModificationStamp() { 419 return fRedoModificationStamp; 420 } 421 } 422 423 /** 424 * Represents an undo-able edit command consisting of several 425 * individual edit commands. 426 */ 427 class CompoundTextCommand extends TextCommand { 428 429 /** The list of individual commands */ 430 private List<TextCommand> fCommands= new ArrayList<>(); 431 432 /** 433 * Creates a new compound text command. 434 * 435 * @param context the undo context for this command 436 * @since 3.1 437 */ CompoundTextCommand(IUndoContext context)438 CompoundTextCommand(IUndoContext context) { 439 super(context); 440 } 441 442 /** 443 * Adds a new individual command to this compound command. 444 * 445 * @param command the command to be added 446 */ add(TextCommand command)447 protected void add(TextCommand command) { 448 fCommands.add(command); 449 } 450 451 @Override undo(IProgressMonitor monitor, IAdaptable uiInfo)452 public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) { 453 resetProcessChangeSate(); 454 455 int size= fCommands.size(); 456 if (size > 0) { 457 458 TextCommand c; 459 460 for (int i= size -1; i > 0; --i) { 461 c= fCommands.get(i); 462 c.undoTextChange(); 463 } 464 465 c= fCommands.get(0); 466 c.undo(monitor, uiInfo); 467 } 468 469 return Status.OK_STATUS; 470 } 471 472 @Override redo(IProgressMonitor monitor, IAdaptable uiInfo)473 public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) { 474 resetProcessChangeSate(); 475 476 int size= fCommands.size(); 477 if (size > 0) { 478 479 TextCommand c; 480 481 for (int i= 0; i < size -1; ++i) { 482 c= fCommands.get(i); 483 c.redoTextChange(); 484 } 485 486 c= fCommands.get(size -1); 487 c.redo(monitor, uiInfo); 488 } 489 return Status.OK_STATUS; 490 } 491 492 /* 493 * @see TextCommand#updateCommand 494 495 */ 496 497 @Override updateCommand()498 protected void updateCommand() { 499 // first gather the data from the buffers 500 super.updateCommand(); 501 502 // the result of the command update is stored as a child command 503 TextCommand c= new TextCommand(fUndoContext); 504 c.fStart= fStart; 505 c.fEnd= fEnd; 506 c.fText= fText; 507 c.fPreservedText= fPreservedText; 508 c.fUndoModificationStamp= fUndoModificationStamp; 509 c.fRedoModificationStamp= fRedoModificationStamp; 510 add(c); 511 512 // clear out all indexes now that the child is added 513 reinitialize(); 514 } 515 516 /* 517 * @see TextCommand#createCurrent 518 */ 519 @Override createCurrent()520 protected TextCommand createCurrent() { 521 522 if (!fFoldingIntoCompoundChange) 523 return new TextCommand(fUndoContext); 524 525 reinitialize(); 526 return this; 527 } 528 529 @Override commit()530 protected void commit() { 531 // if there is pending data, update the command 532 if (fStart > -1) 533 updateCommand(); 534 fCurrent= createCurrent(); 535 resetProcessChangeSate(); 536 } 537 538 /** 539 * Checks whether the command is valid for undo or redo. 540 * 541 * @return true if the command is valid. 542 * @since 3.1 543 */ 544 @Override isValid()545 protected boolean isValid() { 546 if (isConnected()) 547 return (fStart > -1 || !fCommands.isEmpty()); 548 return false; 549 } 550 551 /** 552 * Returns the undo modification stamp. 553 * 554 * @return the undo modification stamp 555 * @since 3.1 556 */ 557 @Override getUndoModificationStamp()558 protected long getUndoModificationStamp() { 559 if (fStart > -1) 560 return super.getUndoModificationStamp(); 561 else if (!fCommands.isEmpty()) 562 return fCommands.get(0).getUndoModificationStamp(); 563 564 return fUndoModificationStamp; 565 } 566 567 /** 568 * Returns the redo modification stamp. 569 * 570 * @return the redo modification stamp 571 * @since 3.1 572 */ 573 @Override getRedoModificationStamp()574 protected long getRedoModificationStamp() { 575 if (fStart > -1) 576 return super.getRedoModificationStamp(); 577 else if (!fCommands.isEmpty()) 578 return fCommands.get(fCommands.size()-1).getRedoModificationStamp(); 579 580 return fRedoModificationStamp; 581 } 582 } 583 584 /** 585 * Internal listener to mouse and key events. 586 */ 587 class KeyAndMouseListener implements MouseListener, KeyListener { 588 589 /* 590 * @see MouseListener#mouseDoubleClick 591 */ 592 @Override mouseDoubleClick(MouseEvent e)593 public void mouseDoubleClick(MouseEvent e) { 594 } 595 596 /* 597 * If the right mouse button is pressed, the current editing command is closed 598 * @see MouseListener#mouseDown 599 */ 600 @Override mouseDown(MouseEvent e)601 public void mouseDown(MouseEvent e) { 602 if (e.button == 1) 603 commit(); 604 } 605 606 /* 607 * @see MouseListener#mouseUp 608 */ 609 @Override mouseUp(MouseEvent e)610 public void mouseUp(MouseEvent e) { 611 } 612 613 /* 614 * @see KeyListener#keyPressed 615 */ 616 @Override keyReleased(KeyEvent e)617 public void keyReleased(KeyEvent e) { 618 } 619 620 /* 621 * On cursor keys, the current editing command is closed 622 * @see KeyListener#keyPressed 623 */ 624 @Override keyPressed(KeyEvent e)625 public void keyPressed(KeyEvent e) { 626 switch (e.keyCode) { 627 case SWT.ARROW_UP: 628 case SWT.ARROW_DOWN: 629 case SWT.ARROW_LEFT: 630 case SWT.ARROW_RIGHT: 631 commit(); 632 break; 633 } 634 } 635 } 636 637 /** 638 * Internal listener to document changes. 639 */ 640 class DocumentListener implements IDocumentListener { 641 642 private String fReplacedText; 643 644 @Override documentAboutToBeChanged(DocumentEvent event)645 public void documentAboutToBeChanged(DocumentEvent event) { 646 try { 647 fReplacedText= event.getDocument().get(event.getOffset(), event.getLength()); 648 fPreservedUndoModificationStamp= event.getModificationStamp(); 649 } catch (BadLocationException x) { 650 fReplacedText= null; 651 } 652 } 653 654 @Override documentChanged(DocumentEvent event)655 public void documentChanged(DocumentEvent event) { 656 fPreservedRedoModificationStamp= event.getModificationStamp(); 657 658 // record the current valid state for the top operation in case it remains the 659 // top operation but changes state. 660 IUndoableOperation op= fHistory.getUndoOperation(fUndoContext); 661 boolean wasValid= false; 662 if (op != null) 663 wasValid= op.canUndo(); 664 // Process the change, providing the before and after timestamps 665 processChange(event.getOffset(), event.getOffset() + event.getLength(), event.getText(), fReplacedText, fPreservedUndoModificationStamp, fPreservedRedoModificationStamp); 666 667 // now update fCurrent with the latest buffers from the document change. 668 fCurrent.pretendCommit(); 669 670 if (op == fCurrent) { 671 // if the document change did not cause a new fCurrent to be created, then we should 672 // notify the history that the current operation changed if its validity has changed. 673 if (wasValid != fCurrent.isValid()) 674 fHistory.operationChanged(op); 675 } 676 else { 677 // if the change created a new fCurrent that we did not yet add to the 678 // stack, do so if it's valid and we are not in the middle of a compound change. 679 if (fCurrent != fLastAddedCommand && fCurrent.isValid()) { 680 addToCommandStack(fCurrent); 681 } 682 } 683 } 684 } 685 686 /** 687 * Internal text input listener. 688 */ 689 class TextInputListener implements ITextInputListener { 690 691 @Override inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput)692 public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { 693 if (oldInput != null && fDocumentListener != null) { 694 oldInput.removeDocumentListener(fDocumentListener); 695 commit(); 696 } 697 } 698 699 @Override inputDocumentChanged(IDocument oldInput, IDocument newInput)700 public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { 701 if (newInput != null) { 702 if (fDocumentListener == null) 703 fDocumentListener= new DocumentListener(); 704 newInput.addDocumentListener(fDocumentListener); 705 } 706 } 707 708 } 709 710 /* 711 * @see IOperationHistoryListener 712 * @since 3.1 713 */ 714 class HistoryListener implements IOperationHistoryListener { 715 private IUndoableOperation fOperation; 716 717 @Override historyNotification(final OperationHistoryEvent event)718 public void historyNotification(final OperationHistoryEvent event) { 719 final int type= event.getEventType(); 720 switch (type) { 721 case OperationHistoryEvent.ABOUT_TO_UNDO: 722 case OperationHistoryEvent.ABOUT_TO_REDO: 723 // if this is one of our operations 724 if (event.getOperation().hasContext(fUndoContext)) { 725 fTextViewer.getTextWidget().getDisplay().syncExec(() -> { 726 // if we are undoing/redoing a command we generated, then ignore 727 // the document changes associated with this undo or redo. 728 if (event.getOperation() instanceof TextCommand) { 729 if (fTextViewer instanceof TextViewer) 730 ((TextViewer) fTextViewer).ignoreAutoEditStrategies(true); 731 listenToTextChanges(false); 732 733 // in the undo case only, make sure compounds are closed 734 if (type == OperationHistoryEvent.ABOUT_TO_UNDO) { 735 if (fFoldingIntoCompoundChange) { 736 endCompoundChange(); 737 } 738 } 739 } else { 740 // the undo or redo has our context, but it is not one of 741 // our commands. We will listen to the changes, but will 742 // reset the state that tracks the undo/redo history. 743 commit(); 744 fLastAddedCommand= null; 745 } 746 }); 747 fOperation= event.getOperation(); 748 } 749 break; 750 case OperationHistoryEvent.UNDONE: 751 case OperationHistoryEvent.REDONE: 752 case OperationHistoryEvent.OPERATION_NOT_OK: 753 if (event.getOperation() == fOperation) { 754 fTextViewer.getTextWidget().getDisplay().syncExec(() -> { 755 listenToTextChanges(true); 756 fOperation= null; 757 if (fTextViewer instanceof TextViewer) 758 ((TextViewer) fTextViewer).ignoreAutoEditStrategies(false); 759 }); 760 } 761 break; 762 } 763 } 764 765 } 766 767 /** Text buffer to collect text which is inserted into the viewer */ 768 private StringBuilder fTextBuffer; 769 /** Text buffer to collect viewer content which has been replaced */ 770 private StringBuilder fPreservedTextBuffer; 771 /** The document modification stamp for undo. */ 772 protected long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 773 /** The document modification stamp for redo. */ 774 protected long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 775 /** The internal key and mouse event listener */ 776 private KeyAndMouseListener fKeyAndMouseListener; 777 /** The internal document listener */ 778 private DocumentListener fDocumentListener; 779 /** The internal text input listener */ 780 private TextInputListener fTextInputListener; 781 782 783 /** Indicates inserting state */ 784 private boolean fInserting= false; 785 /** Indicates overwriting state */ 786 private boolean fOverwriting= false; 787 /** Indicates whether the current change belongs to a compound change */ 788 private boolean fFoldingIntoCompoundChange= false; 789 790 /** The text viewer the undo manager is connected to */ 791 private ITextViewer fTextViewer; 792 793 /** Supported undo level */ 794 private int fUndoLevel; 795 /** The currently constructed edit command */ 796 private TextCommand fCurrent; 797 /** The last delete edit command */ 798 private TextCommand fPreviousDelete; 799 800 /** 801 * The undo context. 802 * @since 3.1 803 */ 804 private IOperationHistory fHistory; 805 /** 806 * The operation history. 807 * @since 3.1 808 */ 809 private IUndoContext fUndoContext; 810 /** 811 * The operation history listener used for managing undo and redo before 812 * and after the individual commands are performed. 813 * @since 3.1 814 */ 815 private IOperationHistoryListener fHistoryListener= new HistoryListener(); 816 817 /** 818 * The command last added to the operation history. This must be tracked 819 * internally instead of asking the history, since outside parties may be placing 820 * items on our undo/redo history. 821 */ 822 private TextCommand fLastAddedCommand= null; 823 824 /** 825 * Creates a new undo manager who remembers the specified number of edit commands. 826 * 827 * @param undoLevel the length of this manager's history 828 */ DefaultUndoManager(int undoLevel)829 public DefaultUndoManager(int undoLevel) { 830 fHistory= OperationHistoryFactory.getOperationHistory(); 831 setMaximalUndoLevel(undoLevel); 832 } 833 834 /** 835 * Returns whether this undo manager is connected to a text viewer. 836 * 837 * @return <code>true</code> if connected, <code>false</code> otherwise 838 * @since 3.1 839 */ isConnected()840 private boolean isConnected() { 841 return fTextViewer != null; 842 } 843 844 /* 845 * @see IUndoManager#beginCompoundChange 846 */ 847 @Override beginCompoundChange()848 public void beginCompoundChange() { 849 if (isConnected()) { 850 fFoldingIntoCompoundChange= true; 851 commit(); 852 } 853 } 854 855 856 /* 857 * @see IUndoManager#endCompoundChange 858 */ 859 @Override endCompoundChange()860 public void endCompoundChange() { 861 if (isConnected()) { 862 fFoldingIntoCompoundChange= false; 863 commit(); 864 } 865 } 866 867 /** 868 * Registers all necessary listeners with the text viewer. 869 */ addListeners()870 private void addListeners() { 871 StyledText text= fTextViewer.getTextWidget(); 872 if (text != null) { 873 fKeyAndMouseListener= new KeyAndMouseListener(); 874 text.addMouseListener(fKeyAndMouseListener); 875 text.addKeyListener(fKeyAndMouseListener); 876 fTextInputListener= new TextInputListener(); 877 fTextViewer.addTextInputListener(fTextInputListener); 878 fHistory.addOperationHistoryListener(fHistoryListener); 879 listenToTextChanges(true); 880 } 881 } 882 883 /** 884 * Unregister all previously installed listeners from the text viewer. 885 */ removeListeners()886 private void removeListeners() { 887 StyledText text= fTextViewer.getTextWidget(); 888 if (text != null) { 889 if (fKeyAndMouseListener != null) { 890 text.removeMouseListener(fKeyAndMouseListener); 891 text.removeKeyListener(fKeyAndMouseListener); 892 fKeyAndMouseListener= null; 893 } 894 if (fTextInputListener != null) { 895 fTextViewer.removeTextInputListener(fTextInputListener); 896 fTextInputListener= null; 897 } 898 listenToTextChanges(false); 899 fHistory.removeOperationHistoryListener(fHistoryListener); 900 } 901 } 902 903 /** 904 * Adds the given command to the operation history if it is not part of 905 * a compound change. 906 * 907 * @param command the command to be added 908 * @since 3.1 909 */ addToCommandStack(TextCommand command)910 private void addToCommandStack(TextCommand command){ 911 if (!fFoldingIntoCompoundChange || command instanceof CompoundTextCommand) { 912 fHistory.add(command); 913 fLastAddedCommand= command; 914 } 915 } 916 917 /** 918 * Disposes the command stack. 919 * 920 * @since 3.1 921 */ disposeCommandStack()922 private void disposeCommandStack() { 923 fHistory.dispose(fUndoContext, true, true, true); 924 } 925 926 /** 927 * Initializes the command stack. 928 * 929 * @since 3.1 930 */ initializeCommandStack()931 private void initializeCommandStack() { 932 if (fHistory != null && fUndoContext != null) 933 fHistory.dispose(fUndoContext, true, true, false); 934 935 } 936 937 /** 938 * Switches the state of whether there is a text listener or not. 939 * 940 * @param listen the state which should be established 941 */ listenToTextChanges(boolean listen)942 private void listenToTextChanges(boolean listen) { 943 if (listen) { 944 if (fDocumentListener == null && fTextViewer.getDocument() != null) { 945 fDocumentListener= new DocumentListener(); 946 fTextViewer.getDocument().addDocumentListener(fDocumentListener); 947 } 948 } else if (!listen) { 949 if (fDocumentListener != null && fTextViewer.getDocument() != null) { 950 fTextViewer.getDocument().removeDocumentListener(fDocumentListener); 951 fDocumentListener= null; 952 } 953 } 954 } 955 956 /** 957 * Closes the current editing command and opens a new one. 958 */ commit()959 private void commit() { 960 // if fCurrent has never been placed on the command stack, do so now. 961 // this can happen when there are multiple programmatically commits in a single 962 // document change. 963 if (fLastAddedCommand != fCurrent) { 964 fCurrent.pretendCommit(); 965 if (fCurrent.isValid()) 966 addToCommandStack(fCurrent); 967 } 968 fCurrent.commit(); 969 } 970 971 /** 972 * Reset processChange state. 973 * 974 * @since 3.2 975 */ resetProcessChangeSate()976 private void resetProcessChangeSate() { 977 fInserting= false; 978 fOverwriting= false; 979 fPreviousDelete.reinitialize(); 980 } 981 982 /** 983 * Checks whether the given text starts with a line delimiter and 984 * subsequently contains a white space only. 985 * 986 * @param text the text to check 987 * @return <code>true</code> if the text is a line delimiter followed by whitespace, <code>false</code> otherwise 988 */ isWhitespaceText(String text)989 private boolean isWhitespaceText(String text) { 990 991 if (text == null || text.isEmpty()) 992 return false; 993 994 String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters(); 995 int index= TextUtilities.startsWith(delimiters, text); 996 if (index > -1) { 997 char c; 998 int length= text.length(); 999 for (int i= delimiters[index].length(); i < length; i++) { 1000 c= text.charAt(i); 1001 if (c != ' ' && c != '\t') 1002 return false; 1003 } 1004 return true; 1005 } 1006 1007 return false; 1008 } 1009 processChange(int modelStart, int modelEnd, String insertedText, String replacedText, long beforeChangeModificationStamp, long afterChangeModificationStamp)1010 private void processChange(int modelStart, int modelEnd, String insertedText, String replacedText, long beforeChangeModificationStamp, long afterChangeModificationStamp) { 1011 1012 if (insertedText == null) 1013 insertedText= ""; //$NON-NLS-1$ 1014 1015 if (replacedText == null) 1016 replacedText= ""; //$NON-NLS-1$ 1017 1018 int length= insertedText.length(); 1019 int diff= modelEnd - modelStart; 1020 1021 if (fCurrent.fUndoModificationStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) 1022 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1023 1024 // normalize 1025 if (diff < 0) { 1026 int tmp= modelEnd; 1027 modelEnd= modelStart; 1028 modelStart= tmp; 1029 } 1030 1031 if (modelStart == modelEnd) { 1032 // text will be inserted 1033 if ((length == 1) || isWhitespaceText(insertedText)) { 1034 // by typing or whitespace 1035 if (!fInserting || (modelStart != fCurrent.fStart + fTextBuffer.length())) { 1036 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; 1037 if (fCurrent.attemptCommit()) 1038 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1039 1040 fInserting= true; 1041 } 1042 if (fCurrent.fStart < 0) 1043 fCurrent.fStart= fCurrent.fEnd= modelStart; 1044 if (length > 0) 1045 fTextBuffer.append(insertedText); 1046 } else if (length >= 0) { 1047 // by pasting or model manipulation 1048 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; 1049 if (fCurrent.attemptCommit()) 1050 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1051 1052 fCurrent.fStart= fCurrent.fEnd= modelStart; 1053 fTextBuffer.append(insertedText); 1054 fCurrent.fRedoModificationStamp= afterChangeModificationStamp; 1055 if (fCurrent.attemptCommit()) 1056 fCurrent.fUndoModificationStamp= afterChangeModificationStamp; 1057 1058 } 1059 } else { 1060 if (length == 0) { 1061 // text will be deleted by backspace or DEL key or empty clipboard 1062 length= replacedText.length(); 1063 String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters(); 1064 1065 if ((length == 1) || TextUtilities.equals(delimiters, replacedText) > -1) { 1066 1067 // whereby selection is empty 1068 1069 if (fPreviousDelete.fStart == modelStart && fPreviousDelete.fEnd == modelEnd) { 1070 // repeated DEL 1071 1072 // correct wrong settings of fCurrent 1073 if (fCurrent.fStart == modelEnd && fCurrent.fEnd == modelStart) { 1074 fCurrent.fStart= modelStart; 1075 fCurrent.fEnd= modelEnd; 1076 } 1077 // append to buffer && extend command range 1078 fPreservedTextBuffer.append(replacedText); 1079 ++fCurrent.fEnd; 1080 1081 } else if (fPreviousDelete.fStart == modelEnd) { 1082 // repeated backspace 1083 1084 // insert in buffer and extend command range 1085 fPreservedTextBuffer.insert(0, replacedText); 1086 fCurrent.fStart= modelStart; 1087 1088 } else { 1089 // either DEL or backspace for the first time 1090 1091 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; 1092 if (fCurrent.attemptCommit()) 1093 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1094 1095 // as we can not decide whether it was DEL or backspace we initialize for backspace 1096 fPreservedTextBuffer.append(replacedText); 1097 fCurrent.fStart= modelStart; 1098 fCurrent.fEnd= modelEnd; 1099 } 1100 1101 fPreviousDelete.set(modelStart, modelEnd); 1102 1103 } else if (length > 0) { 1104 // whereby selection is not empty 1105 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; 1106 if (fCurrent.attemptCommit()) 1107 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1108 1109 fCurrent.fStart= modelStart; 1110 fCurrent.fEnd= modelEnd; 1111 fPreservedTextBuffer.append(replacedText); 1112 } 1113 } else { 1114 // text will be replaced 1115 1116 if (length == 1) { 1117 length= replacedText.length(); 1118 String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters(); 1119 1120 if ((length == 1) || TextUtilities.equals(delimiters, replacedText) > -1) { 1121 // because of overwrite mode or model manipulation 1122 if (!fOverwriting || (modelStart != fCurrent.fStart + fTextBuffer.length())) { 1123 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; 1124 if (fCurrent.attemptCommit()) 1125 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1126 1127 fOverwriting= true; 1128 } 1129 1130 if (fCurrent.fStart < 0) 1131 fCurrent.fStart= modelStart; 1132 1133 fCurrent.fEnd= modelEnd; 1134 fTextBuffer.append(insertedText); 1135 fPreservedTextBuffer.append(replacedText); 1136 fCurrent.fRedoModificationStamp= afterChangeModificationStamp; 1137 return; 1138 } 1139 } 1140 // because of typing or pasting whereby selection is not empty 1141 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp; 1142 if (fCurrent.attemptCommit()) 1143 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp; 1144 1145 fCurrent.fStart= modelStart; 1146 fCurrent.fEnd= modelEnd; 1147 fTextBuffer.append(insertedText); 1148 fPreservedTextBuffer.append(replacedText); 1149 } 1150 } 1151 // in all cases, the redo modification stamp is updated on the open command 1152 fCurrent.fRedoModificationStamp= afterChangeModificationStamp; 1153 } 1154 1155 /** 1156 * Shows the given exception in an error dialog. 1157 * 1158 * @param title the dialog title 1159 * @param ex the exception 1160 * @since 3.1 1161 */ openErrorDialog(final String title, final Exception ex)1162 private void openErrorDialog(final String title, final Exception ex) { 1163 Shell shell= null; 1164 if (isConnected()) { 1165 StyledText st= fTextViewer.getTextWidget(); 1166 if (st != null && !st.isDisposed()) 1167 shell= st.getShell(); 1168 } 1169 if (Display.getCurrent() != null) 1170 MessageDialog.openError(shell, title, ex.getLocalizedMessage()); 1171 else { 1172 Display display; 1173 final Shell finalShell= shell; 1174 if (finalShell != null) 1175 display= finalShell.getDisplay(); 1176 else 1177 display= Display.getDefault(); 1178 display.syncExec(() -> MessageDialog.openError(finalShell, title, ex.getLocalizedMessage())); 1179 } 1180 } 1181 1182 @Override setMaximalUndoLevel(int undoLevel)1183 public void setMaximalUndoLevel(int undoLevel) { 1184 fUndoLevel= Math.max(0, undoLevel); 1185 if (isConnected()) { 1186 fHistory.setLimit(fUndoContext, fUndoLevel); 1187 } 1188 } 1189 1190 @Override connect(ITextViewer textViewer)1191 public void connect(ITextViewer textViewer) { 1192 if (!isConnected() && textViewer != null) { 1193 fTextViewer= textViewer; 1194 fTextBuffer= new StringBuilder(); 1195 fPreservedTextBuffer= new StringBuilder(); 1196 if (fUndoContext == null) 1197 fUndoContext= new ObjectUndoContext(this); 1198 1199 fHistory.setLimit(fUndoContext, fUndoLevel); 1200 1201 initializeCommandStack(); 1202 1203 // open up the current command 1204 fCurrent= new TextCommand(fUndoContext); 1205 1206 fPreviousDelete= new TextCommand(fUndoContext); 1207 addListeners(); 1208 } 1209 } 1210 1211 @Override disconnect()1212 public void disconnect() { 1213 if (isConnected()) { 1214 1215 removeListeners(); 1216 1217 fCurrent= null; 1218 fTextViewer= null; 1219 disposeCommandStack(); 1220 fTextBuffer= null; 1221 fPreservedTextBuffer= null; 1222 fUndoContext= null; 1223 } 1224 } 1225 1226 @Override reset()1227 public void reset() { 1228 if (isConnected()) { 1229 initializeCommandStack(); 1230 fCurrent= new TextCommand(fUndoContext); 1231 fFoldingIntoCompoundChange= false; 1232 fInserting= false; 1233 fOverwriting= false; 1234 fTextBuffer.setLength(0); 1235 fPreservedTextBuffer.setLength(0); 1236 fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 1237 fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP; 1238 } 1239 } 1240 1241 @Override redoable()1242 public boolean redoable() { 1243 return fHistory.canRedo(fUndoContext); 1244 } 1245 1246 @Override undoable()1247 public boolean undoable() { 1248 return fHistory.canUndo(fUndoContext); 1249 } 1250 1251 @Override redo()1252 public void redo() { 1253 if (isConnected() && redoable()) { 1254 try { 1255 fHistory.redo(fUndoContext, null, null); 1256 } catch (ExecutionException ex) { 1257 openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.redoFailed.title"), ex); //$NON-NLS-1$ 1258 } 1259 } 1260 } 1261 1262 @Override undo()1263 public void undo() { 1264 if (isConnected() && undoable()) { 1265 try { 1266 fHistory.undo(fUndoContext, null, null); 1267 } catch (ExecutionException ex) { 1268 openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.undoFailed.title"), ex); //$NON-NLS-1$ 1269 } 1270 } 1271 } 1272 1273 /** 1274 * Selects and reveals the specified range. 1275 * 1276 * @param offset the offset of the range 1277 * @param length the length of the range 1278 * @since 3.0 1279 */ selectAndReveal(int offset, int length)1280 protected void selectAndReveal(int offset, int length) { 1281 if (fTextViewer instanceof ITextViewerExtension5) { 1282 ITextViewerExtension5 extension= (ITextViewerExtension5) fTextViewer; 1283 extension.exposeModelRange(new Region(offset, length)); 1284 } else if (!fTextViewer.overlapsWithVisibleRegion(offset, length)) 1285 fTextViewer.resetVisibleRegion(); 1286 1287 fTextViewer.setSelectedRange(offset, length); 1288 fTextViewer.revealRange(offset, length); 1289 } 1290 1291 @Override getUndoContext()1292 public IUndoContext getUndoContext() { 1293 return fUndoContext; 1294 } 1295 1296 } 1297