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