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