Skip to content

Commit 9f776ab

Browse files
committed
Implemented folding of section-comments.
This fixes #63, fixes #62, fixes #57
1 parent abe19d4 commit 9f776ab

6 files changed

Lines changed: 337 additions & 87 deletions

File tree

.idea/codeStyleSettings.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/dictionaries/patrick.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/de/halirutan/mathematica/codeinsight/folding/MathematicaExpressionFoldingBuilder.java

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,17 @@
2727
import com.intellij.lang.folding.NamedFoldingDescriptor;
2828
import com.intellij.openapi.editor.Document;
2929
import com.intellij.openapi.util.TextRange;
30+
import com.intellij.psi.PsiComment;
3031
import com.intellij.psi.PsiElement;
32+
import com.intellij.psi.PsiFile;
3133
import com.intellij.psi.tree.IElementType;
3234
import com.intellij.psi.tree.TokenSet;
35+
import com.intellij.psi.util.PsiTreeUtil;
3336
import de.halirutan.mathematica.parsing.MathematicaElementTypes;
37+
import de.halirutan.mathematica.parsing.psi.api.CompoundExpression;
3438
import de.halirutan.mathematica.parsing.psi.api.FunctionCall;
39+
import de.halirutan.mathematica.parsing.psi.util.Comments;
40+
import de.halirutan.mathematica.parsing.psi.util.Comments.CommentStyle;
3541
import de.halirutan.mathematica.parsing.psi.util.LocalizationConstruct.ConstructType;
3642
import org.jetbrains.annotations.NotNull;
3743
import org.jetbrains.annotations.Nullable;
@@ -45,6 +51,9 @@
4551
import java.util.regex.Pattern;
4652

4753
/**
54+
* Creates fold-regions from particular nodes of the AST tree. Currently, we support the folding of functions, lists,
55+
* and special "sectioning comments".
56+
*
4857
* @author patrick (26.07.15)
4958
*/
5059
public class MathematicaExpressionFoldingBuilder implements FoldingBuilder {
@@ -87,56 +96,141 @@ private void collectRegionsRecursively(@NotNull final ASTNode node,
8796
if (matcher.end() - matcher.start() > 3) {
8897
final String key = symbol.substring(matcher.start() + 2, matcher.end() - 1);
8998
final TextRange nodeRange = node.getTextRange();
90-
final TextRange range = TextRange.create(nodeRange.getStartOffset()+matcher.start(), nodeRange.getStartOffset() + matcher.end());
99+
final TextRange range = TextRange.create(nodeRange.getStartOffset() + matcher.start(), nodeRange.getStartOffset() + matcher.end());
91100
if (ourNamedCharacters.containsKey(key)) {
92101
descriptors.add(new MathematicaNamedFoldingDescriptor(node, range, null, ourNamedCharacters.get(key), false));
93102
}
94103
}
95104
}
96105
} else if (foldCharacters && elementType == MathematicaElementTypes.STRING_NAMED_CHARACTER) {
97106
final String name = node.getText();
98-
final String key = name.substring(2,name.length()-1);
107+
final String key = name.substring(2, name.length() - 1);
99108
if (ourNamedCharacters.containsKey(key)) {
100109
descriptors.add(new MathematicaNamedFoldingDescriptor(node, node.getTextRange(), null, ourNamedCharacters.get(key), false));
101110
}
102-
} else if(elementType==MathematicaElementTypes.LIST_EXPRESSION) {
111+
} else if (elementType == MathematicaElementTypes.LIST_EXPRESSION) {
103112
// Well, we count the number of elements by counting the commas and adding one. Not bullet-proof, but will do.
104-
final int numberOfListElements = node.getChildren(TokenSet.create(MathematicaElementTypes.COMMA)).length + 1;
105-
descriptors.add(new NamedFoldingDescriptor(
106-
node,
107-
node.getTextRange(),
108-
null,
109-
"{ <<" + numberOfListElements + ">> }"));
113+
final int numberOfListElements = node.getChildren(TokenSet.create(MathematicaElementTypes.COMMA)).length + 1;
114+
descriptors.add(new NamedFoldingDescriptor(
115+
node,
116+
node.getTextRange(),
117+
null,
118+
"{ <<" + numberOfListElements + ">> }"));
119+
} else if (elementType == MathematicaElementTypes.FUNCTION_CALL_EXPRESSION) {
120+
final PsiElement psi = node.getPsi();
121+
if (psi instanceof FunctionCall) {
122+
final FunctionCall functionCall = (FunctionCall) psi;
123+
if (functionCall.getScopingConstruct() != ConstructType.NULL) {
124+
descriptors.add(new NamedFoldingDescriptor(
125+
node,
126+
node.getTextRange(),
127+
null,
128+
functionCall.getHead().getText() + "[...]"
129+
));
130+
}
131+
}
132+
} else if (node instanceof PsiComment) {
133+
collectCommentRegion(node, document, descriptors);
134+
}
135+
136+
for (ASTNode child : node.getChildren(null)) {
137+
collectRegionsRecursively(child, document, descriptors);
138+
}
139+
110140
}
111141

112-
else if(elementType==MathematicaElementTypes.FUNCTION_CALL_EXPRESSION)
113-
114-
{
115-
final PsiElement psi = node.getPsi();
116-
if (psi instanceof FunctionCall) {
117-
final FunctionCall functionCall = (FunctionCall) psi;
118-
if (functionCall.getScopingConstruct() != ConstructType.NULL) {
119-
descriptors.add(new NamedFoldingDescriptor(
120-
node,
121-
node.getTextRange(),
122-
null,
123-
functionCall.getHead().getText() + "[...]"
124-
));
142+
/**
143+
* Entry method that only check if we have a valid section-comment. The work is done in
144+
* {@link MathematicaExpressionFoldingBuilder#collectCommentRegion(PsiComment, Document, List)}
145+
*
146+
* @param node node to a PsiComment
147+
* @param document the document of the code
148+
* @param descriptors collects all folding descriptors
149+
*/
150+
private void collectCommentRegion(@NotNull final ASTNode node,
151+
@NotNull final Document document,
152+
@NotNull List<FoldingDescriptor> descriptors) {
153+
final PsiComment psi = (PsiComment) node.getPsi();
154+
boolean isAtTopLevel = false;
155+
if (psi.getParent() instanceof PsiFile) {
156+
isAtTopLevel = true;
157+
} else {
158+
final PsiElement parent = psi.getParent();
159+
if (parent instanceof CompoundExpression && parent.getParent() instanceof PsiFile) {
160+
isAtTopLevel = true;
125161
}
126162
}
163+
164+
if (isAtTopLevel && Comments.isCorrectSectionComment(psi)) {
165+
final CommentStyle style = Comments.getStyle(psi);
166+
if (style != null && style.compareTo(CommentStyle.SUBSUBSUBSECTION) < 0) {
167+
collectCommentRegion(node, psi, style, document, descriptors);
168+
}
169+
}
170+
127171
}
128172

173+
/**
174+
* Here the work is done for finding the correct region to fold for a sectioning-comment. The sectioning comments like
175+
* <code>
176+
* (* ::Chapter:: *)
177+
* (*this is the text of the chapter*)
178+
* </code>
179+
*
180+
* @param node AST node of the comment
181+
* @param comment the PSI element of node
182+
* @param style Extracted style of the sectioning comment of node
183+
* @param document The document that contains the comment
184+
* @param descriptors Collection for the region descriptors for folding
185+
*/
186+
private void collectCommentRegion(@NotNull ASTNode node,
187+
@NotNull final PsiComment comment,
188+
@NotNull final CommentStyle style,
189+
@NotNull final Document document,
190+
@NotNull List<FoldingDescriptor> descriptors) {
191+
final PsiFile file = comment.getContainingFile();
192+
StringBuilder placeHolderText = new StringBuilder("<< ::");
193+
placeHolderText.append(style).append(":: ");
194+
195+
final int currentLine = document.getLineNumber(comment.getTextOffset());
196+
int endOffset = document.getTextLength();
197+
198+
for (int i = currentLine + 1; i < document.getLineCount(); i++) {
199+
int start = document.getLineStartOffset(i);
200+
int end = document.getLineEndOffset(i);
201+
final PsiComment commentsInLine = PsiTreeUtil.findElementOfClassAtRange(file, start, end, PsiComment.class);
202+
if (commentsInLine != null && Comments.isCorrectSectionComment(commentsInLine)) {
203+
final CommentStyle commentStyle = Comments.getStyle(commentsInLine);
204+
if (commentStyle != null && commentStyle.compareTo(style) <= 0) {
205+
endOffset = start - 1;
206+
if (i < document.getLineCount() - 1) {
207+
final PsiComment nextLineComment = PsiTreeUtil.findElementOfClassAtRange(
208+
file,
209+
document.getLineStartOffset(i + 1),
210+
document.getLineEndOffset(i + 1),
211+
PsiComment.class
212+
);
213+
if (nextLineComment != null && !Comments.isCorrectSectionComment(nextLineComment)) {
214+
placeHolderText.append(Comments.getStrippedText(nextLineComment));
215+
} else {
216+
placeHolderText.append("No description given");
217+
}
218+
}
219+
break;
220+
}
221+
}
222+
}
223+
placeHolderText.append(">>");
224+
descriptors.add(new NamedFoldingDescriptor(
225+
node,
226+
TextRange.create(node.getStartOffset(), endOffset),
227+
null,
228+
placeHolderText.toString()
229+
));
129230

130-
for(
131-
ASTNode child
132-
:node.getChildren(null))
133231

134-
{
135-
collectRegionsRecursively(child, document, descriptors);
136232
}
137233

138-
}
139-
140234
@Nullable
141235
@Override
142236
public String getPlaceholderText(@NotNull final ASTNode node) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2017 Patrick Scheibe
3+
* Permission is hereby granted, free of charge, to any person obtaining a copy
4+
* of this software and associated documentation files (the "Software"), to deal
5+
* in the Software without restriction, including without limitation the rights
6+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
* copies of the Software, and to permit persons to whom the Software is
8+
* furnished to do so, subject to the following conditions:
9+
*
10+
* The above copyright notice and this permission notice shall be included in
11+
* all copies or substantial portions of the Software.
12+
*
13+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
* THE SOFTWARE.
20+
*/
21+
22+
/**
23+
* Provides region folding. That is that some expressions or constructs in the code can be folded together so that they
24+
* don't occupy space. This is handy if you have long lists or Modules. While the implementation for functions and lists
25+
* is simple, there are two special cases.
26+
* <p>
27+
* First, we provide small fold-regions for Mathematica's special characters like \[Alpha]. If folded, those special
28+
* characters are displayed as their UTF8 counterpart which makes the code more readable. I strongly advise against the
29+
* massive use of such things in normal code which should be easily readable as ASCII text, but especially in GUI
30+
* elements a nice rendering inside Mathematica is often required and in such situations the named-character folding
31+
* becomes very handy.
32+
* <p>
33+
* The second, more complex folding regions we support are Mathematica's special sectioning-comments. The sectioning
34+
* comments like
35+
* <p>
36+
* <code> (* ::Chapter:: *)<br> (*this is the text of the chapter*) </code>
37+
* <p>
38+
* should always contain the "text" as separate comment directly in the next line. Mathematica is very strict about the
39+
* sectioning comments and they always need EXACTLY one space around the style specifier (here ::Chapter::)! If you
40+
* don't follow this rule, then it won't be rendered appropriately inside the Mathematica front end. The best approach
41+
* to create a valid section-comment is to use Ctrl+/ to create a new empty (**) followed by pressing Ctrl+Space to get
42+
* a completion for all possible Chapter, Section, Subsection, Item, etc. Note that correct section comments are always
43+
* displayed in bold-face and slightly brighter than other comments.
44+
* <p>
45+
* The folding of such comments takes care of the "level", meaning if you fold a Section, it will always take the region
46+
* to the next Section or any element that has a higher level like "Chapter" or "Subchapter". If you have several
47+
* Subsections between two sections, then you can (a) fold each Subsection separately.
48+
* <p>
49+
* Finally, section-comments can contain some style specifiers, namely ::Closed::, ::Bold::, ::Italic::. Here,
50+
* ::Closed:: is maybe the only really useful because it indicates that a section should be displayed as closed when
51+
* opened in Mathematica. Note that it has no influence of the folding! A correct example is for instance <code> (*
52+
* ::Text::Bold::Italic:: *) </code>
53+
* <p>
54+
* The supported section-comments can be found in {@link de.halirutan.mathematica.parsing.psi.util.Comments.CommentStyle}.
55+
*/
56+
package de.halirutan.mathematica.codeinsight.folding;

src/de/halirutan/mathematica/codeinsight/highlighting/CommentAnnotator.java

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -27,37 +27,26 @@
2727
import com.intellij.openapi.util.TextRange;
2828
import com.intellij.psi.PsiComment;
2929
import com.intellij.psi.PsiElement;
30-
import de.halirutan.mathematica.codeinsight.completion.CommentCompletionProvider;
31-
import org.apache.commons.lang.StringUtils;
30+
import de.halirutan.mathematica.parsing.psi.util.Comments;
3231
import org.jetbrains.annotations.NotNull;
3332

34-
import java.util.ArrayList;
35-
import java.util.List;
3633
import java.util.regex.Matcher;
3734
import java.util.regex.Pattern;
3835

3936

4037
/**
38+
* Provides bold and bright highlighting for tags and sectioning comments.
4139
* @author patrick (03.03.15)
4240
*/
4341
public class CommentAnnotator implements Annotator {
4442

45-
private static final Pattern ourTagPattern;
46-
47-
static {
48-
List<String> commentTags = new ArrayList<>(CommentCompletionProvider.COMMENT_TAGS.length);
49-
for (String tag : CommentCompletionProvider.COMMENT_TAGS) {
50-
commentTags.add(":" + tag + ":");
51-
}
52-
ourTagPattern = Pattern.compile(StringUtils.join(commentTags, "|"));
53-
}
54-
43+
private static final Pattern ourTagPattern = Pattern.compile("[^:]:\\w+:[^:]");
5544

5645
@Override
5746
public void annotate(@NotNull final PsiElement element, @NotNull final AnnotationHolder holder) {
5847
if (element instanceof PsiComment) {
5948
final Annotation commentAnnotation = holder.createInfoAnnotation(element, null);
60-
if (isCorrectSectionComment(element)) {
49+
if (Comments.isCorrectSectionComment((PsiComment) element)) {
6150
commentAnnotation.setTextAttributes(MathematicaSyntaxHighlighterColors.COMMENT_SPECIAL);
6251
return;
6352
}
@@ -76,47 +65,4 @@ private void annotateCommentTags(@NotNull final PsiElement comment, @NotNull fin
7665
tagAnnotation.setTextAttributes(MathematicaSyntaxHighlighterColors.COMMENT_SPECIAL);
7766
}
7867
}
79-
80-
/**
81-
* Tests if a comment is a valid Section, Subsection, ... comment. AFAIK these comments can only contain the section
82-
* specifier and nothing else (whitespace should be OK). Therefore, I match comments that look like this
83-
* <p>
84-
* <code>(* ::Section:: *)</code>
85-
* <p>
86-
* Please see {@link CommentCompletionProvider#COMMENT_SECTIONS}.
87-
*
88-
* @param comment the comment PsiElement
89-
* @return true if the comment is a valid title, section, ... comment
90-
*/
91-
private boolean isCorrectSectionComment(@NotNull final PsiElement comment) {
92-
final String text = comment.getText();
93-
final String name = text.replace("(*", "").replace("*)", "").trim();
94-
if (name.length() > 0 && name.matches("::.*::")) {
95-
final String sectionName = name.replace(":", "");
96-
for (String commentSection : CommentCompletionProvider.COMMENT_SECTIONS) {
97-
if (commentSection.equals(sectionName)) {
98-
return true;
99-
}
100-
}
101-
}
102-
return false;
103-
}
104-
105-
/**
106-
* This is not as strict as {@link CommentAnnotator#isCorrectSectionComment(PsiElement)} and it does only check
107-
* if there is a section tag (::Section::, ::Subsection::, ...) inside the comment.
108-
*
109-
* @param comment Comment to check
110-
* @return true if a section tag could be found inside the comment
111-
*/
112-
private boolean containsSectionTag(@NotNull final PsiElement comment) {
113-
final String text = comment.getText();
114-
for (String commentSection : CommentCompletionProvider.COMMENT_SECTIONS) {
115-
if (text.contains("::" + commentSection + "::")) {
116-
return true;
117-
}
118-
}
119-
return false;
120-
}
121-
12268
}

0 commit comments

Comments
 (0)