2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com
.intellij
.codeInsight
.generation
;
19 import com
.intellij
.codeInsight
.CodeInsightActionHandler
;
20 import com
.intellij
.codeInsight
.CommentUtil
;
21 import com
.intellij
.featureStatistics
.FeatureUsageTracker
;
22 import com
.intellij
.ide
.highlighter
.custom
.SyntaxTable
;
23 import com
.intellij
.lang
.Commenter
;
24 import com
.intellij
.lang
.Language
;
25 import com
.intellij
.lang
.LanguageCommenters
;
26 import com
.intellij
.openapi
.actionSystem
.IdeActions
;
27 import com
.intellij
.openapi
.actionSystem
.ex
.ActionManagerEx
;
28 import com
.intellij
.openapi
.editor
.*;
29 import com
.intellij
.openapi
.fileEditor
.FileDocumentManager
;
30 import com
.intellij
.openapi
.fileTypes
.FileType
;
31 import com
.intellij
.openapi
.fileTypes
.impl
.AbstractFileType
;
32 import com
.intellij
.openapi
.project
.Project
;
33 import com
.intellij
.openapi
.util
.Comparing
;
34 import com
.intellij
.openapi
.util
.TextRange
;
35 import com
.intellij
.psi
.PsiDocumentManager
;
36 import com
.intellij
.psi
.PsiFile
;
37 import com
.intellij
.psi
.codeStyle
.CodeStyleManager
;
38 import com
.intellij
.psi
.codeStyle
.CodeStyleSettingsManager
;
39 import com
.intellij
.psi
.codeStyle
.Indent
;
40 import com
.intellij
.psi
.util
.PsiUtilBase
;
41 import com
.intellij
.util
.StringBuilderSpinAllocator
;
42 import com
.intellij
.util
.containers
.IntArrayList
;
43 import com
.intellij
.util
.text
.CharArrayUtil
;
44 import org
.jetbrains
.annotations
.NotNull
;
45 import org
.jetbrains
.annotations
.Nullable
;
47 public class CommentByLineCommentHandler
implements CodeInsightActionHandler
{
48 private Project myProject
;
49 private PsiFile myFile
;
50 private Document myDocument
;
51 private Editor myEditor
;
52 private int myStartOffset
;
53 private int myEndOffset
;
54 private int myStartLine
;
55 private int myEndLine
;
56 private int[] myStartOffsets
;
57 private int[] myEndOffsets
;
58 private Commenter
[] myCommenters
;
59 private CodeStyleManager myCodeStyleManager
;
61 public void invoke(@NotNull Project project
, @NotNull Editor editor
, @NotNull PsiFile file
) {
63 myFile
= file
.getViewProvider().getPsi(file
.getViewProvider().getBaseLanguage());
64 myDocument
= editor
.getDocument();
67 if (!FileDocumentManager
.getInstance().requestWriting(myDocument
, project
)) {
71 PsiDocumentManager
.getInstance(project
).commitDocument(myDocument
);
73 FeatureUsageTracker
.getInstance().triggerFeatureUsed("codeassists.comment.line");
75 //myCodeInsightSettings = (CodeInsightSettings)ApplicationManager.getApplication().getComponent(CodeInsightSettings.class);
76 myCodeStyleManager
= CodeStyleManager
.getInstance(myProject
);
78 final SelectionModel selectionModel
= editor
.getSelectionModel();
80 boolean hasSelection
= selectionModel
.hasSelection();
81 myStartOffset
= selectionModel
.getSelectionStart();
82 myEndOffset
= selectionModel
.getSelectionEnd();
84 if (myDocument
.getTextLength() == 0) return;
87 int lastLineEnd
= myDocument
.getLineEndOffset(myDocument
.getLineNumber(myEndOffset
));
88 FoldRegion collapsedAt
= editor
.getFoldingModel().getCollapsedRegionAtOffset(lastLineEnd
);
89 if (collapsedAt
!= null) {
90 final int endOffset
= collapsedAt
.getEndOffset();
91 if (endOffset
<= myEndOffset
) {
94 myEndOffset
= endOffset
;
100 boolean wholeLinesSelected
= !hasSelection
|| (
101 myStartOffset
== myDocument
.getLineStartOffset(myDocument
.getLineNumber(myStartOffset
)) &&
102 myEndOffset
== myDocument
.getLineEndOffset(myDocument
.getLineNumber(myEndOffset
- 1)) + 1);
104 boolean startingNewLineComment
= !hasSelection
&& isLineEmpty(myDocument
.getLineNumber(myStartOffset
)) && !Comparing
105 .equal(IdeActions
.ACTION_COMMENT_LINE
, ActionManagerEx
.getInstanceEx().getPrevPreformedActionId());
108 if (startingNewLineComment
) {
109 final Commenter commenter
= myCommenters
[0];
110 if (commenter
!= null) {
111 String prefix
= commenter
.getLineCommentPrefix();
112 if (prefix
== null) prefix
= commenter
.getBlockCommentPrefix();
113 int lineStart
= myDocument
.getLineStartOffset(myStartLine
);
114 lineStart
= CharArrayUtil
.shiftForward(myDocument
.getCharsSequence(), lineStart
, " \t");
115 lineStart
+= prefix
.length();
116 if (lineStart
< myDocument
.getTextLength() && myDocument
.getCharsSequence().charAt(lineStart
) == ' ') lineStart
++;
117 editor
.getCaretModel().moveToOffset(lineStart
);
118 editor
.getScrollingModel().scrollToCaret(ScrollType
.RELATIVE
);
123 editor
.getCaretModel().moveCaretRelatively(0, 1, false, false, true);
126 if (wholeLinesSelected
) {
127 selectionModel
.setSelection(myStartOffset
, selectionModel
.getSelectionEnd());
133 private boolean isLineEmpty(final int line
) {
134 final CharSequence chars
= myDocument
.getCharsSequence();
135 int start
= myDocument
.getLineStartOffset(line
);
136 int end
= Math
.min(myDocument
.getLineEndOffset(line
), myDocument
.getTextLength() - 1);
137 for (int i
= start
; i
<= end
; i
++) {
138 if (!Character
.isWhitespace(chars
.charAt(i
))) return false;
143 public boolean startInWriteAction() {
147 private void doComment() {
148 myStartLine
= myDocument
.getLineNumber(myStartOffset
);
149 myEndLine
= myDocument
.getLineNumber(myEndOffset
);
151 if (myEndLine
> myStartLine
&& myDocument
.getLineStartOffset(myEndLine
) == myEndOffset
) {
155 myStartOffsets
= new int[myEndLine
- myStartLine
+ 1];
156 myEndOffsets
= new int[myEndLine
- myStartLine
+ 1];
157 myCommenters
= new Commenter
[myEndLine
- myStartLine
+ 1];
158 boolean allLineCommented
= true;
159 CharSequence chars
= myDocument
.getCharsSequence();
161 boolean singleline
= myStartLine
== myEndLine
;
162 int offset
= myDocument
.getLineStartOffset(myStartLine
);
163 offset
= CharArrayUtil
.shiftForward(myDocument
.getCharsSequence(), offset
, " \t");
164 final Language languageSuitableForCompleteFragment
= PsiUtilBase
.reallyEvaluateLanguageInRange(offset
, CharArrayUtil
.shiftBackward(
165 myDocument
.getCharsSequence(), myDocument
.getLineEndOffset(myEndLine
), " \t\n"), myFile
);
167 Commenter blockSuitableCommenter
= languageSuitableForCompleteFragment
== null ? LanguageCommenters
.INSTANCE
.forLanguage(myFile
.getLanguage()) : null;
168 if (blockSuitableCommenter
== null && myFile
.getFileType() instanceof AbstractFileType
) {
169 blockSuitableCommenter
= new Commenter() {
170 final SyntaxTable mySyntaxTable
= ((AbstractFileType
)myFile
.getFileType()).getSyntaxTable();
172 public String
getLineCommentPrefix() {
173 return mySyntaxTable
.getLineComment();
177 public String
getBlockCommentPrefix() {
178 return mySyntaxTable
.getStartComment();
182 public String
getBlockCommentSuffix() {
183 return mySyntaxTable
.getEndComment();
186 public String
getCommentedBlockCommentPrefix() {
190 public String
getCommentedBlockCommentSuffix() {
196 for (int line
= myStartLine
; line
<= myEndLine
; line
++) {
197 final Commenter commenter
= blockSuitableCommenter
!= null ? blockSuitableCommenter
: findCommenter(line
);
198 if (commenter
== null) return;
200 if (commenter
.getLineCommentPrefix() == null &&
201 (commenter
.getBlockCommentPrefix() == null || commenter
.getBlockCommentSuffix() == null)) {
204 myCommenters
[line
- myStartLine
] = commenter
;
205 if (!isLineCommented(line
, chars
, commenter
) && (singleline
|| !isLineEmpty(line
))) {
206 allLineCommented
= false;
211 if (!allLineCommented
) {
212 if (CodeStyleSettingsManager
.getSettings(myProject
).LINE_COMMENT_AT_FIRST_COLUMN
) {
213 doDefaultCommenting(blockSuitableCommenter
);
216 doIndentCommenting(blockSuitableCommenter
);
220 for (int line
= myEndLine
; line
>= myStartLine
; line
--) {
222 //int offset1 = myStartOffsets[line - myStartLine];
223 //int offset2 = myEndOffsets[line - myStartLine];
224 //if (offset1 == offset2) continue;
225 //Commenter commenter = myCommenters[line - myStartLine];
226 //String prefix = commenter.getBlockCommentPrefix();
227 //if (prefix == null || !myDocument.getText().substring(offset1, myDocument.getTextLength()).startsWith(prefix)) {
228 // prefix = commenter.getLineCommentPrefix();
231 //String suffix = commenter.getBlockCommentSuffix();
232 //if (suffix == null && prefix != null) suffix = "";
234 //if (prefix != null && suffix != null) {
235 // final int suffixLen = suffix.length();
236 // final int prefixLen = prefix.length();
237 // if (offset2 >= 0) {
238 // if (!CharArrayUtil.regionMatches(chars, offset1 + prefixLen, prefix)) {
239 // myDocument.deleteString(offset2 - suffixLen, offset2);
242 // if (offset1 >= 0) {
243 // for (int i = offset2 - suffixLen - 1; i > offset1 + prefixLen; --i) {
244 // if (CharArrayUtil.regionMatches(chars, i, suffix)) {
245 // myDocument.deleteString(i, i + suffixLen);
247 // else if (CharArrayUtil.regionMatches(chars, i, prefix)) {
248 // myDocument.deleteString(i, i + prefixLen);
251 // myDocument.deleteString(offset1, offset1 + prefixLen);
258 private boolean isLineCommented(final int line
, final CharSequence chars
, final Commenter commenter
) {
259 String prefix
= commenter
.getLineCommentPrefix();
260 int lineStart
= myDocument
.getLineStartOffset(line
);
261 lineStart
= CharArrayUtil
.shiftForward(chars
, lineStart
, " \t");
263 if (prefix
!= null) {
264 commented
= CharArrayUtil
.regionMatches(chars
, lineStart
, prefix
) ||
265 prefix
.endsWith(" ") && CharArrayUtil
.regionMatches(chars
, lineStart
, prefix
.trim()+"\n");
267 myStartOffsets
[line
- myStartLine
] = lineStart
;
268 myEndOffsets
[line
- myStartLine
] = -1;
272 prefix
= commenter
.getBlockCommentPrefix();
273 String suffix
= commenter
.getBlockCommentSuffix();
274 final int textLength
= myDocument
.getTextLength();
275 int lineEnd
= myDocument
.getLineEndOffset(line
);
276 if (lineEnd
== textLength
) {
277 final int shifted
= CharArrayUtil
.shiftBackward(chars
, textLength
- 1, " \t");
278 if (shifted
< textLength
- 1) lineEnd
= shifted
;
281 lineEnd
= CharArrayUtil
.shiftBackward(chars
, lineEnd
, " \t");
283 commented
= (lineStart
== lineEnd
&& myStartLine
!= myEndLine
) || (CharArrayUtil
.regionMatches(chars
, lineStart
, prefix
) &&
284 CharArrayUtil
.regionMatches(chars
, lineEnd
- suffix
.length(), suffix
));
286 myStartOffsets
[line
- myStartLine
] = lineStart
;
287 myEndOffsets
[line
- myStartLine
] = lineEnd
;
294 private Commenter
findCommenter(final int line
) {
295 final FileType fileType
= myFile
.getFileType();
296 if (fileType
instanceof AbstractFileType
) {
297 return ((AbstractFileType
)fileType
).getCommenter();
300 int lineStartOffset
= myDocument
.getLineStartOffset(line
);
301 int lineEndOffset
= myDocument
.getLineEndOffset(line
) - 1;
302 final CharSequence charSequence
= myDocument
.getCharsSequence();
303 lineStartOffset
= CharArrayUtil
.shiftForward(charSequence
, lineStartOffset
, " \t");
304 lineEndOffset
= CharArrayUtil
.shiftBackward(charSequence
, lineEndOffset
< 0 ?
0 : lineEndOffset
, " \t");
305 final Language lineStartLanguage
= PsiUtilBase
.getLanguageAtOffset(myFile
, lineStartOffset
);
306 final Language lineEndLanguage
= PsiUtilBase
.getLanguageAtOffset(myFile
, lineEndOffset
);
307 return CommentByBlockCommentHandler
.getCommenter(myFile
, myEditor
, lineStartLanguage
, lineEndLanguage
);
310 private Indent
computeMinIndent(int line1
, int line2
, CharSequence chars
, CodeStyleManager codeStyleManager
, FileType fileType
) {
311 Indent minIndent
= CommentUtil
.getMinLineIndent(myProject
, myDocument
, line1
, line2
, fileType
);
313 int commentOffset
= getCommentStart(line1
- 1);
314 if (commentOffset
>= 0) {
315 int lineStart
= myDocument
.getLineStartOffset(line1
- 1);
316 String space
= chars
.subSequence(lineStart
, commentOffset
).toString();
317 Indent indent
= codeStyleManager
.getIndent(space
, fileType
);
318 minIndent
= minIndent
!= null ? indent
.min(minIndent
) : indent
;
321 if (minIndent
== null) {
322 minIndent
= codeStyleManager
.zeroIndent();
327 private int getCommentStart(int line
) {
328 int offset
= myDocument
.getLineStartOffset(line
);
329 CharSequence chars
= myDocument
.getCharsSequence();
330 offset
= CharArrayUtil
.shiftForward(chars
, offset
, " \t");
331 final Commenter commenter
= findCommenter(line
);
332 if (commenter
== null) return -1;
333 String prefix
= commenter
.getLineCommentPrefix();
334 if (prefix
== null) prefix
= commenter
.getBlockCommentPrefix();
335 if (prefix
== null) return -1;
336 return CharArrayUtil
.regionMatches(chars
, offset
, prefix
) ? offset
: -1;
339 public void doDefaultCommenting(Commenter commenter
) {
340 for (int line
= myEndLine
; line
>= myStartLine
; line
--) {
341 int offset
= myDocument
.getLineStartOffset(line
);
342 commentLine(line
, offset
, commenter
);
346 private void doIndentCommenting(Commenter commenter
) {
347 CharSequence chars
= myDocument
.getCharsSequence();
348 final FileType fileType
= myFile
.getFileType();
349 Indent minIndent
= computeMinIndent(myStartLine
, myEndLine
, chars
, myCodeStyleManager
, fileType
);
351 for (int line
= myEndLine
; line
>= myStartLine
; line
--) {
352 int lineStart
= myDocument
.getLineStartOffset(line
);
353 int offset
= lineStart
;
354 final StringBuilder buffer
= StringBuilderSpinAllocator
.alloc();
357 String space
= buffer
.toString();
358 Indent indent
= myCodeStyleManager
.getIndent(space
, fileType
);
359 if (indent
.isGreaterThan(minIndent
) || indent
.equals(minIndent
)) break;
360 char c
= chars
.charAt(offset
);
361 if (c
!= ' ' && c
!= '\t') {
362 String newSpace
= myCodeStyleManager
.fillIndent(minIndent
, fileType
);
363 myDocument
.replaceString(lineStart
, offset
, newSpace
);
364 offset
= lineStart
+ newSpace
.length();
372 StringBuilderSpinAllocator
.dispose(buffer
);
374 commentLine(line
, offset
,commenter
);
378 private void uncommentRange(int startOffset
, int endOffset
, @NotNull Commenter commenter
) {
379 final String commentedSuffix
= commenter
.getCommentedBlockCommentSuffix();
380 final String commentedPrefix
= commenter
.getCommentedBlockCommentPrefix();
381 final String prefix
= commenter
.getBlockCommentPrefix();
382 final String suffix
= commenter
.getBlockCommentSuffix();
383 if (prefix
== null || suffix
== null) {
386 if (endOffset
>= suffix
.length() && CharArrayUtil
.regionMatches(myDocument
.getCharsSequence(), endOffset
- suffix
.length(), suffix
)) {
387 myDocument
.deleteString(endOffset
- suffix
.length(), endOffset
);
389 if (commentedPrefix
!= null && commentedSuffix
!= null) {
390 CommentByBlockCommentHandler
.commentNestedComments(myDocument
, new TextRange(startOffset
, endOffset
), commenter
);
392 myDocument
.deleteString(startOffset
, startOffset
+ prefix
.length());
395 private void uncommentLine(int line
) {
396 Commenter commenter
= myCommenters
[line
- myStartLine
];
397 if (commenter
== null) commenter
= findCommenter(line
);
398 if (commenter
== null) return;
400 final int startOffset
= myStartOffsets
[line
- myStartLine
];
401 final int endOffset
= myEndOffsets
[line
- myStartLine
];
402 if (startOffset
== endOffset
) {
405 String prefix
= commenter
.getLineCommentPrefix();
406 if (prefix
!= null) {
407 CharSequence chars
= myDocument
.getCharsSequence();
408 boolean skipNewLine
= false;
409 boolean commented
= CharArrayUtil
.regionMatches(chars
, startOffset
, prefix
) ||
410 (skipNewLine
= (prefix
.endsWith(" ") && CharArrayUtil
.regionMatches(chars
, startOffset
, prefix
.trim() + "\n")));
412 int position
= 0;//text.indexOf(prefix);
413 myDocument
.deleteString(position
+ startOffset
, position
+ startOffset
+ (skipNewLine? prefix
.trim().length():prefix
.length()));
416 String text
= myDocument
.getCharsSequence().subSequence(startOffset
, endOffset
).toString();
418 prefix
= commenter
.getBlockCommentPrefix();
419 final String suffix
= commenter
.getBlockCommentSuffix();
420 if (prefix
== null || suffix
== null) {
424 IntArrayList prefixes
= new IntArrayList();
425 IntArrayList suffixes
= new IntArrayList();
426 for (int position
= 0; position
< text
.length();) {
427 int prefixPos
= text
.indexOf(prefix
, position
);
428 if (prefixPos
== -1) {
431 prefixes
.add(prefixPos
);
432 position
= prefixPos
+ prefix
.length();
433 int suffixPos
= text
.indexOf(suffix
, position
);
434 if (suffixPos
== -1) {
435 suffixPos
= text
.length() - suffix
.length();
437 suffixes
.add(suffixPos
);
438 position
= suffixPos
+ suffix
.length();
441 assert prefixes
.size() == suffixes
.size();
443 for (int i
= prefixes
.size() - 1; i
>= 0; i
--) {
444 uncommentRange(startOffset
+ prefixes
.get(i
), Math
.min(startOffset
+ suffixes
.get(i
) + suffix
.length(), endOffset
), commenter
);
448 private void commentLine(int line
, int offset
, @Nullable Commenter commenter
) {
449 if (commenter
== null) commenter
= findCommenter(line
);
450 if (commenter
== null) return;
451 String prefix
= commenter
.getLineCommentPrefix();
452 if (prefix
!= null) {
453 myDocument
.insertString(offset
, prefix
);
456 prefix
= commenter
.getBlockCommentPrefix();
457 String suffix
= commenter
.getBlockCommentSuffix();
458 if (prefix
== null || suffix
== null) return;
459 int endOffset
= myDocument
.getLineEndOffset(line
);
460 if (endOffset
== offset
&& myStartLine
!= myEndLine
) return;
461 final int textLength
= myDocument
.getTextLength();
462 final CharSequence chars
= myDocument
.getCharsSequence();
463 offset
= CharArrayUtil
.shiftForward(chars
, offset
, " \t");
464 if (endOffset
== textLength
) {
465 final int shifted
= CharArrayUtil
.shiftBackward(chars
, textLength
- 1, " \t");
466 if (shifted
< textLength
- 1) endOffset
= shifted
;
469 endOffset
= CharArrayUtil
.shiftBackward(chars
, endOffset
, " \t");
471 final String text
= myDocument
.getCharsSequence().subSequence(offset
, endOffset
).toString();
472 final IntArrayList prefixes
= new IntArrayList();
473 final IntArrayList suffixes
= new IntArrayList();
474 final String commentedSuffix
= commenter
.getCommentedBlockCommentSuffix();
475 final String commentedPrefix
= commenter
.getCommentedBlockCommentPrefix();
476 for (int position
= 0; position
< text
.length();) {
477 int nearestPrefix
= text
.indexOf(prefix
, position
);
478 if (nearestPrefix
== -1) {
479 nearestPrefix
= text
.length();
481 int nearestSuffix
= text
.indexOf(suffix
, position
);
482 if (nearestSuffix
== -1) {
483 nearestSuffix
= text
.length();
485 if (Math
.min(nearestPrefix
, nearestSuffix
) == text
.length()) {
488 if (nearestPrefix
< nearestSuffix
) {
489 prefixes
.add(nearestPrefix
);
490 position
= nearestPrefix
+ prefix
.length();
493 suffixes
.add(nearestSuffix
);
494 position
= nearestSuffix
+ suffix
.length();
497 if (!(commentedSuffix
== null && !suffixes
.isEmpty() && offset
+ suffixes
.get(suffixes
.size() - 1) + suffix
.length() >= endOffset
)) {
498 myDocument
.insertString(endOffset
, suffix
);
500 int nearestPrefix
= prefixes
.size() - 1;
501 int nearestSuffix
= suffixes
.size() - 1;
502 while (nearestPrefix
>= 0 || nearestSuffix
>= 0) {
503 if (nearestSuffix
== -1 || nearestPrefix
!= -1 && prefixes
.get(nearestPrefix
) > suffixes
.get(nearestSuffix
)) {
504 final int position
= prefixes
.get(nearestPrefix
);
506 if (commentedPrefix
!= null) {
507 myDocument
.replaceString(offset
+ position
, offset
+ position
+ prefix
.length(), commentedPrefix
);
509 else if (position
!= 0) {
510 myDocument
.insertString(offset
+ position
, suffix
);
514 final int position
= suffixes
.get(nearestSuffix
);
516 if (commentedSuffix
!= null) {
517 myDocument
.replaceString(offset
+ position
, offset
+ position
+ suffix
.length(), commentedSuffix
);
519 else if (offset
+ position
+ suffix
.length() < endOffset
) {
520 myDocument
.insertString(offset
+ position
+ suffix
.length(), prefix
);
524 if (!(commentedPrefix
== null && !prefixes
.isEmpty() && prefixes
.get(0) == 0)) {
525 myDocument
.insertString(offset
, prefix
);