diff --git a/plugins/yaml/resources/META-INF/plugin.xml b/plugins/yaml/resources/META-INF/plugin.xml index 66951d45fedf..13ac5761fe54 100644 --- a/plugins/yaml/resources/META-INF/plugin.xml +++ b/plugins/yaml/resources/META-INF/plugin.xml @@ -22,6 +22,8 @@ + diff --git a/plugins/yaml/src/org/jetbrains/yaml/smart/YAMLStatementMover.kt b/plugins/yaml/src/org/jetbrains/yaml/smart/YAMLStatementMover.kt new file mode 100644 index 000000000000..20c5cc0bdc30 --- /dev/null +++ b/plugins/yaml/src/org/jetbrains/yaml/smart/YAMLStatementMover.kt @@ -0,0 +1,110 @@ +// Copyright 2000-2020 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 org.jetbrains.yaml.smart + +import com.intellij.codeInsight.editorActions.moveUpDown.LineMover +import com.intellij.codeInsight.editorActions.moveUpDown.LineRange +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.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtilCore +import org.jetbrains.yaml.YAMLElementTypes +import org.jetbrains.yaml.YAMLLanguage +import org.jetbrains.yaml.psi.impl.YAMLBlockMappingImpl +import org.jetbrains.yaml.psi.impl.YAMLBlockSequenceImpl + +class YAMLStatementMover : LineMover() { + override fun checkAvailable(editor: Editor, file: PsiFile, info: MoveInfo, down: Boolean): Boolean { + if (!file.viewProvider.hasLanguage(YAMLLanguage.INSTANCE)) return false + val offset = editor.caretModel.offset + val selectionModel = editor.selectionModel + val document = editor.document + val lineNumber = document.getLineNumber(offset) + val start: Int + val end: Int + if (selectionModel.hasSelection()) { + start = selectionModel.selectionStart + val selectionEnd = selectionModel.selectionEnd + end = if (selectionEnd == 0) 0 else selectionEnd - 1 + } + else { + start = getLineStartSafeOffset(document, lineNumber) + val lineEndOffset = document.getLineEndOffset(lineNumber) + end = if (lineEndOffset == 0) 0 else lineEndOffset - 1 + } + var elementToMove1 = findNextAtOffset(file, start, document) ?: return false + var elementToMove2 = findPrevAtOffset(file, end, document) ?: return false + if (PsiTreeUtil.isAncestor(elementToMove1, elementToMove2, false)) { + elementToMove2 = elementToMove1 + } + else if (PsiTreeUtil.isAncestor(elementToMove2, elementToMove1, false)) { + elementToMove1 = elementToMove2 + } + if (elementToMove2 !== elementToMove1) { + val commonParent = PsiTreeUtil.findCommonParent(listOf(elementToMove1, elementToMove2)) + ?: return false + val moveScope = PsiTreeUtil.getNonStrictParentOfType(commonParent, YAMLBlockMappingImpl::class.java, + YAMLBlockSequenceImpl::class.java) + ?: return false + if (elementToMove1 !== moveScope) { + while (elementToMove1.parent !== moveScope) { + elementToMove1 = elementToMove1.parent ?: return false + } + } + if (elementToMove2 !== moveScope) { + while (elementToMove2.parent !== moveScope) { + elementToMove2 = elementToMove2.parent ?: return false + } + } + } + val destination = getDestinationScope(file, (if (down) elementToMove2 else elementToMove1), down) ?: return false + info.toMove = LineRange(elementToMove1, elementToMove2) + info.toMove2 = destination + info.indentTarget = false + info.indentSource = false + return true + } + + private fun findNextAtOffset(psiFile: PsiFile, beginAt: Int, document: Document): PsiElement? { + var offset = beginAt + while (offset < document.textLength) { + if (!document.charsSequence[offset].isWhitespace()) { + return psiFile.findElementAt(offset) + } + offset++ + } + return null + } + + private fun findPrevAtOffset(psiFile: PsiFile, beginAt: Int, document: Document): PsiElement? { + var offset = beginAt + while (offset >= 0) { + if (!document.charsSequence[offset].isWhitespace()) { + return psiFile.findElementAt(offset) + } + offset-- + } + return null + } + + private fun getDestinationScope(file: PsiFile, elementToMove: PsiElement, down: Boolean): LineRange? { + val document = file.viewProvider.document ?: return null + val offset = if (down) elementToMove.textRange.endOffset else elementToMove.textRange.startOffset + val lineNumber = if (down) document.getLineNumber(offset) + 1 else document.getLineNumber(offset) - 1 + if (lineNumber < 0 || lineNumber >= document.lineCount) return null + val destination = getDestinationElement(elementToMove, down) ?: return null + val startLine = document.getLineNumber(destination.textRange.startOffset) + val endLine = document.getLineNumber(destination.textRange.endOffset) + return LineRange(startLine, endLine + 1) + } + + private fun getDestinationElement(elementToMove: PsiElement, down: Boolean): PsiElement? { + var destination: PsiElement? = elementToMove + do { + destination = if (down) destination?.nextSibling else destination?.prevSibling + } + while (destination != null && YAMLElementTypes.SPACE_ELEMENTS.contains(PsiUtilCore.getElementType(destination))) + return if (destination == elementToMove) null else destination + } +} diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/editing/YAMLStatementMoverTest.kt b/plugins/yaml/testSrc/org/jetbrains/yaml/editing/YAMLStatementMoverTest.kt new file mode 100644 index 000000000000..15abc7e557dc --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/editing/YAMLStatementMoverTest.kt @@ -0,0 +1,275 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.yaml.editing + +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class YAMLStatementMoverTest : BasePlatformTestCase() { + fun testTopKeyValueUp() { + myFixture.configureByText("test.yaml", """ + top1: + child1: hi + top2: + child2: bye + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION) + //Note: move statement algorithm adds empty line to the end (if no one) + myFixture.checkResult(""" + top2: + child2: bye + top1: + child1: hi + """.trimIndent() + "\n") + } + + fun testTopKeyValueDown() { + myFixture.configureByText("test.yaml", """ + top1: + child1: hi + top2: + child2: bye + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + //Note: move statement algorithm adds empty line to the end (if no one) + myFixture.checkResult(""" + top2: + child2: bye + top1: + child1: hi + """.trimIndent() + "\n") + } + + fun testNonTopKeyValueDown() { + myFixture.configureByText("test.yaml", """ + megatop: + subtop1: + child1: hi + subtop2: + child2: bye + subtop3: + child3: hello + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + myFixture.checkResult(""" + megatop: + subtop2: + child2: bye + subtop1: + child1: hi + subtop3: + child3: hello + """.trimIndent()) + } + + fun testNonTopKeyValueUp() { + myFixture.configureByText("test.yaml", """ + megatop: + subtop1: + child1: hi + subtop2: + child2: bye + subtop3: + child3: hello + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION) + myFixture.checkResult(""" + megatop: + subtop2: + child2: bye + subtop1: + child1: hi + subtop3: + child3: hello + """.trimIndent()) + } + + fun testNonTopKeyValueDownWithSelection() { + myFixture.configureByText("test.yaml", """ + megatop: + subtop1: + child1: hi + subtop2: + child2: bye + subtop3: + child3: hello + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + //Note: move statement algorithm adds empty line to the end (if no one) + myFixture.checkResult(""" + megatop: + subtop3: + child3: hello + subtop1: + child1: hi + subtop2: + child2: bye + """.trimIndent() + "\n") + } + + fun testNonTopKeyValueUpWithSelection() { + myFixture.configureByText("test.yaml", """ + megatop: + subtop1: + child1: hi + subtop2: + child2: bye + subtop3: + child3: hello + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION) + //Note: move statement algorithm adds empty line to the end (if no one) + myFixture.checkResult(""" + megatop: + subtop2: + child2: bye + subtop3: + child3: hello + subtop1: + child1: hi + """.trimIndent() + "\n") + } + + fun testTopItemDown() { + myFixture.configureByText("test.yaml", """ + - subkey1: + value 1 + subkey2: + value 2 + - item 2 + - item 3 + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + myFixture.checkResult(""" + - item 2 + - subkey1: + value 1 + subkey2: + value 2 + - item 3 + """.trimIndent()) + } + + fun testTopItemUp() { + myFixture.configureByText("test.yaml", """ + - subkey1: + value 1 + subkey2: + value 2 + - item 2 + - item 3 + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION) + myFixture.checkResult(""" + - item 2 + - subkey1: + value 1 + subkey2: + value 2 + - item 3 + """.trimIndent()) + } + + fun testNonTopItemDown() { + myFixture.configureByText("test.yaml", """ + top-array: + - subkey1: + value 1 + subkey2: + value 2 + - subkey3: + value 1 + subkey4: + value 2 + - item 3 + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + myFixture.checkResult(""" + top-array: + - subkey3: + value 1 + subkey4: + value 2 + - subkey1: + value 1 + subkey2: + value 2 + - item 3 + """.trimIndent()) + } + + fun testNonTopItemUp() { + myFixture.configureByText("test.yaml", """ + top-array: + - subkey1: + value 1 + subkey2: + value 2 + - subkey3: + value 1 + subkey4: + value 2 + - item 3 + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION) + myFixture.checkResult(""" + top-array: + - subkey3: + value 1 + subkey4: + value 2 + - subkey1: + value 1 + subkey2: + value 2 + - item 3 + """.trimIndent()) + } + + fun testNonTopItemDownWithSelectionParts() { + myFixture.configureByText("test.yaml", """ + top-array: + - subkey1: + value 1 + subkey2: + value 2 + - subkey3: + value 3 + subkey4: + value 4 + - subkey5: + value 5 + subkey6: + value 6 + - item 4 + """.trimIndent()) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + myFixture.checkResult(""" + top-array: + - subkey5: + value 5 + subkey6: + value 6 + - subkey1: + value 1 + subkey2: + value 2 + - subkey3: + value 3 + subkey4: + value 4 + - item 4 + """.trimIndent()) + } + + fun testMoveLastDown() { + val fileContent = """ + top1: hi + top2: bye + + """.trimIndent() + myFixture.configureByText("test.yaml", fileContent) + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION) + // Nothing should be changed + myFixture.checkResult(fileContent) + } +} \ No newline at end of file