IDEA-247565 Implement statement move in YAML

It supports now only block-style key-values and items

IJ-CR-19054

GitOrigin-RevId: 200f3763895782a1eb55ee2907aee0fdb18b44ec
This commit is contained in:
Alexey Merkulov
2021-12-23 15:31:31 +03:00
committed by intellij-monorepo-bot
parent e0c488e837
commit 399b8ba853
3 changed files with 387 additions and 0 deletions

View File

@@ -22,6 +22,8 @@
<typedHandler implementation="org.jetbrains.yaml.formatter.YAMLHyphenTypedHandler"/>
<quoteHandler fileType="YAML" className="org.jetbrains.yaml.smart.YamlQuoteHandler"/>
<copyPastePreProcessor implementation="org.jetbrains.yaml.smart.YAMLCopyPasteProcessor"/>
<statementUpDownMover implementation="org.jetbrains.yaml.smart.YAMLStatementMover" id="yamlStatementMover"
order="before line"/>
<lang.parserDefinition language="yaml" implementationClass="org.jetbrains.yaml.YAMLParserDefinition"/>
<lang.commenter language="yaml" implementationClass="org.jetbrains.yaml.YAMLCommenter"/>
<lang.syntaxHighlighterFactory language="yaml" implementationClass="org.jetbrains.yaml.YAMLSyntaxHighlighterFactory"/>

View File

@@ -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
}
}

View File

@@ -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<caret>:
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<caret>:
child2: bye
top1:
child1: hi
""".trimIndent() + "\n")
}
fun testTopKeyValueDown() {
myFixture.configureByText("test.yaml", """
top1<caret>:
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<caret>:
child1: hi
""".trimIndent() + "\n")
}
fun testNonTopKeyValueDown() {
myFixture.configureByText("test.yaml", """
megatop:
subtop1<caret>:
child1: hi
subtop2:
child2: bye
subtop3:
child3: hello
""".trimIndent())
myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION)
myFixture.checkResult("""
megatop:
subtop2:
child2: bye
subtop1<caret>:
child1: hi
subtop3:
child3: hello
""".trimIndent())
}
fun testNonTopKeyValueUp() {
myFixture.configureByText("test.yaml", """
megatop:
subtop1:
child1: hi
subtop2<caret>:
child2: bye
subtop3:
child3: hello
""".trimIndent())
myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION)
myFixture.checkResult("""
megatop:
subtop2<caret>:
child2: bye
subtop1:
child1: hi
subtop3:
child3: hello
""".trimIndent())
}
fun testNonTopKeyValueDownWithSelection() {
myFixture.configureByText("test.yaml", """
megatop:
<selection>subtop1:
child1: hi
subtop2:
child2: bye</selection>
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
<selection>subtop1:
child1: hi
subtop2:
child2: bye</selection>
""".trimIndent() + "\n")
}
fun testNonTopKeyValueUpWithSelection() {
myFixture.configureByText("test.yaml", """
megatop:
subtop1:
child1: hi
<selection>subtop2:
child2: bye
subtop3:
child3: hello</selection>
""".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:
<selection>subtop2:
child2: bye
subtop3:
child3: hello</selection>
subtop1:
child1: hi
""".trimIndent() + "\n")
}
fun testTopItemDown() {
myFixture.configureByText("test.yaml", """
<caret>- subkey1:
value 1
subkey2:
value 2
- item 2
- item 3
""".trimIndent())
myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION)
myFixture.checkResult("""
- item 2
<caret>- subkey1:
value 1
subkey2:
value 2
- item 3
""".trimIndent())
}
fun testTopItemUp() {
myFixture.configureByText("test.yaml", """
- subkey1:
value 1
subkey2:
value 2
- item 2<caret>
- item 3
""".trimIndent())
myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION)
myFixture.checkResult("""
- item 2<caret>
- subkey1:
value 1
subkey2:
value 2
- item 3
""".trimIndent())
}
fun testNonTopItemDown() {
myFixture.configureByText("test.yaml", """
top-array:
<caret>- 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
<caret>- subkey1:
value 1
subkey2:
value 2
- item 3
""".trimIndent())
}
fun testNonTopItemUp() {
myFixture.configureByText("test.yaml", """
top-array:
- subkey1:
value 1
subkey2:
value 2
<caret>- subkey3:
value 1
subkey4:
value 2
- item 3
""".trimIndent())
myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION)
myFixture.checkResult("""
top-array:
<caret>- 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
<selection>subkey2:
value 2
- subkey3:
value 3</selection>
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
<selection>subkey2:
value 2
- subkey3:
value 3</selection>
subkey4:
value 4
- item 4
""".trimIndent())
}
fun testMoveLastDown() {
val fileContent = """
top1: hi
top2<caret>: bye
""".trimIndent()
myFixture.configureByText("test.yaml", fileContent)
myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION)
// Nothing should be changed
myFixture.checkResult(fileContent)
}
}