IJPL-181799 execute java move up/down statement actions on FE in RD

GitOrigin-RevId: 5df74728054974e44f2664018c3535c16c24927d
This commit is contained in:
Dmitry Batkovich
2025-04-24 13:24:03 +00:00
committed by intellij-monorepo-bot
parent 5ad848e379
commit de7d4de1c0
10 changed files with 213 additions and 29 deletions
@@ -67,6 +67,15 @@
<applicationService serviceInterface="com.intellij.codeInsight.folding.JavaCodeFoldingSettings"
serviceImplementation="com.intellij.codeInsight.folding.impl.JavaCodeFoldingSettingsImpl"/>
<statementUpDownMover implementation="com.intellij.codeInsight.editorActions.moveUpDown.JavaDeclarationMover" id="declaration"
order="before xml"/>
<statementUpDownMover implementation="com.intellij.codeInsight.editorActions.moveUpDown.JavaStatementMover" id="statement"
order="before declaration"/>
<statementUpDownMover implementation="com.intellij.codeInsight.editorActions.moveUpDown.JavaCaseBlockMover" id="caseBlock"
order="before statement"/>
<statementUpDownMover implementation="com.intellij.codeInsight.editorActions.moveUpDown.JavaCatchBlockMover" id="catchBlock"
order="before statement"/>
<registryKey key="java.formatter.chained.calls.pre212.compatibility"
defaultValue="false"
description="Format chained calls as in versions prior to 2021.2"/>
@@ -0,0 +1,166 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight;
import com.intellij.lang.Language;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.psi.*;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.FileTypeUtils;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilCore;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Set;
public final class CodeInsightFrontbackUtil {
public static @Nullable PsiExpression findExpressionInRange(PsiFile file, int startOffset, int endOffset) {
if (!file.getViewProvider().getLanguages().contains(JavaLanguage.INSTANCE)) return null;
PsiExpression expression = findElementInRange(file, startOffset, endOffset, PsiExpression.class);
if (expression == null && findStatementsInRange(file, startOffset, endOffset).length == 0) {
PsiElement element2 = file.getViewProvider().findElementAt(endOffset - 1, JavaLanguage.INSTANCE);
if (element2 instanceof PsiJavaToken token) {
final IElementType tokenType = token.getTokenType();
if (tokenType.equals(JavaTokenType.SEMICOLON) || element2.getParent() instanceof PsiErrorElement) {
expression = findElementInRange(file, startOffset, element2.getTextRange().getStartOffset(), PsiExpression.class);
}
}
}
if (expression == null && findStatementsInRange(file, startOffset, endOffset).length == 0) {
PsiElement element = PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(endOffset));
if (element != null) {
element = PsiTreeUtil.skipWhitespacesAndCommentsBackward(element.getLastChild());
if (element != null) {
final int newEndOffset = element.getTextRange().getEndOffset();
if (newEndOffset < endOffset) {
expression = findExpressionInRange(file, startOffset, newEndOffset);
}
}
}
}
if (expression instanceof PsiReferenceExpression && expression.getParent() instanceof PsiMethodCallExpression) return null;
return expression;
}
public static <T extends PsiElement> T findElementInRange(PsiFile file, int startOffset, int endOffset, Class<T> klass) {
return CodeInsightUtilCore.findElementInRange(file, startOffset, endOffset, klass, JavaLanguage.INSTANCE);
}
public static PsiElement @NotNull [] findStatementsInRange(@NotNull PsiFile file, int startOffset, int endOffset) {
Language language = findJavaOrLikeLanguage(file);
if (language == null) return PsiElement.EMPTY_ARRAY;
FileViewProvider viewProvider = file.getViewProvider();
PsiElement element1 = viewProvider.findElementAt(startOffset, language);
PsiElement element2 = viewProvider.findElementAt(endOffset - 1, language);
if (element1 instanceof PsiWhiteSpace) {
startOffset = element1.getTextRange().getEndOffset();
element1 = file.findElementAt(startOffset);
}
if (element2 instanceof PsiWhiteSpace) {
endOffset = element2.getTextRange().getStartOffset();
element2 = file.findElementAt(endOffset - 1);
}
if (element1 == null || element2 == null) return PsiElement.EMPTY_ARRAY;
PsiElement parent = PsiTreeUtil.findCommonParent(element1, element2);
if (parent == null) return PsiElement.EMPTY_ARRAY;
while (true) {
if (parent instanceof PsiStatement) {
if (!(element1 instanceof PsiComment)) {
parent = parent.getParent();
}
break;
}
if (parent instanceof PsiCodeBlock) break;
if (FileTypeUtils.isInServerPageFile(parent) && parent instanceof PsiFile) break;
if (parent instanceof PsiCodeFragment) break;
if (parent == null || parent instanceof PsiFile) return PsiElement.EMPTY_ARRAY;
parent = parent.getParent();
}
if (!parent.equals(element1)) {
while (!parent.equals(element1.getParent())) {
element1 = element1.getParent();
}
}
if (startOffset != element1.getTextRange().getStartOffset()) return PsiElement.EMPTY_ARRAY;
if (!parent.equals(element2)) {
while (!parent.equals(element2.getParent())) {
element2 = element2.getParent();
}
}
if (endOffset != element2.getTextRange().getEndOffset() && !isAtTrailingComment(element1, element2, endOffset)) {
return PsiElement.EMPTY_ARRAY;
}
if (parent instanceof PsiCodeBlock &&
element1 == ((PsiCodeBlock)parent).getLBrace() && element2 == ((PsiCodeBlock)parent).getRBrace()) {
if (parent.getParent() instanceof PsiBlockStatement) {
return new PsiElement[]{parent.getParent()};
}
PsiElement[] children = parent.getChildren();
return getStatementsInRange(children, ((PsiCodeBlock)parent).getFirstBodyElement(), ((PsiCodeBlock)parent).getLastBodyElement());
}
PsiElement[] children = parent.getChildren();
return getStatementsInRange(children, element1, element2);
}
private static boolean isAtTrailingComment(PsiElement element1, PsiElement element2, int offset) {
if (element1 == element2 && element1 instanceof PsiExpressionStatement) {
for (PsiElement child = element1.getLastChild(); child != null; child = child.getPrevSibling()) {
if (PsiUtil.isJavaToken(child, JavaTokenType.SEMICOLON) && child.getTextRange().getEndOffset() == offset) {
return false; // findExpressionInRange() counts this as an expression - don't interfere with it
}
}
}
PsiElement trailing = element2;
while (trailing.getTextRange().contains(offset) && trailing.getLastChild() != null) {
trailing = trailing.getLastChild();
}
while (trailing instanceof PsiComment || trailing instanceof PsiWhiteSpace) {
PsiElement previous = trailing.getPrevSibling();
if (trailing.getTextRange().contains(offset)) {
return true;
}
trailing = previous;
}
return false;
}
private static @Nullable Language findJavaOrLikeLanguage(final @NotNull PsiFile file) {
final Set<Language> languages = file.getViewProvider().getLanguages();
if (languages.contains(JavaLanguage.INSTANCE)) return JavaLanguage.INSTANCE;
for (final Language language : languages) {
if (language.isKindOf(JavaLanguage.INSTANCE)) return language;
}
return null;
}
private static PsiElement @NotNull [] getStatementsInRange(PsiElement[] children, PsiElement element1, PsiElement element2) {
ArrayList<PsiElement> array = new ArrayList<>();
boolean flag = false;
for (PsiElement child : children) {
if (child.equals(element1)) {
flag = true;
}
if (flag && !(child instanceof PsiWhiteSpace)) {
array.add(child);
}
if (child.equals(element2)) {
break;
}
}
for (PsiElement element : array) {
if (!(element instanceof PsiStatement || element instanceof PsiWhiteSpace || element instanceof PsiComment)) {
return PsiElement.EMPTY_ARRAY;
}
}
return PsiUtilCore.toPsiElementArray(array);
}
}
@@ -0,0 +1,108 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.editorActions.moveUpDown;
import com.intellij.codeInsight.CodeInsightFrontbackUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiSwitchLabelStatement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.SmartList;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public final class JavaCaseBlockMover extends LineMover {
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
if (!(file instanceof PsiJavaFile)) return false;
if (!super.checkAvailable(editor, file, info, down)) return false;
final Document document = editor.getDocument();
int startOffset = document.getLineStartOffset(info.toMove.startLine);
int endOffset = getLineStartSafeOffset(document, info.toMove.endLine);
List<PsiSwitchLabelStatement> statements = new SmartList<>();
PsiElement firstElement = null;
for (PsiElement element : CodeInsightFrontbackUtil.findStatementsInRange(file, startOffset, endOffset)) {
if (element instanceof PsiSwitchLabelStatement) {
statements.add((PsiSwitchLabelStatement)element);
}
else if (statements.isEmpty()) {
firstElement = element;
}
}
if (statements.isEmpty()) return false;
if (firstElement != null) return info.prohibitMove(); // nonsensical selection
PsiSwitchLabelStatement firstToMove = getThisCaseBlockStart(statements.get(0));
PsiSwitchLabelStatement lastStatement = statements.get(statements.size() - 1);
PsiElement nextCaseBlockStart = getNextCaseBlockStart(lastStatement);
PsiElement lastToMove = PsiTreeUtil.skipWhitespacesBackward(nextCaseBlockStart);
assert lastToMove != null;
LineRange range = createRange(document, firstToMove, lastToMove);
if (range == null) return info.prohibitMove();
info.toMove = range;
PsiElement firstToMove2;
PsiElement lastToMove2;
if (down) {
if (!(nextCaseBlockStart instanceof PsiSwitchLabelStatement) || nextCaseBlockStart == lastStatement) return info.prohibitMove();
firstToMove2 = nextCaseBlockStart;
nextCaseBlockStart = getNextCaseBlockStart((PsiSwitchLabelStatement)firstToMove2);
lastToMove2 = PsiTreeUtil.skipWhitespacesBackward(nextCaseBlockStart);
assert lastToMove2 != null;
}
else {
lastToMove2 = PsiTreeUtil.skipWhitespacesBackward(firstToMove);
if (lastToMove2 == null) return info.prohibitMove();
firstToMove2 = PsiTreeUtil.getPrevSiblingOfType(lastToMove2, PsiSwitchLabelStatement.class);
if (firstToMove2 == null) return info.prohibitMove();
firstToMove2 = getThisCaseBlockStart((PsiSwitchLabelStatement)firstToMove2);
}
LineRange range2 = createRange(document, firstToMove2, lastToMove2);
if (range2 == null) return info.prohibitMove();
info.toMove2 = range2;
return true;
}
// returns PsiSwitchLabelStatement starting this case block
private static @NotNull PsiSwitchLabelStatement getThisCaseBlockStart(PsiSwitchLabelStatement element) {
PsiElement tmp;
while ((tmp = PsiTreeUtil.skipWhitespacesBackward(element)) instanceof PsiSwitchLabelStatement) {
element = (PsiSwitchLabelStatement)tmp;
}
return element;
}
// returns PsiSwitchLabelStatement starting next case block, or switch block's closing brace, if there is no next case block
private static @NotNull PsiElement getNextCaseBlockStart(PsiSwitchLabelStatement element) {
PsiElement result = element;
PsiElement tmp;
while ((tmp = PsiTreeUtil.skipWhitespacesForward(result)) instanceof PsiSwitchLabelStatement) {
result = tmp;
}
tmp = PsiTreeUtil.getNextSiblingOfType(result, PsiSwitchLabelStatement.class);
return tmp == null ? result.getParent().getLastChild() : tmp;
}
private static @Nullable LineRange createRange(@NotNull Document document, @NotNull PsiElement startElement, @NotNull PsiElement endElement) {
CharSequence text = document.getImmutableCharSequence();
int startOffset = startElement.getTextRange().getStartOffset();
int startLine = document.getLineNumber(startOffset);
if (!CharArrayUtil.isEmptyOrSpaces(text, document.getLineStartOffset(startLine), startOffset)) {
return null;
}
int endOffset = endElement.getTextRange().getEndOffset();
int endLine = document.getLineNumber(endOffset);
if (!CharArrayUtil.isEmptyOrSpaces(text, endOffset, document.getLineEndOffset(endLine))) {
return null;
}
return new LineRange(startLine, endLine + 1);
}
}
@@ -0,0 +1,87 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.editorActions.moveUpDown;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public final class JavaCatchBlockMover extends LineMover {
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
if (!(file instanceof PsiJavaFile)) return false;
if (!super.checkAvailable(editor, file, info, down)) return false;
final Document document = editor.getDocument();
final SelectionModel selectionModel = editor.getSelectionModel();
final int startOffset;
final int endOffset;
if (selectionModel.hasSelection()) {
startOffset = selectionModel.getSelectionStart();
endOffset = selectionModel.getSelectionEnd();
}
else {
startOffset = document.getLineStartOffset(info.toMove.startLine);
endOffset = getLineStartSafeOffset(document, info.toMove.endLine);
}
final PsiElement element = file.findElementAt(startOffset);
if (element == null) return false;
final PsiTryStatement tryStatement = PsiTreeUtil.getParentOfType(element, PsiTryStatement.class, true, PsiMember.class);
if (tryStatement == null) return false;
PsiCatchSection firstToMove = null;
PsiCatchSection lastToMove = null;
for (PsiCatchSection catchSection : tryStatement.getCatchSections()) {
final int offset = catchSection.getTextOffset();
final PsiElement child = catchSection.getFirstChild();
if (!(child instanceof PsiKeyword)) return info.prohibitMove();
if (offset >= startOffset && offset < endOffset || child.getTextRange().contains(startOffset)) {
if (firstToMove == null) firstToMove = catchSection;
lastToMove = catchSection;
}
}
if (firstToMove == null) return false;
if (!sanityCheck(firstToMove)) {
return info.prohibitMove();
}
if (element instanceof PsiWhiteSpace && element.getNextSibling() instanceof PsiStatement
|| PsiTreeUtil.getParentOfType(element, PsiStatement.class, true, PsiMember.class) != tryStatement) {
// nonsensical selection
return info.prohibitMove();
}
final PsiCatchSection sibling = down
? PsiTreeUtil.getNextSiblingOfType(lastToMove, PsiCatchSection.class)
: PsiTreeUtil.getPrevSiblingOfType(firstToMove, PsiCatchSection.class);
if (sibling == null) return info.prohibitMove();
info.toMove = new LineRange(firstToMove, lastToMove, document);
info.toMove2 = new LineRange(sibling, sibling, document);
if (down ? info.toMove.endLine > info.toMove2.startLine : info.toMove2.endLine > info.toMove.startLine) {
info.toMove = new LineRange(info.toMove.startLine, info.toMove.endLine - 1);
info.toMove2 = new LineRange(info.toMove2.startLine, info.toMove2.endLine - 1);
}
info.indentSource = false;
info.indentTarget = false;
return true;
}
private static boolean sanityCheck(PsiCatchSection catchSection) {
final PsiCatchSection[] catchSections = catchSection.getTryStatement().getCatchSections();
if (catchSections.length < 2) return false;
final boolean newLine = containsNewLine(catchSections[0].getPrevSibling());
for (int i = 1; i < catchSections.length; i++) {
if (newLine != containsNewLine(catchSections[i].getPrevSibling())) return false;
}
return true;
}
private static boolean containsNewLine(PsiElement element) {
return element instanceof PsiWhiteSpace && element.getText().contains("\n");
}
}
@@ -0,0 +1,350 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.editorActions.moveUpDown;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.tree.Factory;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.jsp.IJspClassLevelDeclarationStatement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
final class JavaDeclarationMover extends LineMover {
private static final Logger LOG = Logger.getInstance(JavaDeclarationMover.class);
@SuppressWarnings("StatefulEp")
private PsiEnumConstant myEnumToInsertSemicolonAfter;
private boolean moveEnumConstant;
@Override
public void beforeMove(final @NotNull Editor editor, final @NotNull MoveInfo info, final boolean down) {
super.beforeMove(editor, info, down);
if (myEnumToInsertSemicolonAfter != null) {
TreeElement semicolon = Factory.createSingleLeafElement(JavaTokenType.SEMICOLON, ";", 0, 1, null, myEnumToInsertSemicolonAfter.getManager());
try {
PsiElement inserted = myEnumToInsertSemicolonAfter.getParent().addAfter(semicolon.getPsi(), myEnumToInsertSemicolonAfter);
inserted = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(inserted);
final LogicalPosition position = editor.offsetToLogicalPosition(inserted.getTextRange().getEndOffset());
info.toMove2 = new LineRange(position.line + 1, position.line + 1);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
finally {
myEnumToInsertSemicolonAfter = null;
}
}
}
@Override
public void afterMove(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
super.afterMove(editor, file, info, down);
if (moveEnumConstant) {
final Document document = editor.getDocument();
final CharSequence cs = document.getCharsSequence();
int end1 = info.range1.getEndOffset();
char c1 = cs.charAt(--end1);
while (Character.isWhitespace(c1)) {
c1 = cs.charAt(--end1);
}
int end2 = info.range2.getEndOffset();
char c2 = cs.charAt(--end2);
while (Character.isWhitespace(c2)) {
c2 = cs.charAt(--end2);
}
if (c1 == c2 || !contains(info.range1, end1) || !contains(info.range2, end2)) {
return;
}
if (c1 == ',' || c1 == ';') {
document.deleteString(end1, end1 + 1);
if (end1 < end2) {
end1--;
end2--;
}
document.insertString(end2 + 1, String.valueOf(c1));
}
if (c2 == ',' || c2 == ';'){
document.deleteString(end2, end2 + 1);
if (end2 < end1) end1--;
document.insertString(end1 + 1, String.valueOf(c2));
}
}
}
private static boolean contains(RangeMarker rangeMarker, int index) {
return rangeMarker.getStartOffset() <= index && rangeMarker.getEndOffset() >= index;
}
@Override
public boolean checkAvailable(final @NotNull Editor editor, final @NotNull PsiFile file, final @NotNull MoveInfo info, final boolean down) {
if (!(file instanceof PsiJavaFile)) {
return false;
}
boolean available = super.checkAvailable(editor, file, info, down);
if (!available) return false;
final Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, info.toMove);
if (psiRange == null) return false;
final PsiMember firstMember = PsiTreeUtil.getParentOfType(psiRange.getFirst(), PsiMember.class, false);
PsiElement endElement = psiRange.getSecond();
if (firstMember instanceof PsiEnumConstant && endElement instanceof PsiJavaToken) {
final IElementType tokenType = ((PsiJavaToken)endElement).getTokenType();
if (down && tokenType == JavaTokenType.SEMICOLON) {
return info.prohibitMove();
}
if (tokenType == JavaTokenType.COMMA || tokenType == JavaTokenType.SEMICOLON) {
endElement = PsiTreeUtil.skipWhitespacesBackward(endElement);
}
}
PsiMember lastMember = PsiTreeUtil.getParentOfType(endElement, PsiMember.class, false);
if (firstMember == null || lastMember == null) return false;
if (lastMember instanceof PsiEnumConstantInitializer enumConstantInitializer) {
lastMember = enumConstantInitializer.getEnumConstant();
}
LineRange range;
if (firstMember == lastMember) {
moveEnumConstant = firstMember instanceof PsiEnumConstant;
range = memberRange(firstMember, editor, info.toMove);
if (range == null) return false;
range.firstElement = range.lastElement = firstMember;
}
else {
final PsiElement parent = PsiTreeUtil.findCommonParent(firstMember, lastMember);
if (parent == null) return false;
final Pair<PsiElement, PsiElement> combinedRange = getElementRange(parent, firstMember, lastMember);
if (combinedRange == null) return false;
final LineRange lineRange1 = memberRange(combinedRange.getFirst(), editor, info.toMove);
if (lineRange1 == null) return false;
final LineRange lineRange2 = memberRange(combinedRange.getSecond(), editor, info.toMove);
if (lineRange2 == null) return false;
range = new LineRange(lineRange1.startLine, lineRange2.endLine);
range.firstElement = combinedRange.getFirst();
range.lastElement = combinedRange.getSecond();
}
Document document = editor.getDocument();
PsiElement sibling = (down ? range.endLine >= document.getLineCount() : range.startLine == 0) ? null :
firstNonWhiteElement(down ? document.getLineStartOffset(range.endLine)
: document.getLineEndOffset(range.startLine - 1),
file, down);
if (range.lastElement instanceof PsiEnumConstant) {
if (sibling instanceof PsiJavaToken token) {
final IElementType tokenType = token.getTokenType();
if (down && tokenType == JavaTokenType.SEMICOLON) {
return info.prohibitMove();
}
if (tokenType == JavaTokenType.COMMA) {
sibling = down ?
PsiTreeUtil.skipWhitespacesForward(sibling) :
PsiTreeUtil.skipWhitespacesBackward(sibling);
}
}
else if (sibling instanceof PsiField && !(sibling instanceof PsiEnumConstant)) {
// do not move enum constant past regular field
return info.prohibitMove();
}
}
final boolean areWeMovingClass = range.firstElement instanceof PsiClass;
info.toMove = range;
int neighbourLine = down ? range.endLine : range.startLine - 1;
if (neighbourLine >= 0 && neighbourLine < document.getLineCount() &&
CharArrayUtil.containsOnlyWhiteSpaces(document.getImmutableCharSequence().subSequence(document.getLineStartOffset(neighbourLine),
document.getLineEndOffset(neighbourLine))) &&
emptyLineCanBeDeletedAccordingToCodeStyle(file, document, document.getLineEndOffset(neighbourLine))) {
info.toMove2 = new LineRange(neighbourLine, neighbourLine + 1);
}
else {
try {
LineRange intraClassRange = moveInsideOutsideClassPosition(editor, sibling, down, areWeMovingClass);
if (intraClassRange == null) {
Couple<LineRange> splitRange = extractCommentRange(sibling);
info.toMove2 = splitRange.first.startLine == splitRange.first.endLine || !down ? splitRange.second : splitRange.first;
if (down && sibling.getNextSibling() == null) return false;
}
else {
info.toMove2 = intraClassRange;
}
if (down ? info.toMove2.startLine < info.toMove.endLine : info.toMove2.endLine > info.toMove.startLine) {
return false;
}
}
catch (IllegalMoveException e) {
info.prohibitMove();
}
}
return true;
}
private static boolean emptyLineCanBeDeletedAccordingToCodeStyle(PsiFile file, Document document, int offset) {
CharSequence text = document.getImmutableCharSequence();
String whitespace = " \t\n";
int whitespaceStartOffset = CharArrayUtil.shiftBackward(text, offset - 1, whitespace) + 1;
int whitespaceEndOffset = CharArrayUtil.shiftForward(text, offset, whitespace);
int minLineFeeds = CodeStyleManager.getInstance(file.getProject()).getMinLineFeeds(file, whitespaceEndOffset);
int actualLineFeeds = StringUtil.countNewLines(text.subSequence(whitespaceStartOffset, whitespaceEndOffset));
return actualLineFeeds > minLineFeeds;
}
private static LineRange memberRange(@NotNull PsiElement member, Editor editor, LineRange lineRange) {
final TextRange textRange = member.getTextRange();
if (editor.getDocument().getTextLength() < textRange.getEndOffset()) return null;
int startLine = editor.offsetToLogicalPosition(textRange.getStartOffset()).line;
int endLine = editor.offsetToLogicalPosition(textRange.getEndOffset()).line+1;
// if member includes a comment (non-javadoc) and it wasn't selected by user, don't move it with member
Couple<LineRange> splitRanges = extractCommentRange(member);
if (lineRange.startLine >= splitRanges.first.endLine) startLine = splitRanges.second.startLine;
else if (lineRange.endLine < splitRanges.second.startLine) endLine = splitRanges.first.endLine;
if (!isInsideDeclaration(member, startLine, endLine, lineRange, editor)) return null;
return new LineRange(startLine, endLine);
}
private static Couple<LineRange> extractCommentRange(@NotNull PsiElement member) {
PsiElement firstChild = member.getFirstChild();
PsiElement firstCoreChild = firstChild;
while (firstCoreChild instanceof PsiComment && !(firstCoreChild instanceof PsiDocComment) || firstCoreChild instanceof PsiWhiteSpace) {
firstCoreChild = firstCoreChild.getNextSibling();
}
PsiElement lastAttachedChild = PsiTreeUtil.skipWhitespacesBackward(firstCoreChild);
if (lastAttachedChild == null) {
LineRange wholeRange = new LineRange(member);
return Couple.of(new LineRange(wholeRange.startLine, wholeRange.startLine), wholeRange);
}
else {
return Couple.of(new LineRange(firstChild, lastAttachedChild), new LineRange(firstCoreChild, member));
}
}
private static boolean isInsideDeclaration(final @NotNull PsiElement member,
final int startLine,
final int endLine,
final LineRange lineRange,
final Editor editor) {
// if we positioned on member start or end we'll be able to move it
if (startLine == lineRange.startLine || startLine == lineRange.endLine || endLine == lineRange.startLine ||
endLine == lineRange.endLine) {
return true;
}
List<PsiElement> memberSuspects = new ArrayList<>();
PsiModifierList modifierList = member instanceof PsiMember ? ((PsiMember)member).getModifierList() : null;
if (modifierList != null) memberSuspects.add(modifierList);
if (member instanceof PsiClass aClass) {
if (aClass instanceof PsiAnonymousClass) return false; // move new expression instead of anon class
PsiIdentifier nameIdentifier = aClass.getNameIdentifier();
if (nameIdentifier != null) memberSuspects.add(nameIdentifier);
}
if (member instanceof PsiMethod method) {
PsiIdentifier nameIdentifier = method.getNameIdentifier();
if (nameIdentifier != null) memberSuspects.add(nameIdentifier);
PsiTypeElement returnTypeElement = method.getReturnTypeElement();
if (returnTypeElement != null) memberSuspects.add(returnTypeElement);
}
if (member instanceof PsiField field) {
PsiIdentifier nameIdentifier = field.getNameIdentifier();
memberSuspects.add(nameIdentifier);
PsiTypeElement typeElement = field.getTypeElement();
if (typeElement != null) memberSuspects.add(typeElement);
}
TextRange lineTextRange = new TextRange(editor.getDocument().getLineStartOffset(lineRange.startLine), editor.getDocument().getLineEndOffset(lineRange.endLine));
for (PsiElement suspect : memberSuspects) {
TextRange textRange = suspect.getTextRange();
if (textRange != null && lineTextRange.intersects(textRange)) return true;
}
return false;
}
private static class IllegalMoveException extends Exception {
}
// null means we are not crossing class border
// throws IllegalMoveException when corresponding movement has no sense
private @Nullable LineRange moveInsideOutsideClassPosition(Editor editor, PsiElement sibling, final boolean isDown, boolean areWeMovingClass) throws IllegalMoveException{
if (sibling == null || sibling instanceof PsiImportList) throw new IllegalMoveException();
if (PsiUtil.isJavaToken(sibling, (isDown ? JavaTokenType.RBRACE : JavaTokenType.LBRACE)) &&
sibling.getParent() instanceof PsiClass aClass) {
// moving outside class
final PsiElement parent = aClass.getParent();
if (!areWeMovingClass && !(parent instanceof PsiClass)) throw new IllegalMoveException();
if (aClass instanceof PsiAnonymousClass) throw new IllegalMoveException();
PsiElement start = isDown ? sibling : aClass.getModifierList();
return new LineRange(start, sibling, editor.getDocument());
//return isDown ? nextLineOffset(editor, aClass.getTextRange().getEndOffset()) : aClass.getTextRange().getStartOffset();
}
// trying to move up inside enum constant list, move outside enum class instead
if (!isDown
&& sibling.getParent() instanceof PsiClass aClass
&& (PsiUtil.isJavaToken(sibling, JavaTokenType.SEMICOLON) || sibling instanceof PsiErrorElement)
&& firstNonWhiteElement(sibling.getPrevSibling(), false) instanceof PsiEnumConstant) {
if (!areWeMovingClass && !(aClass.getParent() instanceof PsiClass)) throw new IllegalMoveException();
Document document = editor.getDocument();
int startLine = document.getLineNumber(aClass.getTextRange().getStartOffset());
int endLine = document.getLineNumber(sibling.getTextRange().getEndOffset()) + 1;
return new LineRange(startLine, endLine);
}
if (sibling instanceof PsiClass aClass) {
// moving inside class
if (aClass instanceof PsiAnonymousClass) throw new IllegalMoveException();
if (isDown) {
PsiElement child = aClass.getFirstChild();
if (child == null) throw new IllegalMoveException();
return new LineRange(child, aClass.isEnum() ? afterEnumConstantsPosition(aClass) : aClass.getLBrace(),
editor.getDocument());
}
else {
PsiElement rBrace = aClass.getRBrace();
if (rBrace == null) throw new IllegalMoveException();
return new LineRange(rBrace, rBrace, editor.getDocument());
}
}
if (sibling instanceof IJspClassLevelDeclarationStatement) {
// there should be another scriptlet/decl to move
if (firstNonWhiteElement(isDown ? sibling.getNextSibling() : sibling.getPrevSibling(), isDown) == null) throw new IllegalMoveException();
}
return null;
}
private PsiElement afterEnumConstantsPosition(final PsiClass aClass) {
PsiField[] fields = aClass.getFields();
for (int i = fields.length-1;i>=0; i--) {
PsiField field = fields[i];
if (field instanceof PsiEnumConstant) {
PsiElement anchor = firstNonWhiteElement(field.getNextSibling(), true);
if (!(PsiUtil.isJavaToken(anchor, JavaTokenType.SEMICOLON))) {
anchor = field;
myEnumToInsertSemicolonAfter = (PsiEnumConstant)field;
}
return anchor;
}
}
// no enum constants at all ?
return aClass.getLBrace();
}
}
@@ -0,0 +1,297 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.editorActions.moveUpDown;
import com.intellij.codeInsight.CodeInsightFrontbackUtil;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiDocumentManagerBase;
import com.intellij.psi.jsp.IJspClassLevelDeclarationStatement;
import com.intellij.psi.jsp.IJspTemplateStatement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
final class JavaStatementMover extends LineMover {
private static final Logger LOG = Logger.getInstance(JavaStatementMover.class);
private static final Key<PsiElement> STATEMENT_TO_SURROUND_WITH_CODE_BLOCK_KEY = Key.create("STATEMENT_TO_SURROUND_WITH_CODE_BLOCK_KEY");
@Override
public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) {
super.beforeMove(editor, info, down);
PsiElement statement = STATEMENT_TO_SURROUND_WITH_CODE_BLOCK_KEY.get(info);
if (statement != null) {
surroundWithCodeBlock(info, down, statement);
}
}
private static void surroundWithCodeBlock(MoveInfo info, boolean down, PsiElement statement) {
try {
Document document = PsiDocumentManager.getInstance(statement.getProject()).getDocument(statement.getContainingFile());
assert document != null : statement.getContainingFile();
int startOffset = document.getLineStartOffset(info.toMove.startLine);
int endOffset = getLineStartSafeOffset(document, info.toMove.endLine);
if (document.getText().charAt(endOffset - 1) == '\n') endOffset--;
RangeMarker lineRangeMarker = document.createRangeMarker(startOffset, endOffset);
PsiElementFactory factory = JavaPsiFacade.getElementFactory(statement.getProject());
PsiCodeBlock codeBlock = factory.createCodeBlock();
codeBlock.add(statement);
PsiBlockStatement blockStatement = (PsiBlockStatement)factory.createStatementFromText("{}", statement);
blockStatement.getCodeBlock().replace(codeBlock);
PsiBlockStatement newStatement = (PsiBlockStatement)statement.replace(blockStatement);
newStatement = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(newStatement);
info.toMove = new LineRange(document.getLineNumber(lineRangeMarker.getStartOffset()), document.getLineNumber(lineRangeMarker.getEndOffset())+1);
PsiCodeBlock newCodeBlock = newStatement.getCodeBlock();
if (down) {
PsiElement blockChild = firstNonWhiteElement(newCodeBlock.getFirstBodyElement(), true);
if (blockChild == null) blockChild = newCodeBlock.getRBrace();
assert blockChild != null : newCodeBlock;
info.toMove2 = new LineRange(info.toMove2.startLine, document.getLineNumber(blockChild.getTextRange().getStartOffset()));
}
else {
PsiJavaToken brace = newCodeBlock.getRBrace();
assert brace != null : newCodeBlock;
int start = document.getLineNumber(brace.getTextRange().getStartOffset());
int end = info.toMove.startLine;
if (start > end) end = start;
info.toMove2 = new LineRange(start, end);
}
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
boolean available = super.checkAvailable(editor, file, info, down);
if (!available) return false;
LineRange range = expandLineRangeToCoverPsiElements(info.toMove, editor, file);
if (range == null) return false;
info.toMove = range;
int startOffset = editor.logicalPositionToOffset(new LogicalPosition(range.startLine, 0));
int endOffset = editor.logicalPositionToOffset(new LogicalPosition(range.endLine, 0));
PsiElement[] statements = CodeInsightFrontbackUtil.findStatementsInRange(file, startOffset, endOffset);
if (statements.length == 0) return false;
range.firstElement = statements[0];
range.lastElement = statements[statements.length - 1];
if (!checkMovingInsideOutside(file, editor, info, down)) {
return info.prohibitMove();
}
return true;
}
private static int getDestLineForAnonymous(Editor editor, LineRange range, boolean down) {
int destLine = down ? range.endLine + 1 : range.startLine - 1;
if (!(range.firstElement instanceof PsiStatement)) {
return destLine;
}
PsiElement sibling = firstNonWhiteElement(down ? range.lastElement.getNextSibling() : range.firstElement.getPrevSibling(), down);
if (sibling != null) {
PsiClass aClass = PsiTreeUtil.findChildOfType(sibling, PsiClass.class, true, PsiStatement.class);
if (aClass != null && PsiTreeUtil.getParentOfType(aClass, PsiStatement.class) == sibling) {
destLine = editor.getDocument().getLineNumber(down ? sibling.getTextRange().getEndOffset() + 1 : sibling.getTextRange().getStartOffset());
}
}
return destLine;
}
private static boolean calcInsertOffset(PsiFile file, Editor editor, LineRange range, MoveInfo info, boolean down) {
int destLine = getDestLineForAnonymous(editor, range, down);
int startLine = down ? range.endLine : range.startLine - 1;
if (destLine < 0 || startLine < 0) return false;
while (true) {
int offset = editor.logicalPositionToOffset(new LogicalPosition(destLine, 0));
PsiElement element = firstNonWhiteElement(offset, file, true);
while (element != null && !(element instanceof PsiFile)) {
TextRange elementTextRange = element.getTextRange();
if (elementTextRange.isEmpty() || !elementTextRange.grown(-1).shiftRight(1).contains(offset)) {
PsiElement elementToSurround = null;
boolean found = false;
if ((element instanceof PsiStatement || element instanceof PsiComment) && statementCanBePlacedAlong(element)) {
found = true;
if (!(statementsCanBeMovedWithin(element.getParent()))) {
elementToSurround = element;
}
}
else if (PsiUtil.isJavaToken(element, JavaTokenType.RBRACE) && statementsCanBeMovedWithin(element.getParent())) {
// before code block closing brace
found = true;
}
if (found) {
STATEMENT_TO_SURROUND_WITH_CODE_BLOCK_KEY.set(info, elementToSurround);
info.toMove = range;
int endLine = destLine;
if (startLine > endLine) {
int tmp = endLine;
endLine = startLine;
startLine = tmp;
}
info.toMove2 = down ? new LineRange(startLine, endLine) : new LineRange(startLine, endLine + 1);
return true;
}
}
element = element.getParent();
}
destLine += down ? 1 : -1;
if (destLine < 0 || destLine >= editor.getDocument().getLineCount()) {
return false;
}
}
}
private static boolean statementCanBePlacedAlong(PsiElement element) {
if (element instanceof IJspTemplateStatement) {
PsiElement neighbour = element.getPrevSibling();
// we can place statement inside scriptlet only
return neighbour != null && !(neighbour instanceof IJspTemplateStatement);
}
PsiElement parent = element.getParent();
if (element instanceof PsiBlockStatement && !(parent instanceof PsiCodeBlock)) return false;
if (parent instanceof IJspClassLevelDeclarationStatement) return false;
if (statementsCanBeMovedWithin(parent)) return true;
if (parent instanceof PsiIfStatement &&
(element == ((PsiIfStatement)parent).getThenBranch() || element == ((PsiIfStatement)parent).getElseBranch())) {
return true;
}
if (parent instanceof PsiWhileStatement && element == ((PsiWhileStatement)parent).getBody()) {
return true;
}
if (parent instanceof PsiDoWhileStatement && element == ((PsiDoWhileStatement)parent).getBody()) {
return true;
}
// know nothing about that
return false;
}
private static boolean statementsCanBeMovedWithin(PsiElement parent) {
return parent instanceof PsiCodeBlock || parent instanceof PsiJavaModule;
}
private static boolean checkMovingInsideOutside(PsiFile file, Editor editor, @NotNull MoveInfo info, boolean down) {
int offset = editor.getCaretModel().getOffset();
PsiElement elementAtOffset = file.getViewProvider().findElementAt(offset, JavaLanguage.INSTANCE);
if (elementAtOffset == null) return false;
PsiElement guard = findGuard(elementAtOffset);
PsiElement brace = itIsTheClosingCurlyBraceWeAreMoving(file, editor);
if (brace != null) {
int line = editor.getDocument().getLineNumber(offset);
LineRange toMove = new LineRange(line, line + 1);
toMove.firstElement = toMove.lastElement = brace;
info.toMove = toMove;
}
// cannot move in/outside method/class/initializer/comment
if (!calcInsertOffset(file, editor, info.toMove, info, down)) return false;
int insertOffset = down ? getLineStartSafeOffset(editor.getDocument(), info.toMove2.endLine)
: editor.getDocument().getLineStartOffset(info.toMove2.startLine);
PsiElement elementAtInsertOffset = file.getViewProvider().findElementAt(insertOffset, JavaLanguage.INSTANCE);
PsiElement newGuard = findGuard(elementAtInsertOffset);
if (brace != null &&
PsiTreeUtil.getParentOfType(brace, PsiCodeBlock.class, false) != PsiTreeUtil.getParentOfType(elementAtInsertOffset, PsiCodeBlock.class, false)) {
info.indentSource = true;
}
if (newGuard == guard && isInside(insertOffset, newGuard) == isInside(offset, guard)) return true;
// moving in/out nested class is OK
if (guard instanceof PsiClass && guard.getParent() instanceof PsiClass) return true;
if (newGuard instanceof PsiClass && newGuard.getParent() instanceof PsiClass) return true;
return false;
}
private static PsiElement findGuard(PsiElement element) {
PsiElement guard = element;
do {
guard = PsiTreeUtil.getParentOfType(guard, PsiMethod.class, PsiClassInitializer.class, PsiClass.class, PsiComment.class);
}
while (guard instanceof PsiAnonymousClass);
return guard;
}
private static boolean isInside(int offset, PsiElement guard) {
if (guard == null) return false;
TextRange inside;
if (guard instanceof PsiMethod) {
PsiCodeBlock body = ((PsiMethod)guard).getBody();
inside = body != null ? body.getTextRange() : null;
}
else if (guard instanceof PsiClassInitializer) {
inside = ((PsiClassInitializer)guard).getBody().getTextRange();
}
else if (guard instanceof PsiClass) {
PsiElement left = ((PsiClass)guard).getLBrace(), right = ((PsiClass)guard).getRBrace();
inside = left != null && right != null ? new TextRange(left.getTextOffset(), right.getTextOffset()) : null;
}
else {
inside = guard.getTextRange();
}
return inside != null && inside.contains(offset);
}
private static LineRange expandLineRangeToCoverPsiElements(LineRange range, Editor editor, PsiFile file) {
Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, range);
if (psiRange == null) return null;
PsiElement parent = PsiTreeUtil.findCommonParent(psiRange.getFirst(), psiRange.getSecond());
Pair<PsiElement, PsiElement> elementRange = getElementRange(parent, psiRange.getFirst(), psiRange.getSecond());
if (elementRange == null) return null;
int endOffset = elementRange.getSecond().getTextRange().getEndOffset();
Document document = editor.getDocument();
if (endOffset > document.getTextLength()) {
LOG.assertTrue(!PsiDocumentManager.getInstance(file.getProject()).isUncommited(document));
LOG.assertTrue(PsiDocumentManagerBase.checkConsistency(file, document));
}
int endLine;
if (endOffset == document.getTextLength()) {
endLine = document.getLineCount();
}
else {
endLine = editor.offsetToLogicalPosition(endOffset).line + 1;
endLine = Math.min(endLine, document.getLineCount());
}
int startLine = Math.min(range.startLine, editor.offsetToLogicalPosition(elementRange.getFirst().getTextOffset()).line);
endLine = Math.max(endLine, range.endLine);
return new LineRange(startLine, endLine);
}
private static PsiElement itIsTheClosingCurlyBraceWeAreMoving(PsiFile file, Editor editor) {
LineRange range = getLineRangeFromSelection(editor);
if (range.endLine - range.startLine != 1) return null;
int offset = editor.getCaretModel().getOffset();
Document document = editor.getDocument();
int line = document.getLineNumber(offset);
int lineStartOffset = document.getLineStartOffset(line);
String lineText = document.getText().substring(lineStartOffset, document.getLineEndOffset(line));
if (!lineText.trim().equals("}")) return null;
return file.findElementAt(lineStartOffset + lineText.indexOf('}'));
}
}