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