/bundles/plugins-trunk/TextTools/TextToolsComments.java
Java | 547 lines | 368 code | 39 blank | 140 comment | 63 complexity | 3c0ee84de00a1b0ad18fa9c7bcb69b70 MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, Apache-2.0, LGPL-2.0, LGPL-3.0, GPL-2.0, CC-BY-SA-3.0, LGPL-2.1, GPL-3.0, MPL-2.0-no-copyleft-exception, IPL-1.0
1/*
2 * TextToolsComments.java - Actions for toggling range and line comments
3 * :tabSize=8:indentSize=8:noTabs=false:
4 * :folding=explicit:collapseFolds=1:
5 *
6 * Copyright (C) 2003 Robert Fletcher
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21 */
22
23//{{{ Imports
24import java.io.PrintWriter;
25import java.io.StringWriter;
26import java.util.Iterator;
27import javax.swing.text.Segment;
28import org.gjt.sp.jedit.MiscUtilities;
29import org.gjt.sp.jedit.View;
30import org.gjt.sp.jedit.jEdit;
31import org.gjt.sp.jedit.buffer.JEditBuffer;
32import org.gjt.sp.jedit.textarea.JEditTextArea;
33import org.gjt.sp.jedit.textarea.Selection;
34import org.gjt.sp.util.Log;
35import org.gjt.sp.util.StandardUtilities;
36//}}}
37
38/**
39 * Actions for toggling range and line comments.
40 *
41 * @author <a href="mailto:rfletch6@yahoo.co.uk">Robert Fletcher</a>
42 * @version $Revision: 20902 $ $Date: 2012-01-21 09:51:18 +0100 (Sat, 21 Jan 2012) $
43 */
44public class TextToolsComments
45{
46 //{{{ toggleLineComments() method
47 /**
48 * Toggles a line comment on or off at all currently selected lines or, if
49 * there are none, at the line where the caret currently sits.
50 *
51 * @param view a jEdit view.
52 */
53 public static void toggleLineComments(View view)
54 {
55 JEditTextArea textArea = view.getTextArea();
56 JEditBuffer buffer = textArea.getBuffer();
57 try
58 {
59 if(!buffer.isEditable())
60 {
61 view.getToolkit().beep();
62 return;
63 }
64
65 int magicCaretPos = textArea.getMagicCaretPosition();
66
67 // get an array of all selected lines, or if there are no selections,
68 // just the line of the caret
69 Selection[] selections = textArea.getSelection();
70
71 int selectionNo;
72 if (selections.length < 1)
73 {
74 selectionNo = 1;
75 }
76 else
77 {
78 selectionNo = selections.length;
79 }
80
81 int[] lines;
82
83 for (int j=0; j < selectionNo; j++)
84 {
85
86 //Get the lines for this selection.
87 if (selections.length < 1)
88 {
89 lines = new int[]{textArea.getCaretLine()};
90 }
91 else
92 {
93 lines = new int[selections[j].getEndLine() - selections[j].getStartLine() + 1];
94 for (int i=0; i < lines.length; i++) {
95 lines[i] = i + selections[j].getStartLine();
96 }
97 }
98
99 // If we're inserting as block find the leftmost indent
100 int leftmost = Integer.MAX_VALUE;
101 String line;
102 if(jEdit.getBooleanProperty("options.toggle-comments.indentAsBlock"))
103 {
104 for(int i = 0; i < lines.length; i++)
105 {
106 line = buffer.getLineText(lines[i]);
107 if(line.trim().length() < 1)
108 {
109 continue;
110 }
111 leftmost = Math.min(leftmost, StandardUtilities.getLeadingWhiteSpaceWidth(line, buffer.getTabSize()));
112 }
113 }
114
115 // if block commenting we need to check for un-commented lines, if
116 // any are found NO lines will be uncommented
117 boolean doUncomment = true;
118 if(jEdit.getBooleanProperty("options.toggle-comments.commentAsBlock"))
119 {
120 for(int i = 0; doUncomment && i < lines.length; i++)
121 {
122 line = buffer.getLineText(lines[i]).trim();
123 String lineComment = buffer.getContextSensitiveProperty(buffer.getLineStartOffset(lines[i]), "lineComment");
124 if(lineComment == null || lineComment.length() == 0)
125 {
126 continue;
127 }
128 if(line.length() > 0 && !line.startsWith(lineComment))
129 {
130 doUncomment = false;
131 }
132 }
133 }
134
135 // loop through each line
136 boolean noCommentableLines = true;
137 String lineComment = null;
138 for(int i = 0; i < lines.length; i++)
139 {
140 Log.log(Log.DEBUG, TextToolsComments.class, "looping line: "+lines[i]);
141 line = buffer.getLineText(lines[i]);
142
143 int lineStart = buffer.getLineStartOffset(lines[i]);
144 // get position after any leading whitespace
145 int pos = lineStart + StandardUtilities.getLeadingWhiteSpace(line);
146 if (i == 0)
147 {
148 //first time through get the line comment.
149 lineComment = buffer.getContextSensitiveProperty(pos + 1, "lineComment");
150 }
151
152 // skip over blank lines
153 if(line.trim().length() < 1)
154 {
155 continue;
156 }
157
158 // re-get the lineComment property as it can vary
159 if(lineComment == null || lineComment.length() == 0)
160 {
161 Log.log(Log.DEBUG, TextToolsComments.class, "No line comment: "+lines[i]);
162 continue;
163 }
164 else
165 {
166 noCommentableLines = false;
167 }
168
169 lockBuffer(buffer);
170
171 // if the first non-whitespace char in the line is the line
172 // comment symbol and we are doing an uncomment
173 // then, remove, otherwise insert
174 if(line.trim().startsWith(lineComment) && doUncomment)
175 {
176 buffer.remove(pos, lineComment.length());
177 if(Character.isWhitespace(buffer.getText(pos, 1).charAt(0)))
178 {
179 buffer.remove(pos, 1);
180 }
181 }
182 else
183 {
184 // depending on options, add comment symbol at:
185 // - start of line
186 if(jEdit.getBooleanProperty("options.toggle-comments.indentAtLineStart"))
187 {
188 buffer.insert(lineStart, lineComment + " ");
189 }
190 // - leftmost indent of selected lines
191 else if(jEdit.getBooleanProperty("options.toggle-comments.indentAsBlock"))
192 {
193 Segment seg = new Segment();
194 buffer.getLineText(lines[i], seg);
195 Log.log(Log.DEBUG, TextToolsComments.class, "commenting line: "+lines[i]);
196 buffer.insert(lineStart + StandardUtilities.getOffsetOfVirtualColumn(seg, buffer.getTabSize(), leftmost, null), lineComment + " ");
197 }
198 // - or after all leading whitespace
199 else
200 {
201 buffer.insert(pos, lineComment + " ");
202 }
203 }
204 }
205
206 // if there were no commentable lines, beep & return
207 if(noCommentableLines)
208 {
209 view.getToolkit().beep();
210 return;
211 }
212
213 // optionally retain selection - by default behave as jEdit
214 if(!jEdit.getBooleanProperty("options.toggle-comments.keepSelected"))
215 {
216 Selection[] sArr = textArea.getSelection();
217 if(sArr.length > 0)
218 {
219 textArea.setCaretPosition(sArr[sArr.length - 1].getEnd());
220 }
221 }
222
223 }
224
225 // restore magic caret position - (un)commenting should not change it
226 textArea.setMagicCaretPosition(magicCaretPos);
227
228 // optionally move line down
229 if(jEdit.getBooleanProperty("options.toggle-comments.moveDown"))
230 {
231 // we don't do it if some text is selected
232 // we'd better avoid beeping if trying to go past the last line
233 if (textArea.getSelection().length==0 &&
234 textArea.getCaretLine()+1 < textArea.getLineCount())
235 {
236 textArea.goToNextLine(false);
237 }
238 }
239 }
240 finally
241 {
242 unlockBuffer(buffer);
243 }
244 } //}}}
245
246 //{{{ toggleRangeComments() method
247 /**
248 * Toggles all range comments in the specified view by applying {@link
249 * #toggleRangeComment(Buffer, int, int)} to each selection. If there are no
250 * selections {@link #toggleRangeComment(Buffer, int)} will be applied at the
251 * current caret position.
252 *
253 * @param view a jEdit view.
254 */
255 public static void toggleRangeComments(View view)
256 {
257 JEditTextArea textArea = view.getTextArea();
258 JEditBuffer buffer = textArea.getBuffer();
259 if(!buffer.isEditable() || !usesRangeComments(buffer))
260 {
261 view.getToolkit().beep();
262 return;
263 }
264
265 try
266 {
267 // get an array of all selections, if there are none, use the method
268 // that toggles comments around the caret
269 Selection[] selections = textArea.getSelection();
270 if(selections.length < 1)
271 {
272 toggleRangeComment(buffer, textArea.getCaretPosition());
273 }
274
275 // loop through each selection
276 String selectTxt;
277 for(int i = 0; i < selections.length; i++)
278 {
279 selectTxt = textArea.getSelectedText(selections[i]);
280 try
281 {
282 lockBuffer(buffer);
283
284 // get the start & end of the selection so we can retain it
285 int start = selections[i].getStart();
286 int end = selections[i].getEnd();
287
288 // toggle comments in this selection
289 int length = toggleRangeComment(buffer, start, end);
290
291 // optionally retain selection - by default behave as jEdit
292 if(jEdit.getBooleanProperty("options.toggle-comments.keepSelected"))
293 {
294 textArea.addToSelection(new Selection.Range(start, start + length));
295 }
296 }
297 catch(IndexOutOfBoundsException e)
298 {
299 StringWriter sw = new StringWriter();
300 e.printStackTrace(new PrintWriter(sw));
301 Log.log(Log.ERROR, TextToolsComments.class, sw.toString());
302 }
303 }
304 }
305 finally
306 {
307 unlockBuffer(buffer);
308 }
309 } //}}}
310
311 //{{{ isInRangeComment() method
312 /**
313 * Tests whether the specified buffer index is within a range comment block.
314 *
315 * @param buffer a jEdit buffer
316 * @param offset an index within <code>buffer</code>
317 * @return <code>true</code> if <code>offset</code> is within a range comment,
318 * <code>false</code> otherwise
319 */
320 private static boolean isInRangeComment(JEditBuffer buffer, int offset)
321 {
322 String preceding = buffer.getText(0, offset);
323 String commentStart = buffer.getContextSensitiveProperty(offset, "commentStart");
324 String commentEnd = buffer.getContextSensitiveProperty(offset, "commentEnd");
325 int lastStart = preceding.lastIndexOf(commentStart);
326 int lastEnd = preceding.lastIndexOf(commentEnd);
327 return (lastStart != -1 && lastStart > lastEnd);
328 } //}}}
329
330 //{{{ lockBuffer() method
331 /**
332 * If the specified <code>JEditBuffer</code> is not currently inside a compound
333 * edit, this method will start one, lock the buffer and return <code>true</code>
334 * , otherwise it will return <code>false</code>
335 *
336 * @param buffer a jEdit buffer
337 * @return <code>true</code> if this method locked the buffer, <code>false</code>
338 * if it was already locked
339 */
340 private static boolean lockBuffer(JEditBuffer buffer)
341 {
342 if(!buffer.insideCompoundEdit())
343 {
344 buffer.writeLock();
345 buffer.beginCompoundEdit();
346 return true;
347 }
348 return false;
349 }//}}}
350
351 //{{{ toggleRangeComment() method
352 /**
353 * Toggles range comments around a specified buffer index. If the index is
354 * within a range comment, the comment block will be un-commented. If the index
355 * is not within a range comment, the method behaves as {@link
356 * #toggleRangeComment(Buffer, int, int)} with <code>start</code> and <code>end</code>
357 * values equal to the start and end indexes of non-whitespace text on the same
358 * line as <code>offset</code> .
359 *
360 * @param buffer a jEdit buffer.
361 * @param offset a index in <code>buffer</code> .
362 */
363 private static void toggleRangeComment(JEditBuffer buffer, int offset)
364 {
365 String commentStart = buffer.getContextSensitiveProperty(offset, "commentStart");
366 String commentEnd = buffer.getContextSensitiveProperty(offset, "commentEnd");
367
368 if(isInRangeComment(buffer, offset))
369 {
370 String preceding = buffer.getText(0, offset);
371 String following = buffer.getText(offset, buffer.getLength() - offset);
372 int start = preceding.lastIndexOf(commentStart);
373 int end = following.indexOf(commentEnd) + offset;
374 try
375 {
376 lockBuffer(buffer);
377 buffer.remove(end, commentEnd.length());
378 buffer.remove(start, commentStart.length());
379 }
380 finally
381 {
382 unlockBuffer(buffer);
383 }
384 }
385 else
386 {
387 int line = buffer.getLineOfOffset(offset);
388 String lineTxt = buffer.getLineText(line);
389 int start = buffer.getLineStartOffset(line) + StandardUtilities.getLeadingWhiteSpace(lineTxt);
390 int end = buffer.getLineEndOffset(line) - (StandardUtilities.getTrailingWhiteSpace(lineTxt) + 1); // the +1 catches the \n
391 toggleRangeComment(buffer, start, end);
392 }
393 } //}}}
394
395 //{{{ toggleRangeComment() method
396 /**
397 * Toggles range commenting in a buffer around/within the range from <code>start
398 * </code> to <code>end</code> . In the simplest case comment symbols are added /
399 * removed around the specified range. If, however, there are already range
400 * comments within the specified range, the method will reverse their
401 * commenting state.
402 *
403 * @param buffer a jEdit buffer.
404 * @param start the start index of a range within <code>buffer</code> .
405 * @param end the end index of a range within <code>buffer</code> .
406 * @return the length of the text the specified range has been replaced
407 * with.
408 */
409 private static int toggleRangeComment(JEditBuffer buffer, int start, int end)
410 {
411 String commentStart = buffer.getContextSensitiveProperty(start, "commentStart");
412 String commentEnd = buffer.getContextSensitiveProperty(start, "commentEnd");
413 StringBuilder buf = new StringBuilder(buffer.getText(start, end - start));
414
415 // use a state machine to step through text and reverse commenting
416 final byte REMOVE_COMMENT_START = 0;
417 final byte REMOVE_COMMENT_END = 1;
418 final byte LOOK_FOR_COMMENT_END = 2;
419 final byte LOOK_FOR_COMMENT_START = 3;
420 final byte INSERT_COMMENT_START = 4;
421 final byte INSERT_COMMENT_END = 5;
422
423 byte state;
424 if(buf.indexOf(commentStart) == 0)
425 {
426 state = REMOVE_COMMENT_START;
427 }
428 else if(buf.indexOf(commentEnd) == 0)
429 {
430 state = REMOVE_COMMENT_END;
431 }
432 else if(isInRangeComment(buffer, start))
433 {
434 state = INSERT_COMMENT_END;
435 }
436 else
437 {
438 state = INSERT_COMMENT_START;
439 }
440
441 int i = 0;
442 boolean atStart;
443 while(i > -1 && i < buf.length())
444 {
445 switch(state)
446 {
447 case REMOVE_COMMENT_START:
448 atStart = i == 0;
449 buf.delete(i, i + commentStart.length());
450 state = atStart ? LOOK_FOR_COMMENT_END : INSERT_COMMENT_END;
451 break;
452 case REMOVE_COMMENT_END:
453 atStart = i == 0;
454 buf.delete(i, i + commentEnd.length());
455 state = atStart ? LOOK_FOR_COMMENT_START : INSERT_COMMENT_START;
456 break;
457 case INSERT_COMMENT_START:
458 buf.insert(i, commentStart);
459 i += commentStart.length();
460 state = LOOK_FOR_COMMENT_START;
461 break;
462 case INSERT_COMMENT_END:
463 buf.insert(i, commentEnd);
464 i += commentEnd.length();
465 state = LOOK_FOR_COMMENT_END;
466 break;
467 case LOOK_FOR_COMMENT_END:
468 i = buf.indexOf(commentEnd, i);
469 if(i == -1)
470 {
471 buf.append(commentStart);
472 }
473 else
474 {
475 state = REMOVE_COMMENT_END;
476 }
477 break;
478 case LOOK_FOR_COMMENT_START:
479 i = buf.indexOf(commentStart, i);
480 if(i == -1)
481 {
482 buf.append(commentEnd);
483 }
484 else
485 {
486 state = REMOVE_COMMENT_START;
487 }
488 break;
489 default:
490 throw new IllegalStateException("unknown state "+state);
491 }
492 }
493
494 boolean newEdit = false;
495 try
496 {
497 newEdit = lockBuffer(buffer);
498 buffer.remove(start, end - start);
499 buffer.insert(start, buf.toString());
500 return buf.length();
501 }
502 finally
503 {
504 // only unlock if this method started the edit
505 if(newEdit)
506 {
507 unlockBuffer(buffer);
508 }
509 }
510 } //}}}
511
512 //{{{ unlockBuffer() method
513 /**
514 * If the specified <code>JEditBuffer</code> is currently inside a compound edit,
515 * this method will end it, unlock the buffer and return <code>true</code>,
516 * otherwise it will return <code>false</code>
517 *
518 * @param buffer a jEdit buffer
519 * @return <code>true</code> if this method unlocked the buffer, <code>false</code>
520 * if it was already unlocked
521 */
522 private static boolean unlockBuffer(JEditBuffer buffer)
523 {
524 if(buffer.insideCompoundEdit())
525 {
526 buffer.endCompoundEdit();
527 buffer.writeUnlock();
528 return true;
529 }
530 return false;
531 }//}}}
532
533 //{{{ usesRangeComments() method
534 /**
535 * Determines if the given buffer has defined range comment settings.
536 *
537 * @param buffer a jEdit buffer.
538 * @return <code>true</code> if <code>buffer</code> uses range comments, <code>
539 * false</code> otherwise.
540 */
541 private static boolean usesRangeComments(JEditBuffer buffer)
542 {
543 return buffer.getStringProperty("commentStart") != null &&
544 buffer.getStringProperty("commentEnd") != null;
545 } //}}}
546}
547