fix #774 infinite loop in net.sourceforge.phpeclipse.builder.IdentifierIndexManager...
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpeclipse / phpeditor / ToggleCommentAction.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2004 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.phpeclipse.phpeditor;
12
13 import java.util.HashMap;
14 import java.util.Map;
15 import java.util.ResourceBundle;
16
17 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
18
19 import org.eclipse.jface.dialogs.MessageDialog;
20 import org.eclipse.jface.text.BadLocationException;
21 import org.eclipse.jface.text.IDocument;
22 import org.eclipse.jface.text.IRegion;
23 import org.eclipse.jface.text.ITextOperationTarget;
24 import org.eclipse.jface.text.ITextSelection;
25 import org.eclipse.jface.text.ITypedRegion;
26 import org.eclipse.jface.text.Region;
27 import org.eclipse.jface.text.TextUtilities;
28 import org.eclipse.jface.text.source.ISourceViewer;
29 import org.eclipse.jface.text.source.SourceViewerConfiguration;
30 import org.eclipse.jface.viewers.ISelection;
31 import org.eclipse.swt.custom.BusyIndicator;
32 import org.eclipse.swt.widgets.Display;
33 import org.eclipse.swt.widgets.Shell;
34 import org.eclipse.ui.texteditor.ITextEditor;
35 import org.eclipse.ui.texteditor.ResourceAction;
36 import org.eclipse.ui.texteditor.TextEditorAction;
37
38 /**
39  * An action which toggles comment prefixes on the selected lines.
40  * 
41  * @since 3.0
42  */
43 public final class ToggleCommentAction extends TextEditorAction {
44
45         /** The text operation target */
46         private ITextOperationTarget fOperationTarget;
47
48         /** The document partitioning */
49         private String fDocumentPartitioning;
50
51         /** The comment prefixes */
52         private Map fPrefixesMap;
53
54         /**
55          * Creates and initializes the action for the given text editor. The action
56          * configures its visual representation from the given resource bundle.
57          * 
58          * @param bundle
59          *            the resource bundle
60          * @param prefix
61          *            a prefix to be prepended to the various resource keys
62          *            (described in <code>ResourceAction</code> constructor), or
63          *            <code>null</code> if none
64          * @param editor
65          *            the text editor
66          * @see ResourceAction#ResourceAction(ResourceBundle, String, int)
67          */
68         public ToggleCommentAction(ResourceBundle bundle, String prefix,
69                         ITextEditor editor) {
70                 super(bundle, prefix, editor);
71         }
72
73         /**
74          * Implementation of the <code>IAction</code> prototype. Checks if the
75          * selected lines are all commented or not and uncomments/comments them
76          * respectively.
77          */
78         public void run() {
79                 if (fOperationTarget == null || fDocumentPartitioning == null
80                                 || fPrefixesMap == null)
81                         return;
82
83                 ITextEditor editor = getTextEditor();
84                 if (editor == null)
85                         return;
86
87                 if (!validateEditorInputState())
88                         return;
89
90                 final int operationCode;
91                 if (isSelectionCommented(editor.getSelectionProvider().getSelection()))
92                         operationCode = ITextOperationTarget.STRIP_PREFIX;
93                 else
94                         operationCode = ITextOperationTarget.PREFIX;
95
96                 Shell shell = editor.getSite().getShell();
97                 if (!fOperationTarget.canDoOperation(operationCode)) {
98                         if (shell != null)
99                                 MessageDialog
100                                                 .openError(
101                                                                 shell,
102                                                                 PHPEditorMessages
103                                                                                 .getString("ToggleComment.error.title"), PHPEditorMessages.getString("ToggleComment.error.message")); //$NON-NLS-1$ //$NON-NLS-2$
104                         return;
105                 }
106
107                 Display display = null;
108                 if (shell != null && !shell.isDisposed())
109                         display = shell.getDisplay();
110
111                 BusyIndicator.showWhile(display, new Runnable() {
112                         public void run() {
113                                 fOperationTarget.doOperation(operationCode);
114                         }
115                 });
116         }
117
118         /**
119          * Is the given selection single-line commented?
120          * 
121          * @param selection
122          *            Selection to check
123          * @return <code>true</code> iff all selected lines are commented
124          */
125         private boolean isSelectionCommented(ISelection selection) {
126                 if (!(selection instanceof ITextSelection))
127                         return false;
128
129                 ITextSelection textSelection = (ITextSelection) selection;
130                 if (textSelection.getStartLine() < 0 || textSelection.getEndLine() < 0)
131                         return false;
132
133                 IDocument document = getTextEditor().getDocumentProvider().getDocument(
134                                 getTextEditor().getEditorInput());
135
136                 try {
137
138                         IRegion block = getTextBlockFromSelection(textSelection, document);
139                         ITypedRegion[] regions = TextUtilities.computePartitioning(
140                                         document, fDocumentPartitioning, block.getOffset(), block
141                                                         .getLength(), false);
142
143                         int lineCount = 0;
144                         int[] lines = new int[regions.length * 2]; // [startline, endline,
145                                                                                                                 // startline, endline,
146                                                                                                                 // ...]
147                         for (int i = 0, j = 0; i < regions.length; i++, j += 2) {
148                                 // start line of region
149                                 lines[j] = getFirstCompleteLineOfRegion(regions[i], document);
150                                 // end line of region
151                                 int length = regions[i].getLength();
152                                 int offset = regions[i].getOffset() + length;
153                                 if (length > 0)
154                                         offset--;
155                                 lines[j + 1] = (lines[j] == -1 ? -1 : document
156                                                 .getLineOfOffset(offset));
157                                 lineCount += lines[j + 1] - lines[j] + 1;
158                         }
159
160                         // Perform the check
161                         for (int i = 0, j = 0; i < regions.length; i++, j += 2) {
162                                 String[] prefixes = (String[]) fPrefixesMap.get(regions[i]
163                                                 .getType());
164                                 if (prefixes != null && prefixes.length > 0 && lines[j] >= 0
165                                                 && lines[j + 1] >= 0)
166                                         if (!isBlockCommented(lines[j], lines[j + 1], prefixes,
167                                                         document))
168                                                 return false;
169                         }
170
171                         return true;
172
173                 } catch (BadLocationException x) {
174                         // should not happen
175                         PHPeclipsePlugin.log(x);
176                 }
177
178                 return false;
179         }
180
181         /**
182          * Creates a region describing the text block (something that starts at the
183          * beginning of a line) completely containing the current selection.
184          * 
185          * @param selection
186          *            The selection to use
187          * @param document
188          *            The document
189          * @return the region describing the text block comprising the given
190          *         selection
191          */
192         private IRegion getTextBlockFromSelection(ITextSelection selection,
193                         IDocument document) {
194
195                 try {
196                         IRegion line = document.getLineInformationOfOffset(selection
197                                         .getOffset());
198                         int length = selection.getLength() == 0 ? line.getLength()
199                                         : selection.getLength()
200                                                         + (selection.getOffset() - line.getOffset());
201                         return new Region(line.getOffset(), length);
202
203                 } catch (BadLocationException x) {
204                         // should not happen
205                         PHPeclipsePlugin.log(x);
206                 }
207
208                 return null;
209         }
210
211         /**
212          * Returns the index of the first line whose start offset is in the given
213          * text range.
214          * 
215          * @param region
216          *            the text range in characters where to find the line
217          * @param document
218          *            The document
219          * @return the first line whose start index is in the given range, -1 if
220          *         there is no such line
221          */
222         private int getFirstCompleteLineOfRegion(IRegion region, IDocument document) {
223
224                 try {
225
226                         int startLine = document.getLineOfOffset(region.getOffset());
227
228                         int offset = document.getLineOffset(startLine);
229                         if (offset >= region.getOffset())
230                                 return startLine;
231
232                         offset = document.getLineOffset(startLine + 1);
233                         return (offset > region.getOffset() + region.getLength() ? -1
234                                         : startLine + 1);
235
236                 } catch (BadLocationException x) {
237                         // should not happen
238                         PHPeclipsePlugin.log(x);
239                 }
240
241                 return -1;
242         }
243
244         /**
245          * Determines whether each line is prefixed by one of the prefixes.
246          * 
247          * @param startLine
248          *            Start line in document
249          * @param endLine
250          *            End line in document
251          * @param prefixes
252          *            Possible comment prefixes
253          * @param document
254          *            The document
255          * @return <code>true</code> iff each line from <code>startLine</code>
256          *         to and including <code>endLine</code> is prepended by one of
257          *         the <code>prefixes</code>, ignoring whitespace at the begin of
258          *         line
259          */
260         private boolean isBlockCommented(int startLine, int endLine,
261                         String[] prefixes, IDocument document) {
262
263                 try {
264
265                         // check for occurrences of prefixes in the given lines
266                         for (int i = startLine; i <= endLine; i++) {
267
268                                 IRegion line = document.getLineInformation(i);
269                                 String text = document.get(line.getOffset(), line.getLength());
270
271                                 int[] found = TextUtilities.indexOf(prefixes, text, 0);
272
273                                 if (found[0] == -1)
274                                         // found a line which is not commented
275                                         return false;
276
277                                 String s = document.get(line.getOffset(), found[0]);
278                                 s = s.trim();
279                                 if (s.length() != 0)
280                                         // found a line which is not commented
281                                         return false;
282
283                         }
284
285                         return true;
286
287                 } catch (BadLocationException x) {
288                         // should not happen
289                         PHPeclipsePlugin.log(x);
290                 }
291
292                 return false;
293         }
294
295         /**
296          * Implementation of the <code>IUpdate</code> prototype method discovers
297          * the operation through the current editor's
298          * <code>ITextOperationTarget</code> adapter, and sets the enabled state
299          * accordingly.
300          */
301         public void update() {
302                 super.update();
303
304                 if (!canModifyEditor()) {
305                         setEnabled(false);
306                         return;
307                 }
308
309                 ITextEditor editor = getTextEditor();
310                 if (fOperationTarget == null && editor != null)
311                         fOperationTarget = (ITextOperationTarget) editor
312                                         .getAdapter(ITextOperationTarget.class);
313
314                 boolean isEnabled = (fOperationTarget != null
315                                 && fOperationTarget.canDoOperation(ITextOperationTarget.PREFIX) && fOperationTarget
316                                 .canDoOperation(ITextOperationTarget.STRIP_PREFIX));
317                 setEnabled(isEnabled);
318         }
319
320         /*
321          * @see TextEditorAction#setEditor(ITextEditor)
322          */
323         public void setEditor(ITextEditor editor) {
324                 super.setEditor(editor);
325                 fOperationTarget = null;
326         }
327
328         public void configure(ISourceViewer sourceViewer,
329                         SourceViewerConfiguration configuration) {
330                 fPrefixesMap = null;
331
332                 String[] types = configuration.getConfiguredContentTypes(sourceViewer);
333                 Map prefixesMap = new HashMap(types.length);
334                 for (int i = 0; i < types.length; i++) {
335                         String type = types[i];
336                         String[] prefixes = configuration.getDefaultPrefixes(sourceViewer,
337                                         type);
338                         if (prefixes != null && prefixes.length > 0) {
339                                 int emptyPrefixes = 0;
340                                 for (int j = 0; j < prefixes.length; j++)
341                                         if (prefixes[j].length() == 0)
342                                                 emptyPrefixes++;
343
344                                 if (emptyPrefixes > 0) {
345                                         String[] nonemptyPrefixes = new String[prefixes.length
346                                                         - emptyPrefixes];
347                                         for (int j = 0, k = 0; j < prefixes.length; j++) {
348                                                 String prefix = prefixes[j];
349                                                 if (prefix.length() != 0) {
350                                                         nonemptyPrefixes[k] = prefix;
351                                                         k++;
352                                                 }
353                                         }
354                                         prefixes = nonemptyPrefixes;
355                                 }
356
357                                 prefixesMap.put(type, prefixes);
358                         }
359                 }
360                 fDocumentPartitioning = configuration
361                                 .getConfiguredDocumentPartitioning(sourceViewer);
362                 fPrefixesMap = prefixesMap;
363         }
364 }