PY-81471 Move lang.foldingBuilder to common

GitOrigin-RevId: 3c4f9cc773d14492e9fbf026f0472a908c9262ec
This commit is contained in:
Petr
2025-06-18 20:02:26 +00:00
committed by intellij-monorepo-bot
parent a21d6414d8
commit 685695021f
10 changed files with 71 additions and 64 deletions
@@ -24,6 +24,8 @@
<statementUpDownMover implementation="com.jetbrains.python.codeInsight.editorActions.moveUpDown.PyStatementMover" id="pyStatementMover"
order="before line"/>
<joinLinesHandler implementation="com.jetbrains.python.editor.PyJoinLinesHandler"/>
<lang.foldingBuilder language="Python" implementationClass="com.jetbrains.python.PythonFoldingBuilder"/>
<applicationService serviceImplementation="com.jetbrains.python.PythonFoldingSettings"/>
</extensions>
<extensions defaultExtensionNs="Pythonid">
<pyAnnotator implementation="com.jetbrains.python.validation.PyHighlightingAnnotator"/>
@@ -0,0 +1,291 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python;
import com.intellij.codeInsight.folding.CodeFoldingSettings;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.CustomFoldingBuilder;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.FoldingGroup;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.jetbrains.python.ast.*;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class PythonFoldingBuilder extends CustomFoldingBuilder implements DumbAware {
public static final TokenSet FOLDABLE_COLLECTIONS_LITERALS = TokenSet.create(
PyElementTypes.SET_LITERAL_EXPRESSION,
PyElementTypes.DICT_LITERAL_EXPRESSION,
PyElementTypes.GENERATOR_EXPRESSION,
PyElementTypes.SET_COMP_EXPRESSION,
PyElementTypes.DICT_COMP_EXPRESSION,
PyElementTypes.LIST_LITERAL_EXPRESSION,
PyElementTypes.LIST_COMP_EXPRESSION,
PyElementTypes.TUPLE_EXPRESSION);
public static final String PYTHON_TYPE_ANNOTATION_GROUP_NAME = "Python type annotation";
@Override
protected void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors,
@NotNull PsiElement root,
@NotNull Document document,
boolean quick) {
appendDescriptors(root.getNode(), descriptors);
}
private static void appendDescriptors(ASTNode node, List<FoldingDescriptor> descriptors) {
IElementType elementType = node.getElementType();
if (node.getPsi() instanceof PyAstFile) {
final List<? extends PyAstImportStatementBase> imports = ((PyAstFile)node.getPsi()).getImportBlock();
if (imports.size() > 1) {
final PyAstImportStatementBase firstImport = imports.get(0);
final PyAstImportStatementBase lastImport = imports.get(imports.size()-1);
descriptors.add(new FoldingDescriptor(firstImport, new TextRange(firstImport.getTextRange().getStartOffset(),
lastImport.getTextRange().getEndOffset())));
}
}
else if (elementType == PyElementTypes.MATCH_STATEMENT) {
foldMatchStatement(node, descriptors);
}
else if (elementType == PyElementTypes.STATEMENT_LIST) {
foldStatementList(node, descriptors);
}
else if (elementType == PyElementTypes.STRING_LITERAL_EXPRESSION) {
foldLongStrings(node, descriptors);
}
else if (FOLDABLE_COLLECTIONS_LITERALS.contains(elementType)) {
foldCollectionLiteral(node, descriptors);
}
else if (elementType == PyTokenTypes.END_OF_LINE_COMMENT) {
foldSequentialComments(node, descriptors);
}
else if (elementType == PyElementTypes.ANNOTATION) {
var annotation = node.getPsi();
if (annotation instanceof PyAstAnnotation pyAnnotation && pyAnnotation.getValue() != null) {
descriptors.add(new FoldingDescriptor(node, pyAnnotation.getValue().getTextRange(),
FoldingGroup.newGroup(PYTHON_TYPE_ANNOTATION_GROUP_NAME)));
}
}
ASTNode child = node.getFirstChildNode();
while (child != null) {
appendDescriptors(child, descriptors);
child = child.getTreeNext();
}
}
private static void foldSequentialComments(ASTNode node, List<FoldingDescriptor> descriptors) {
//do not start folded comments from custom region
if (isCustomRegionElement(node.getPsi())) {
return;
}
//need to skip previous comments in sequence
ASTNode curNode = node.getTreePrev();
while (curNode != null) {
if (curNode.getElementType() == PyTokenTypes.END_OF_LINE_COMMENT) {
if (isCustomRegionElement(curNode.getPsi())) {
break;
}
return;
}
curNode = curNode.getPsi() instanceof PsiWhiteSpace ? curNode.getTreePrev() : null;
}
//fold sequence comments in one block
curNode = node.getTreeNext();
ASTNode lastCommentNode = node;
while (curNode != null) {
if (curNode.getElementType() == PyTokenTypes.END_OF_LINE_COMMENT) {
//do not end folded comments with custom region
if (isCustomRegionElement(curNode.getPsi())) {
break;
}
lastCommentNode = curNode;
curNode = curNode.getTreeNext();
continue;
}
curNode = curNode.getPsi() instanceof PsiWhiteSpace ? curNode.getTreeNext() : null;
}
if (lastCommentNode != node) {
descriptors.add(new FoldingDescriptor(node, TextRange.create(node.getStartOffset(), lastCommentNode.getTextRange().getEndOffset())));
}
}
private static void foldCollectionLiteral(ASTNode node, List<FoldingDescriptor> descriptors) {
if (StringUtil.countNewLines(node.getChars()) > 0) {
TextRange range = node.getTextRange();
int delta = node.getElementType() == PyElementTypes.TUPLE_EXPRESSION ? 0 : 1;
descriptors.add(new FoldingDescriptor(node, TextRange.create(range.getStartOffset() + delta, range.getEndOffset() - delta)));
}
}
private static void foldMatchStatement(@NotNull ASTNode node, @NotNull List<FoldingDescriptor> descriptors) {
TextRange nodeRange = node.getTextRange();
if (nodeRange.isEmpty()) {
return;
}
IElementType elType = node.getElementType();
if (elType == PyElementTypes.MATCH_STATEMENT) {
ASTNode colon = node.findChildByType(PyTokenTypes.COLON);
foldSegment(node, descriptors, nodeRange, colon);
}
}
private static void foldSegment(@NotNull ASTNode node, @NotNull List<FoldingDescriptor> descriptors, @NotNull TextRange nodeRange, @Nullable ASTNode colon) {
int nodeEnd = nodeRange.getEndOffset();
if (colon != null && nodeEnd - (colon.getStartOffset() + 1) > 1) {
CharSequence chars = node.getChars();
int nodeStart = nodeRange.getStartOffset();
int foldStart = colon.getStartOffset() + 1;
int foldEnd = nodeEnd;
while (foldEnd > Math.max(nodeStart, foldStart + 1) && Character.isWhitespace(chars.charAt(foldEnd - nodeStart - 1))) {
foldEnd--;
}
descriptors.add(new FoldingDescriptor(node, new TextRange(foldStart, foldEnd)));
}
else if (nodeRange.getLength() > 1) { // only for ranges at least 1 char wide
descriptors.add(new FoldingDescriptor(node, nodeRange));
}
}
private static void foldStatementList(ASTNode node, List<FoldingDescriptor> descriptors) {
final TextRange nodeRange = node.getTextRange();
if (nodeRange.isEmpty()) {
return;
}
final IElementType elType = node.getTreeParent().getElementType();
if (elType == PyElementTypes.FUNCTION_DECLARATION || elType == PyElementTypes.CLASS_DECLARATION || checkFoldBlocks(node, elType)) {
ASTNode colon = node.getTreeParent().findChildByType(PyTokenTypes.COLON);
foldSegment(node, descriptors, nodeRange, colon);
}
}
private static boolean checkFoldBlocks(@NotNull ASTNode statementList, @NotNull IElementType parentType) {
PsiElement element = statementList.getPsi();
assert element instanceof PyAstStatementList;
return PyElementTypes.PARTS.contains(parentType) ||
parentType == PyElementTypes.WITH_STATEMENT ||
parentType == PyElementTypes.CASE_CLAUSE;
}
private static void foldLongStrings(ASTNode node, List<FoldingDescriptor> descriptors) {
//don't want to fold docstrings like """\n string \n """
boolean shouldFoldDocString = getDocStringOwnerType(node) != null && StringUtil.countNewLines(node.getChars()) > 1;
boolean shouldFoldString = getDocStringOwnerType(node) == null && StringUtil.countNewLines(node.getChars()) > 0;
if (shouldFoldDocString || shouldFoldString) {
descriptors.add(new FoldingDescriptor(node, node.getTextRange()));
}
}
private static @Nullable IElementType getDocStringOwnerType(ASTNode node) {
final ASTNode treeParent = node.getTreeParent();
IElementType parentType = treeParent.getElementType();
if (parentType == PyElementTypes.EXPRESSION_STATEMENT && treeParent.getTreeParent() != null) {
final ASTNode parent2 = treeParent.getTreeParent();
if (parent2.getElementType() == PyElementTypes.STATEMENT_LIST && parent2.getTreeParent() != null && treeParent == parent2.getFirstChildNode()) {
final ASTNode parent3 = parent2.getTreeParent();
if (parent3.getElementType() == PyElementTypes.FUNCTION_DECLARATION || parent3.getElementType() == PyElementTypes.CLASS_DECLARATION) {
return parent3.getElementType();
}
}
else if (parent2 instanceof PyAstFile) {
return parent2.getElementType();
}
}
return null;
}
@Override
protected String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
if (isImport(node)) {
return "import ...";
}
if (node.getElementType() == PyElementTypes.STRING_LITERAL_EXPRESSION) {
PyAstStringLiteralExpression stringLiteralExpression = (PyAstStringLiteralExpression)node.getPsi();
String prefix = stringLiteralExpression.getStringElements().get(0).getPrefix();
if (stringLiteralExpression.isDocString()) {
final String stringValue = stringLiteralExpression.getStringValue().trim();
final String[] lines = LineTokenizer.tokenize(stringValue, true);
if (lines.length > 2 && lines[1].trim().isEmpty()) {
return prefix + "\"\"\"" + lines[0].trim() + "...\"\"\"";
}
return prefix + "\"\"\"...\"\"\"";
} else {
return prefix + getLanguagePlaceholderForString(stringLiteralExpression);
}
}
return "...";
}
private static String getLanguagePlaceholderForString(PyAstStringLiteralExpression stringLiteralExpression) {
String stringText = stringLiteralExpression.getText();
Pair<String, String> quotes = PyStringLiteralCoreUtil.getQuotes(stringText);
if (quotes != null) {
return quotes.second + "..." + quotes.second;
}
return "...";
}
@Override
protected boolean isRegionCollapsedByDefault(@NotNull ASTNode node) {
if (isImport(node)) {
return CodeFoldingSettings.getInstance().COLLAPSE_IMPORTS;
}
var elementType = node.getElementType();
if (elementType == PyElementTypes.STRING_LITERAL_EXPRESSION) {
if (getDocStringOwnerType(node) == PyElementTypes.FUNCTION_DECLARATION && CodeFoldingSettings.getInstance().COLLAPSE_METHODS) {
// method will be collapsed, no need to also collapse docstring
return false;
}
if (getDocStringOwnerType(node) != null) {
return CodeFoldingSettings.getInstance().COLLAPSE_DOC_COMMENTS;
}
return PythonFoldingSettings.getInstance().isCollapseLongStrings();
}
if (elementType == PyTokenTypes.END_OF_LINE_COMMENT) {
return PythonFoldingSettings.getInstance().isCollapseSequentialComments();
}
if (elementType == PyElementTypes.ANNOTATION) {
return PythonFoldingSettings.getInstance().isCollapseTypeAnnotations();
}
if (elementType == PyElementTypes.STATEMENT_LIST && node.getTreeParent().getElementType() == PyElementTypes.FUNCTION_DECLARATION) {
return CodeFoldingSettings.getInstance().COLLAPSE_METHODS;
}
if (FOLDABLE_COLLECTIONS_LITERALS.contains(elementType)) {
return PythonFoldingSettings.getInstance().isCollapseLongCollections();
}
return false;
}
@Override
protected boolean isCustomFoldingCandidate(@NotNull ASTNode node) {
return node.getElementType() == PyTokenTypes.END_OF_LINE_COMMENT;
}
@Override
protected boolean isCustomFoldingRoot(@NotNull ASTNode node) {
return node.getPsi() instanceof PyAstFile || node.getElementType() == PyElementTypes.STATEMENT_LIST;
}
private static boolean isImport(@NotNull ASTNode node) {
return PyElementTypes.IMPORT_STATEMENTS.contains(node.getElementType());
}
}
@@ -0,0 +1,49 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.SettingsCategory;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State(name = "PythonFoldingSettings", storages = @Storage("editor.xml"), category = SettingsCategory.CODE)
public class PythonFoldingSettings implements PersistentStateComponent<PythonFoldingSettings> {
public boolean COLLAPSE_LONG_STRINGS;
public boolean COLLAPSE_LONG_COLLECTIONS;
public boolean COLLAPSE_SEQUENTIAL_COMMENTS;
public boolean COLLAPSE_TYPE_ANNOTATIONS;
@Override
public @Nullable PythonFoldingSettings getState() {
return this;
}
public static @NotNull PythonFoldingSettings getInstance() {
return ApplicationManager.getApplication().getService(PythonFoldingSettings.class);
}
@Override
public void loadState(@NotNull PythonFoldingSettings state) {
XmlSerializerUtil.copyBean(state, this);
}
public boolean isCollapseLongStrings() {
return COLLAPSE_LONG_STRINGS;
}
public boolean isCollapseLongCollections() {
return COLLAPSE_LONG_COLLECTIONS;
}
public boolean isCollapseSequentialComments() {
return COLLAPSE_SEQUENTIAL_COMMENTS;
}
public boolean isCollapseTypeAnnotations() {
return COLLAPSE_TYPE_ANNOTATIONS;
}
}