fix #774 infinite loop in net.sourceforge.phpeclipse.builder.IdentifierIndexManager...
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / TypingRunDetector.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2003 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  *
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text;
12
13 import java.util.ArrayList;
14 import java.util.HashSet;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Set;
18
19 import net.sourceforge.phpdt.internal.ui.text.TypingRun.ChangeType;
20
21 import org.eclipse.jface.text.Assert;
22 import org.eclipse.jface.text.DocumentEvent;
23 import org.eclipse.jface.text.ITextListener;
24 import org.eclipse.jface.text.ITextViewer;
25 import org.eclipse.jface.text.TextEvent;
26 import org.eclipse.swt.SWT;
27 import org.eclipse.swt.custom.StyledText;
28 import org.eclipse.swt.events.FocusEvent;
29 import org.eclipse.swt.events.FocusListener;
30 import org.eclipse.swt.events.KeyEvent;
31 import org.eclipse.swt.events.KeyListener;
32 import org.eclipse.swt.events.MouseEvent;
33 import org.eclipse.swt.events.MouseListener;
34
35 /**
36  * When connected to a text viewer, a <code>TypingRunDetector</code> observes
37  * <code>TypingRun</code> events. A typing run is a sequence of similar text
38  * modifications, such as inserting or deleting single characters.
39  * <p>
40  * Listeners are informed about the start and end of a <code>TypingRun</code>.
41  * </p>
42  * 
43  * @since 3.0
44  */
45 public class TypingRunDetector {
46         /*
47          * Implementation note: This class is independent of JDT and may be pulled
48          * up to jface.text if needed.
49          */
50
51         /** Debug flag. */
52         private static final boolean DEBUG = false;
53
54         /**
55          * Instances of this class abstract a text modification into a simple
56          * description. Typing runs consists of a sequence of one or more modifying
57          * changes of the same type. Every change records the type of change
58          * described by a text modification, and an offset it can be followed by
59          * another change of the same run.
60          */
61         private static final class Change {
62                 private ChangeType fType;
63
64                 private int fNextOffset;
65
66                 /**
67                  * Creates a new change of type <code>type</code>.
68                  * 
69                  * @param type
70                  *            the <code>ChangeType</code> of the new change
71                  * @param nextOffset
72                  *            the offset of the next change in a typing run
73                  */
74                 public Change(ChangeType type, int nextOffset) {
75                         fType = type;
76                         fNextOffset = nextOffset;
77                 }
78
79                 /**
80                  * Returns <code>true</code> if the receiver can extend the typing
81                  * range the last change of which is described by <code>change</code>.
82                  * 
83                  * @param change
84                  *            the last change in a typing run
85                  * @return <code>true</code> if the receiver is a valid extension to
86                  *         <code>change</code>,<code>false</code> otherwise
87                  */
88                 public boolean canFollow(Change change) {
89                         if (fType == TypingRun.NO_CHANGE)
90                                 return true;
91                         else if (fType.equals(TypingRun.UNKNOWN))
92                                 return false;
93                         if (fType.equals(change.fType)) {
94                                 if (fType == TypingRun.DELETE)
95                                         return fNextOffset == change.fNextOffset - 1;
96                                 else if (fType == TypingRun.INSERT)
97                                         return fNextOffset == change.fNextOffset + 1;
98                                 else if (fType == TypingRun.OVERTYPE)
99                                         return fNextOffset == change.fNextOffset + 1;
100                                 else if (fType == TypingRun.SELECTION)
101                                         return true;
102                         }
103                         return false;
104                 }
105
106                 /**
107                  * Returns <code>true</code> if the receiver describes a text
108                  * modification, <code>false</code> if it describes a focus /
109                  * selection change.
110                  * 
111                  * @return <code>true</code> if the receiver is a text modification
112                  */
113                 public boolean isModification() {
114                         return fType.isModification();
115                 }
116
117                 /*
118                  * @see java.lang.Object#toString()
119                  */
120                 public String toString() {
121                         return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
122                 }
123
124                 /**
125                  * Returns the change type of this change.
126                  * 
127                  * @return the change type of this change
128                  */
129                 public ChangeType getType() {
130                         return fType;
131                 }
132         }
133
134         /**
135          * Observes any events that modify the content of the document displayed in
136          * the editor. Since text events may start a new run, this listener is
137          * always registered if the detector is connected.
138          */
139         private class TextListener implements ITextListener {
140
141                 /*
142                  * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
143                  */
144                 public void textChanged(TextEvent event) {
145                         handleTextChanged(event);
146                 }
147         }
148
149         /**
150          * Observes non-modifying events that will end a run, such as clicking into
151          * the editor, moving the caret, and the editor losing focus. These events
152          * can never start a run, therefore this listener is only registered if
153          * there is an ongoing run.
154          */
155         private class SelectionListener implements MouseListener, KeyListener,
156                         FocusListener {
157
158                 /*
159                  * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
160                  */
161                 public void focusGained(FocusEvent e) {
162                         handleSelectionChanged();
163                 }
164
165                 /*
166                  * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
167                  */
168                 public void focusLost(FocusEvent e) {
169                 }
170
171                 /*
172                  * @see MouseListener#mouseDoubleClick
173                  */
174                 public void mouseDoubleClick(MouseEvent e) {
175                 }
176
177                 /*
178                  * If the right mouse button is pressed, the current editing command is
179                  * closed
180                  * 
181                  * @see MouseListener#mouseDown
182                  */
183                 public void mouseDown(MouseEvent e) {
184                         if (e.button == 1)
185                                 handleSelectionChanged();
186                 }
187
188                 /*
189                  * @see MouseListener#mouseUp
190                  */
191                 public void mouseUp(MouseEvent e) {
192                 }
193
194                 /*
195                  * @see KeyListener#keyPressed
196                  */
197                 public void keyReleased(KeyEvent e) {
198                 }
199
200                 /*
201                  * On cursor keys, the current editing command is closed
202                  * 
203                  * @see KeyListener#keyPressed
204                  */
205                 public void keyPressed(KeyEvent e) {
206                         switch (e.keyCode) {
207                         case SWT.ARROW_UP:
208                         case SWT.ARROW_DOWN:
209                         case SWT.ARROW_LEFT:
210                         case SWT.ARROW_RIGHT:
211                         case SWT.END:
212                         case SWT.HOME:
213                         case SWT.PAGE_DOWN:
214                         case SWT.PAGE_UP:
215                                 handleSelectionChanged();
216                                 break;
217                         }
218                 }
219         }
220
221         /** The listeners. */
222         private final Set fListeners = new HashSet();
223
224         /**
225          * The viewer we work upon. Set to <code>null</code> in
226          * <code>uninstall</code>.
227          */
228         private ITextViewer fViewer;
229
230         /** The text event listener. */
231         private final TextListener fTextListener = new TextListener();
232
233         /**
234          * The selection listener. Set to <code>null</code> when no run is active.
235          */
236         private SelectionListener fSelectionListener;
237
238         /* state variables */
239
240         /** The most recently observed change. Never <code>null</code>. */
241         private Change fLastChange;
242
243         /** The current run, or <code>null</code> if there is none. */
244         private TypingRun fRun;
245
246         /**
247          * Installs the receiver with a text viewer.
248          * 
249          * @param viewer
250          *            the viewer to install on
251          */
252         public void install(ITextViewer viewer) {
253                 Assert.isLegal(viewer != null);
254                 fViewer = viewer;
255                 connect();
256         }
257
258         /**
259          * Initializes the state variables and registers any permanent listeners.
260          */
261         private void connect() {
262                 if (fViewer != null) {
263                         fLastChange = new Change(TypingRun.UNKNOWN, -1);
264                         fRun = null;
265                         fSelectionListener = null;
266                         fViewer.addTextListener(fTextListener);
267                 }
268         }
269
270         /**
271          * Uninstalls the receiver and removes all listeners. <code>install()</code>
272          * must be called for events to be generated.
273          */
274         public void uninstall() {
275                 if (fViewer != null) {
276                         fListeners.clear();
277                         disconnect();
278                         fViewer = null;
279                 }
280         }
281
282         /**
283          * Disconnects any registered listeners.
284          */
285         private void disconnect() {
286                 fViewer.removeTextListener(fTextListener);
287                 ensureSelectionListenerRemoved();
288         }
289
290         /**
291          * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
292          * the same listener instance has no effect. Listeners may be added even if
293          * the receiver is neither connected nor installed.
294          * 
295          * @param listener
296          *            the listener add
297          */
298         public void addTypingRunListener(ITypingRunListener listener) {
299                 Assert.isLegal(listener != null);
300                 fListeners.add(listener);
301                 if (fListeners.size() == 1)
302                         connect();
303         }
304
305         /**
306          * Removes the listener from this manager. If <code>listener</code> is not
307          * registered with the receiver, nothing happens.
308          * 
309          * @param listener
310          *            the listener to remove, or <code>null</code>
311          */
312         public void removeTypingRunListener(ITypingRunListener listener) {
313                 fListeners.remove(listener);
314                 if (fListeners.size() == 0)
315                         disconnect();
316         }
317
318         /**
319          * Handles an incoming text event.
320          * 
321          * @param event
322          *            the text event that describes the text modification
323          */
324         void handleTextChanged(TextEvent event) {
325                 Change type = computeChange(event);
326                 handleChange(type);
327         }
328
329         /**
330          * Computes the change abstraction given a text event.
331          * 
332          * @param event
333          *            the text event to analyze
334          * @return a change object describing the event
335          */
336         private Change computeChange(TextEvent event) {
337                 DocumentEvent e = event.getDocumentEvent();
338                 if (e == null)
339                         return new Change(TypingRun.NO_CHANGE, -1);
340
341                 int start = e.getOffset();
342                 int end = e.getOffset() + e.getLength();
343                 String newText = e.getText();
344                 if (newText == null)
345                         newText = new String();
346
347                 if (start == end) {
348                         // no replace / delete / overwrite
349                         if (newText.length() == 1)
350                                 return new Change(TypingRun.INSERT, end + 1);
351                 } else if (start == end - 1) {
352                         if (newText.length() == 1)
353                                 return new Change(TypingRun.OVERTYPE, end);
354                         if (newText.length() == 0)
355                                 return new Change(TypingRun.DELETE, start);
356                 }
357
358                 return new Change(TypingRun.UNKNOWN, -1);
359         }
360
361         /**
362          * Handles an incoming selection event.
363          */
364         void handleSelectionChanged() {
365                 handleChange(new Change(TypingRun.SELECTION, -1));
366         }
367
368         /**
369          * State machine. Changes state given the current state and the incoming
370          * change.
371          * 
372          * @param change
373          *            the incoming change
374          */
375         private void handleChange(Change change) {
376                 if (change.getType() == TypingRun.NO_CHANGE)
377                         return;
378
379                 if (DEBUG)
380                         System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
381
382                 if (!change.canFollow(fLastChange))
383                         endIfStarted(change);
384                 fLastChange = change;
385                 if (change.isModification())
386                         startOrContinue();
387
388                 if (DEBUG)
389                         System.err.println("New change: " + change); //$NON-NLS-1$
390         }
391
392         /**
393          * Starts a new run if there is none and informs all listeners. If there
394          * already is a run, nothing happens.
395          */
396         private void startOrContinue() {
397                 if (!hasRun()) {
398                         if (DEBUG)
399                                 System.err.println("+Start run"); //$NON-NLS-1$
400                         fRun = new TypingRun(fLastChange.getType());
401                         ensureSelectionListenerAdded();
402                         fireRunBegun(fRun);
403                 }
404         }
405
406         /**
407          * Returns <code>true</code> if there is an active run, <code>false</code>
408          * otherwise.
409          * 
410          * @return <code>true</code> if there is an active run, <code>false</code>
411          *         otherwise
412          */
413         private boolean hasRun() {
414                 return fRun != null;
415         }
416
417         /**
418          * Ends any active run and informs all listeners. If there is none, nothing
419          * happens.
420          * 
421          * @param change
422          *            the change that triggered ending the active run
423          */
424         private void endIfStarted(Change change) {
425                 if (hasRun()) {
426                         ensureSelectionListenerRemoved();
427                         if (DEBUG)
428                                 System.err.println("-End run"); //$NON-NLS-1$
429                         fireRunEnded(fRun, change.getType());
430                         fRun = null;
431                 }
432         }
433
434         /**
435          * Adds the selection listener to the text widget underlying the viewer, if
436          * not already done.
437          */
438         private void ensureSelectionListenerAdded() {
439                 if (fSelectionListener == null) {
440                         fSelectionListener = new SelectionListener();
441                         StyledText textWidget = fViewer.getTextWidget();
442                         textWidget.addFocusListener(fSelectionListener);
443                         textWidget.addKeyListener(fSelectionListener);
444                         textWidget.addMouseListener(fSelectionListener);
445                 }
446         }
447
448         /**
449          * If there is a selection listener, it is removed from the text widget
450          * underlying the viewer.
451          */
452         private void ensureSelectionListenerRemoved() {
453                 if (fSelectionListener != null) {
454                         StyledText textWidget = fViewer.getTextWidget();
455                         textWidget.removeFocusListener(fSelectionListener);
456                         textWidget.removeKeyListener(fSelectionListener);
457                         textWidget.removeMouseListener(fSelectionListener);
458                         fSelectionListener = null;
459                 }
460         }
461
462         /**
463          * Informs all listeners about a newly started <code>TypingRun</code>.
464          * 
465          * @param run
466          *            the new run
467          */
468         private void fireRunBegun(TypingRun run) {
469                 List listeners = new ArrayList(fListeners);
470                 for (Iterator it = listeners.iterator(); it.hasNext();) {
471                         ITypingRunListener listener = (ITypingRunListener) it.next();
472                         listener.typingRunStarted(fRun);
473                 }
474         }
475
476         /**
477          * Informs all listeners about an ended <code>TypingRun</code>.
478          * 
479          * @param run
480          *            the previously active run
481          * @param reason
482          *            the type of change that caused the run to be ended
483          */
484         private void fireRunEnded(TypingRun run, ChangeType reason) {
485                 List listeners = new ArrayList(fListeners);
486                 for (Iterator it = listeners.iterator(); it.hasNext();) {
487                         ITypingRunListener listener = (ITypingRunListener) it.next();
488                         listener.typingRunEnded(fRun, reason);
489                 }
490         }
491 }