diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/indentGuide/IndentGuideTest.groovy b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/indentGuide/IndentGuideTest.groovy new file mode 100644 index 000000000000..6383205b6b8a --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/indentGuide/IndentGuideTest.groovy @@ -0,0 +1,194 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInsight.daemon.indentGuide + +import com.intellij.openapi.editor.impl.IndentsModelImpl +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase +import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl +import com.intellij.util.ArrayUtilRt +import org.jetbrains.annotations.NotNull + +import static junit.framework.Assert.assertNotNull + +/** + * @author Denis Zhdanov + * @since 2/7/13 4:01 PM + */ +class IndentGuideTest extends LightCodeInsightFixtureTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp() + } + + void "test indent guides which cross commented code between comment mark and comment text"() { + // IDEA-99572. + doTest( +"""\ +class Test { + void test() { + | //test(); + | if (true) { + | | int i = 1; + | | | +//| | | comment; + | | int j = 1; + | } + } +} +""" + ) + } + + void "test that inner comment indents do not exceed"() { + // IDEA-89471. + doTest( +"""\ +class Test { + void test() { + | return; +//| | if (true) { +//| | | int i1 = 1; +//| | | int i2 = 2; +//| | | if (true) { +//| | | | int j1 = 1; +//| | | | int j2 = 2; +//| | | } +//| | } +//| int k = 1; + } +} +""" + ) + } + + void "test indent guide which starts on comment line"() { + // IDEA-62184 + doTest( +"""\ +class Test { + void test(int i) { + | switch (i) { + | // + | | case 1: + | | case 2: + | } + } +} +""" + ) + } + + void "test no indent guide for javadoc"() { + doTest( +"""\ +class Test { + /** + * doc + */ + int i; +} +""" + ) + } + + void "test no unnecessary guide for non-first line comments"() { + doTest( + """\ +class Test { + void test() { + | //int i1; + | //int i2; + | return; + } +} +""" + ) + } + + void "test block comment and inner indents"() { + doTest( + """\ +class Test { + int test() { + | return 1 /*{ + | | int test2() { + | | | int i1; + | | } + | | int i2; + | }*/; + } +} +""" + ) + } + + private void doTest(@NotNull String text) { + IndentGuideTestData testData = parse text + myFixture.configureByText("${getTestName(false)}.java", testData.documentText) + CodeInsightTestFixtureImpl.instantiateAndRun(myFixture.file, myFixture.editor, ArrayUtilRt.EMPTY_INT_ARRAY, false) + IndentsModelImpl model = myFixture.editor.indentsModel as IndentsModelImpl + assertEquals("expected to find ${testData.guides.size()} indent guide but got ${model.indents.size()}", + testData.guides.size(), model.indents.size()) + + testData.guides.each { + def descriptor = model.getDescriptor(it[0], it[1]) + assertNotNull("expected to find an indent guide at lines ${it[0]}-${it[1]}", descriptor) + assertEquals( + "expected that indent guide descriptor at lines ${it[0]}-${it[1]} has indent ${it[2]} but got ${descriptor.indentLevel}", + it[2], descriptor.indentLevel + ) + } + } + + @NotNull + private static IndentGuideTestData parse(@NotNull String text) { + def buffer = new StringBuilder() + def indentGuides = [] + def prevLineIndents = [:] // indent level -> start line + int shift, i, textStart + text.eachLine { lineText, line -> + shift = textStart = 0 + def endedGuides = prevLineIndents.clone() as Map + for (i = lineText.indexOf('|', 0); i >= 0; i = lineText.indexOf('|', textStart)) { + def indentLevel = i - shift + if (prevLineIndents[indentLevel]) { + endedGuides.remove(indentLevel) + } + else { + prevLineIndents[indentLevel] = line - 1 + } + shift++ + buffer << lineText[textStart.. + indentGuides << [startLine, line, level] + prevLineIndents.remove(level) + } + if (textStart < lineText.length()) { + buffer << lineText[textStart..-1] + } + buffer << '\n' + } + new IndentGuideTestData(documentText: buffer[0..-2], guides: indentGuides) + } +} + +class IndentGuideTestData { + String documentText + def guides // List of three element lists: [start indent line; end indent line; indent level] +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/IndentsPass.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/IndentsPass.java index 936f3adb19f7..d70e7223649d 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/IndentsPass.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/IndentsPass.java @@ -21,7 +21,7 @@ package com.intellij.codeInsight.daemon.impl; import com.intellij.codeHighlighting.TextEditorHighlightingPass; import com.intellij.codeInsight.highlighting.BraceMatchingUtil; -import com.intellij.lang.LanguageParserDefinitions; +import com.intellij.lang.*; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsScheme; @@ -40,19 +40,23 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.util.DocumentUtil; +import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.IntStack; import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; import java.awt.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; +import java.util.*; import java.util.List; +import java.util.concurrent.ConcurrentMap; public class IndentsPass extends TextEditorHighlightingPass implements DumbAware { + private static final ConcurrentMap COMMENT_PREFIXES = ContainerUtil.newConcurrentMap(); + private static final String NO_COMMENT_INFO_MARKER = "hopefully, noone uses this string as a comment prefix"; + private static final Key> INDENT_HIGHLIGHTERS_IN_EDITOR_KEY = Key.create("INDENT_HIGHLIGHTERS_IN_EDITOR_KEY"); private static final Key LAST_TIME_INDENTS_BUILT = Key.create("LAST_TIME_INDENTS_BUILT"); @@ -209,6 +213,7 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware @Override public void doCollectInformation(@NotNull ProgressIndicator progress) { + assert myDocument != null; final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT); if (stamp != null && stamp.longValue() == nowStamp()) return; @@ -228,6 +233,7 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware private long nowStamp() { if (!myEditor.getSettings().isIndentGuidesShown()) return -1; + assert myDocument != null; return myDocument.getModificationStamp(); } @@ -271,6 +277,7 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware } final int startRangeIndex = curRange; + assert myDocument != null; DocumentUtil.executeInBulk(myDocument, myRanges.size() > 10000, new Runnable() { @Override public void run() { @@ -279,7 +286,7 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware } } }); - + myEditor.putUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY, newHighlighters); myEditor.putUserData(LAST_TIME_INDENTS_BUILT, nowStamp()); @@ -298,6 +305,7 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware lines.push(0); indents.push(0); + assert myDocument != null; final CharSequence chars = myDocument.getCharsSequence(); for (int line = 1; line < lineIndents.length; line++) { ProgressManager.checkCanceled(); @@ -307,7 +315,9 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware ProgressManager.checkCanceled(); final int level = indents.pop(); int startLine = lines.pop(); - descriptors.add(createDescriptor(level, startLine, line, chars)); + if (level > 0) { + descriptors.add(createDescriptor(level, startLine, line, chars)); + } } int prevLine = line - 1; @@ -344,44 +354,40 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware return CharArrayUtil.shiftForward(chars, startOffset, " \t") >= myDocument.getLineEndOffset(line); } + /** + * Calculates line indents for the {@link #myDocument target document}. + * + * @return line indents array which uses lines as indices + */ + @NotNull private int[] calcIndents() { - final Document doc = myDocument; - CharSequence chars = doc.getCharsSequence(); - int[] lineIndents = new int[doc.getLineCount()]; - TokenSet comments = LanguageParserDefinitions.INSTANCE.forLanguage(myFile.getLanguage()).getCommentTokens(); - - int prevColumn = -1; - final EditorHighlighter highlighter = myEditor.getHighlighter(); + Context context = new Context(myDocument, myFile.getLanguage(), myEditor.getHighlighter()); + int[] lineIndents = context.lineIndents; final FileType fileType = myFile.getFileType(); + int prevLineIndent = -1; for (int line = 0; line < lineIndents.length; line++) { ProgressManager.checkCanceled(); - int lineStart = doc.getLineStartOffset(line); - int lineEnd = doc.getLineEndOffset(line); - - int nonWhitespaceOffset = CharArrayUtil.shiftForward(chars, lineStart, " \t"); - if (nonWhitespaceOffset < lineEnd) { - final int column = myEditor.calcColumnNumber(nonWhitespaceOffset, line); - if (column != prevColumn) { - final HighlighterIterator it = highlighter.createIterator(nonWhitespaceOffset); - if (comments.contains(it.getTokenType())) { - final int commentIndentStartOffset = CharArrayUtil.shiftForwardUntil(chars, Math.max(it.getStart(), lineStart), " \t"); - if (commentIndentStartOffset < it.getEnd() && commentIndentStartOffset < lineEnd) { - final int indentOffsetInsideComment = CharArrayUtil.shiftForward(chars, commentIndentStartOffset, " \t"); - if (indentOffsetInsideComment < it.getEnd() && indentOffsetInsideComment < lineEnd) { - lineIndents[line] = myEditor.calcColumnNumber(indentOffsetInsideComment, line); - continue; - } - } - continue; - } - } - - lineIndents[line] = column; - prevColumn = column; + int lineStart = myDocument.getLineStartOffset(line); + int lineEnd = myDocument.getLineEndOffset(line); + final int nonWhitespaceOffset = CharArrayUtil.shiftForward(context.chars, lineStart, " \t"); + final int column; + if (nonWhitespaceOffset == lineEnd) { + lineIndents[line] = -1; // Blank line marker + } + else if (nonWhitespaceOffset > lineEnd) { + lineIndents[line] = calcIndent(context, line, lineStart, lineEnd, 0); + prevLineIndent = lineIndents[line]; } else { - lineIndents[line] = -1; + column = myEditor.calcColumnNumber(nonWhitespaceOffset, line); + if (prevLineIndent > 0 && prevLineIndent > column) { + lineIndents[line] = calcIndent(context, line, nonWhitespaceOffset, lineEnd, column); + } + else { + lineIndents[line] = column; + } + prevLineIndent = lineIndents[line]; } } @@ -402,9 +408,9 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware int indent = Math.min(topIndent, bottomIndent); if (bottomIndent < topIndent) { - int nonWhitespaceOffset = CharArrayUtil.shiftForward(chars, doc.getLineStartOffset(line), " \t"); - HighlighterIterator iterator = highlighter.createIterator(nonWhitespaceOffset); - if (BraceMatchingUtil.isRBraceToken(iterator, chars, fileType)) { + int nonWhitespaceOffset = CharArrayUtil.shiftForward(context.chars, myDocument.getLineStartOffset(line), " \t"); + HighlighterIterator iterator = context.highlighter.createIterator(nonWhitespaceOffset); + if (BraceMatchingUtil.isRBraceToken(iterator, context.chars, fileType)) { indent = topIndent; } } @@ -422,6 +428,87 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware return lineIndents; } + /** + * Tries to calculate given line's indent column assuming that there might be a comment at the given indent offset + * (see {@link #parseComments(IElementType)}). + * + * @param context current calculation context + * @param line target line + * @param indentOffset start indent offset to use for the given line + * @param lineEndOffset given line's end offset + * @param fallbackColumn column to return if it's not possible to apply comment-specific indent calculation rules + * @return given line's indent column to use + */ + private int calcIndent(@NotNull Context context, int line, int indentOffset, int lineEndOffset, int fallbackColumn) { + final HighlighterIterator it = context.highlighter.createIterator(indentOffset); + if (context.comments.contains(it.getTokenType()) && indentOffset == it.getStart()) { + String prefix = COMMENT_PREFIXES.get(it.getTokenType()); + if (prefix == null) { + prefix = parseComments(it.getTokenType()); + } + if (!NO_COMMENT_INFO_MARKER.equals(prefix)) { + final int indentInsideCommentOffset = CharArrayUtil.shiftForward(context.chars, indentOffset + prefix.length(), " \t"); + if (indentInsideCommentOffset < lineEndOffset) { + return myEditor.calcColumnNumber(indentInsideCommentOffset, line); + } + } + } + return fallbackColumn; + } + + /** + * We want to treat comments specially in a way to skip comment prefix on line indent calculation. + *

+ * Example: + *

+   *   it (true) {
+   *     int i1;
+   * //    int i2;
+   *     int i3;
+   *   }
+   * 
+ * We want to use 'int i2;' start offset as the third line indent (though it has non-white space comment prefix (//) + * at the first column. + *

+ * This method tries to parse comment prefix for the language implied by the given comment type. It uses + * {@link #NO_COMMENT_INFO_MARKER} as an indicator that that information is unavailable + * + * @param commentType target comment type + * @return prefix of the comment denoted by the given type if any; + * {@link #NO_COMMENT_INFO_MARKER} otherwise + */ + @NotNull + private static String parseComments(@NotNull IElementType commentType) { + Commenter c = LanguageCommenters.INSTANCE.forLanguage(commentType.getLanguage()); + if (!(c instanceof CodeDocumentationAwareCommenter)) { + COMMENT_PREFIXES.put(commentType, NO_COMMENT_INFO_MARKER); + return NO_COMMENT_INFO_MARKER; + } + CodeDocumentationAwareCommenter commenter = (CodeDocumentationAwareCommenter)c; + + IElementType lineCommentType = commenter.getLineCommentTokenType(); + String lineCommentPrefix = commenter.getLineCommentPrefix(); + if (lineCommentType != null) { + COMMENT_PREFIXES.put(lineCommentType, lineCommentPrefix == null ? NO_COMMENT_INFO_MARKER : lineCommentPrefix); + } + + IElementType blockCommentType = commenter.getBlockCommentTokenType(); + String blockCommentPrefix = commenter.getBlockCommentPrefix(); + if (blockCommentType != null) { + COMMENT_PREFIXES.put(blockCommentType, blockCommentPrefix == null ? NO_COMMENT_INFO_MARKER : blockCommentPrefix); + } + + IElementType docCommentType = commenter.getDocumentationCommentTokenType(); + String docCommentPrefix = commenter.getDocumentationCommentPrefix(); + if (docCommentType != null) { + COMMENT_PREFIXES.put(docCommentType, docCommentPrefix == null ? NO_COMMENT_INFO_MARKER : docCommentPrefix); + } + + COMMENT_PREFIXES.putIfAbsent(commentType, NO_COMMENT_INFO_MARKER); + return COMMENT_PREFIXES.get(commentType); + } + + @NotNull private static RangeHighlighter createHighlighter(MarkupModel mm, TextRange range) { final RangeHighlighter highlighter = mm.addRangeHighlighter(range.getStartOffset(), range.getEndOffset(), 0, null, HighlighterTargetArea.EXACT_RANGE); @@ -429,8 +516,26 @@ public class IndentsPass extends TextEditorHighlightingPass implements DumbAware return highlighter; } - private static int compare(TextRange r, RangeHighlighter h) { + private static int compare(@NotNull TextRange r, @NotNull RangeHighlighter h) { int answer = r.getStartOffset() - h.getStartOffset(); return answer != 0 ? answer : r.getEndOffset() - h.getEndOffset(); } + + /** + * 'Parameter object' for {@link #calcIndent(Context, int, int, int, int)}. + */ + private static class Context { + + @NotNull public final int[] lineIndents; + @NotNull public final CharSequence chars; + @NotNull public final TokenSet comments; + @NotNull public final EditorHighlighter highlighter; + + Context(@NotNull Document document, @NotNull Language language, @NotNull EditorHighlighter highlighter) { + lineIndents = new int[document.getLineCount()]; + chars = document.getCharsSequence(); + comments = LanguageParserDefinitions.INSTANCE.forLanguage(language).getCommentTokens(); + this.highlighter = highlighter; + } + } } diff --git a/platform/platform-api/src/com/intellij/openapi/editor/IndentGuideDescriptor.java b/platform/platform-api/src/com/intellij/openapi/editor/IndentGuideDescriptor.java index 9c49d6e86a48..f0ead0728c05 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/IndentGuideDescriptor.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/IndentGuideDescriptor.java @@ -31,8 +31,29 @@ public class IndentGuideDescriptor { } @Override - public boolean equals(Object obj) { - IndentGuideDescriptor other = (IndentGuideDescriptor)obj; - return indentLevel == other.indentLevel && startLine == other.startLine && endLine == other.endLine; + public int hashCode() { + int result = indentLevel; + result = 31 * result + startLine; + result = 31 * result + endLine; + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IndentGuideDescriptor that = (IndentGuideDescriptor)o; + + if (endLine != that.endLine) return false; + if (indentLevel != that.indentLevel) return false; + if (startLine != that.startLine) return false; + + return true; + } + + @Override + public String toString() { + return String.format("%d (%d-%d)", indentLevel, startLine, endLine); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/IndentsModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/IndentsModelImpl.java index 2bf5702bd5a0..6565d5bf4a48 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/IndentsModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/IndentsModelImpl.java @@ -22,22 +22,27 @@ package com.intellij.openapi.editor.impl; import com.intellij.openapi.editor.IndentGuideDescriptor; import com.intellij.openapi.editor.IndentsModel; import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.util.containers.ContainerUtilRt; +import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; public class IndentsModelImpl implements IndentsModel { - private final Map myIndentsByLines = new HashMap(); - private List myIndents = new ArrayList(); - private final EditorImpl myEditor; + private final Map myIndentsByLines = ContainerUtilRt.newHashMap(); + private List myIndents = ContainerUtilRt.newArrayList(); + @NotNull private final EditorImpl myEditor; - public IndentsModelImpl(EditorImpl editor) { + public IndentsModelImpl(@NotNull EditorImpl editor) { myEditor = editor; } + @NotNull + public List getIndents() { + return myIndents; + } + @Override public IndentGuideDescriptor getCaretIndentGuide() { final LogicalPosition pos = myEditor.getCaretModel().getLogicalPosition(); @@ -60,16 +65,16 @@ public class IndentsModelImpl implements IndentsModel { } @Override - public void assumeIndents(List descriptors) { + public void assumeIndents(@NotNull List descriptors) { myIndents = descriptors; myIndentsByLines.clear(); for (IndentGuideDescriptor descriptor : myIndents) { myIndentsByLines.put(new IntPair(descriptor.startLine, descriptor.endLine), descriptor); } } - + private static class IntPair { - + private final int start; private final int end;