comparison dwtx/text/undo/DocumentUndoManager.d @ 129:eb30df5ca28b

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