From a68bf87f86b92c52c4cb8dbe6ebaf390100ad738 Mon Sep 17 00:00:00 2001 From: Alexey Merkulov Date: Thu, 12 Jul 2018 16:38:11 +0300 Subject: [PATCH 1/2] Extracted methods to utility class IDEA-CR-34731 --- .../jetbrains/yaml/YAMLElementGenerator.java | 17 +----- .../src/org/jetbrains/yaml/YAMLTextUtil.java | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 plugins/yaml/src/org/jetbrains/yaml/YAMLTextUtil.java diff --git a/plugins/yaml/src/org/jetbrains/yaml/YAMLElementGenerator.java b/plugins/yaml/src/org/jetbrains/yaml/YAMLElementGenerator.java index a2a60ff60d28..6cf1eec5efb0 100644 --- a/plugins/yaml/src/org/jetbrains/yaml/YAMLElementGenerator.java +++ b/plugins/yaml/src/org/jetbrains/yaml/YAMLElementGenerator.java @@ -59,7 +59,7 @@ public class YAMLElementGenerator { text = keyName + ": " + valueText; } else { - text = keyName + ":\n" + indentText(valueText); + text = keyName + ":\n" + YAMLTextUtil.indentText(valueText, 2); } final PsiFile tempFile = createDummyYamlWithText(text); @@ -105,19 +105,4 @@ public class YAMLElementGenerator { assert at != null && at.getNode().getElementType() == YAMLTokenTypes.COLON; return at; } - - @NotNull - private static String indentText(@NotNull String text) { - StringBuilder buffer = new StringBuilder(); - String indentString = StringUtil.repeatSymbol(' ', 2); - buffer.append(indentString); - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - buffer.append(c); - if (c == '\n') { - buffer.append(indentString); - } - } - return buffer.toString(); - } } diff --git a/plugins/yaml/src/org/jetbrains/yaml/YAMLTextUtil.java b/plugins/yaml/src/org/jetbrains/yaml/YAMLTextUtil.java new file mode 100644 index 000000000000..d8fcf24b6f13 --- /dev/null +++ b/plugins/yaml/src/org/jetbrains/yaml/YAMLTextUtil.java @@ -0,0 +1,60 @@ +// Copyright 2000-2018 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; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +/** + * This is text-level utility collection + *

+ * Many of this methods could be used in other plug-ins (especially for indent-based languages). + * Maybe it will be better to move this class or part of it into IDEA platform. + */ +public class YAMLTextUtil { + private YAMLTextUtil() {} + + + // Copy-paste from com.jetbrains.python.editor.PythonCopyPasteProcessor.getLineStartSafeOffset + /** @return start line character number or document border if specified line is outside the document range */ + public static int getLineStartSafeOffset(@NotNull Document document, int line) { + if (line >= document.getLineCount()) return document.getTextLength(); + if (line < 0) return 0; + return document.getLineStartOffset(line); + } + + // Copy-paste from com.jetbrains.python.psi.PyIndentUtil#getLineIndentSize(CharSequence) + /** @return text first line indent size (just character number) */ + public static int getStartIndentSize(@NotNull CharSequence text) { + int stop; + for (stop = 0; stop < text.length(); stop++) { + final char c = text.charAt(stop); + if (!(c == ' ' || c == '\t')) { + break; + } + } + return stop; + } + + // This method is similar with com.jetbrains.python.psi.PyIndentUtil.changeIndent + /** + * This method indents each line of text by specified space number. + * Empty lines will be indented also. + * + * @return indented text + */ + @NotNull + public static String indentText(@NotNull String text, int indent) { + StringBuilder buffer = new StringBuilder(); + String indentString = StringUtil.repeatSymbol(' ', indent); + buffer.append(indentString); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + buffer.append(c); + if (c == '\n') { + buffer.append(indentString); + } + } + return buffer.toString(); + } +} From b9df542b72dcff172a019ed0245ff3a3b1292750 Mon Sep 17 00:00:00 2001 From: Alexey Merkulov Date: Fri, 6 Jul 2018 16:32:53 +0300 Subject: [PATCH 2/2] RUBY-21937 Added YAML simple copy-paste pre-processor IDEA-CR-34731 --- plugins/yaml/resources/META-INF/plugin.xml | 1 + .../formatter/YAMLCopyPasteProcessor.java | 100 ++++++++++++ .../jetbrains/yaml/paste/YAMLPasteTest.java | 148 ++++++++++++++++++ .../yaml/paste/data/oneLinePaste.txt | 3 + .../yaml/paste/data/oneLinePaste.yml | 3 + .../yaml/paste/data/pasteItem.empty.txt | 6 + .../jetbrains/yaml/paste/data/pasteItem.txt | 7 + .../jetbrains/yaml/paste/data/pasteItem.yml | 4 + .../jetbrains/yaml/paste/data/pasteLines.txt | 6 + .../jetbrains/yaml/paste/data/pasteLines.yml | 3 + .../yaml/paste/data/pasteValue.empty.txt | 5 + .../jetbrains/yaml/paste/data/pasteValue.txt | 6 + .../jetbrains/yaml/paste/data/pasteValue.yml | 3 + .../yaml/paste/data/pasteWithReplace.txt | 6 + .../yaml/paste/data/pasteWithReplace.yml | 3 + 15 files changed, 304 insertions(+) create mode 100644 plugins/yaml/src/org/jetbrains/yaml/formatter/YAMLCopyPasteProcessor.java create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/YAMLPasteTest.java create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.yml create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.empty.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.yml create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.yml create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.empty.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.yml create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.txt create mode 100644 plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.yml diff --git a/plugins/yaml/resources/META-INF/plugin.xml b/plugins/yaml/resources/META-INF/plugin.xml index c3e0c5467355..c11568bc15ad 100644 --- a/plugins/yaml/resources/META-INF/plugin.xml +++ b/plugins/yaml/resources/META-INF/plugin.xml @@ -13,6 +13,7 @@ + diff --git a/plugins/yaml/src/org/jetbrains/yaml/formatter/YAMLCopyPasteProcessor.java b/plugins/yaml/src/org/jetbrains/yaml/formatter/YAMLCopyPasteProcessor.java new file mode 100644 index 000000000000..ddd597105029 --- /dev/null +++ b/plugins/yaml/src/org/jetbrains/yaml/formatter/YAMLCopyPasteProcessor.java @@ -0,0 +1,100 @@ +// Copyright 2000-2018 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.formatter; + +import com.intellij.codeInsight.editorActions.CopyPastePreProcessor; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.LineTokenizer; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.yaml.YAMLLanguage; +import org.jetbrains.yaml.YAMLTextUtil; + +import java.util.Iterator; +import java.util.List; + +public class YAMLCopyPasteProcessor implements CopyPastePreProcessor { + @Nullable + @Override + public String preprocessOnCopy(PsiFile file, int[] startOffsets, int[] endOffsets, String text) { + return null; + } + + @NotNull + @Override + public String preprocessOnPaste(Project project, PsiFile file, Editor editor, String text, RawText rawText) { + if (file.getLanguage() != YAMLLanguage.INSTANCE) return text; + CaretModel caretModel = editor.getCaretModel(); + SelectionModel selectionModel = editor.getSelectionModel(); + Document document = editor.getDocument(); + int caretOffset = selectionModel.getSelectionStart() != selectionModel.getSelectionEnd() ? + selectionModel.getSelectionStart() : caretModel.getOffset(); + int lineNumber = document.getLineNumber(caretOffset); + int lineStartOffset = YAMLTextUtil.getLineStartSafeOffset(document, lineNumber); + int indent = caretOffset - lineStartOffset; + if (indent == 0) { + // It could be copy and paste of lines + // User could fix indentation later if he wanted to copy some block into top-level block + return text; + } + + return indentText(text, StringUtil.repeatSymbol(' ', indent)); + } + + @NotNull + private static String indentText(@NotNull String text, @NotNull String curLineIndent) { + List lines = LineTokenizer.tokenizeIntoList(text, false, false); + if (lines.isEmpty()) { + // Such situation should not be possible + Logger.getInstance(YAMLCopyPasteProcessor.class).error(text.isEmpty() + ? "Pasted empty text" + : "Text '" + text + "' was converted into empty line list"); + return text; + } + int minIndent = calculateMinBlockIndent(lines); + String firstLine = lines.iterator().next(); + if (lines.size() == 1) { + return firstLine; + } + return firstLine.substring(YAMLTextUtil.getStartIndentSize(firstLine)) + "\n" + + lines.stream().skip(1).map(line -> { + // remove common indent and add needed indent + if (isEmptyLine(line)) { + return curLineIndent + line.substring(minIndent); + } + else { + // do not indent empty lines at all + return ""; + } + }).reduce((left, right) -> left + "\n" + right).orElse(""); + } + + private static int calculateMinBlockIndent(@NotNull List list) { + Iterator it = list.iterator(); + String str = ""; + while (it.hasNext()) { + str = it.next(); + if (isEmptyLine(str)) { + break; + } + } + if (!it.hasNext()) { + return 0; + } + int minIndent = YAMLTextUtil.getStartIndentSize(str); + while (it.hasNext()) { + str = it.next(); + if (isEmptyLine(str)) { + minIndent = Math.min(minIndent, YAMLTextUtil.getStartIndentSize(str)); + } + } + return minIndent; + } + + private static boolean isEmptyLine(@NotNull String str) { + return YAMLTextUtil.getStartIndentSize(str) < str.length(); + } +} diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/YAMLPasteTest.java b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/YAMLPasteTest.java new file mode 100644 index 000000000000..49e41aec1ba5 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/YAMLPasteTest.java @@ -0,0 +1,148 @@ +// Copyright 2000-2018 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.paste; + +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.openapi.actionSystem.IdeActions; +import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; + +import java.awt.datatransfer.StringSelection; + +public abstract class YAMLPasteTest extends LightPlatformCodeInsightFixtureTestCase { + private static final String ZERO_INDENT_SAMPLE = "key1:\n" + + " subKey: val1\n" + + "\n" + + "key2: val2"; + + private static final String INDENTED_SAMPLE = " key1:\n" + + " subKey: val1\n" + + "\n" + + " key2: val2"; + + private static final String EMPTY_LINES_SAMPLE = "\n" + + "\n" + + "\n"; + + private final int myReformatOnPaste; + + private int myDefaultReformatOnPaste = CodeInsightSettings.INDENT_EACH_LINE; + + @SuppressWarnings("JUnitTestCaseWithNonTrivialConstructors") + YAMLPasteTest(int paste) { + myReformatOnPaste = paste; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myDefaultReformatOnPaste = CodeInsightSettings.getInstance().REFORMAT_ON_PASTE; + CodeInsightSettings.getInstance().REFORMAT_ON_PASTE = myReformatOnPaste; + } + + @Override + protected void tearDown() throws Exception { + try { + CodeInsightSettings.getInstance().REFORMAT_ON_PASTE = myDefaultReformatOnPaste; + } + finally { + super.tearDown(); + } + } + + @Override + protected String getTestDataPath() { + return PathManagerEx.getCommunityHomePath() + "/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/"; + } + + public void testOneLinePaste() { + doTest(" middle"); + } + + public void testPasteValue_zeroIndent() { + doTest(ZERO_INDENT_SAMPLE); + } + + public void testPasteValue_indented() { + doTest(INDENTED_SAMPLE); + } + + public void testPasteValue_empty() { + doTest(EMPTY_LINES_SAMPLE, ".empty"); + } + + public void testPasteLines() { + doTest(INDENTED_SAMPLE); + } + + public void testPasteItem_zeroIndent() { + doTest(ZERO_INDENT_SAMPLE); + } + + public void testPasteItem_indented() { + doTest(INDENTED_SAMPLE); + } + + public void testPasteItem_empty() { + doTest(EMPTY_LINES_SAMPLE, ".empty"); + } + + public void testPasteWithReplace_zeroIndent() { + doInsertTest(ZERO_INDENT_SAMPLE); + } + + public void testPasteWithReplace_indented() { + doInsertTest(INDENTED_SAMPLE); + } + + private void doTest(@NotNull String insert) { + doTest(insert, ""); + } + + private void doInsertTest(@NotNull String insert) { + doTest(insert, "", true); + } + + private void doTest(@NotNull String insert, @NotNull String resultSuffix) { + doTest(insert, resultSuffix, false); + } + + private void doTest(@NotNull String insert, @NotNull String resultSuffix, boolean selectWord) { + String testName = getTestName(true); + String fileName = ObjectUtils.notNull(StringUtil.substringBefore(testName, "_"), testName); + myFixture.configureByFile(fileName + ".yml"); + CopyPasteManager.getInstance().setContents(new StringSelection(insert)); + if (selectWord) { + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET); + } + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE); + myFixture.checkResultByFile(fileName + resultSuffix + ".txt"); + } + + public static class YAMLPasteNoReformatTest extends YAMLPasteTest { + public YAMLPasteNoReformatTest() { + super(CodeInsightSettings.NO_REFORMAT); + } + } + + public static class YAMLPasteIndentBlockTest extends YAMLPasteTest { + public YAMLPasteIndentBlockTest() { + super(CodeInsightSettings.INDENT_BLOCK); + } + } + + public static class YAMLPasteIndentEachLineTest extends YAMLPasteTest { + public YAMLPasteIndentEachLineTest() { + super(CodeInsightSettings.INDENT_EACH_LINE); + } + } + + public static class YAMLPasteReformatBlockTest extends YAMLPasteTest { + public YAMLPasteReformatBlockTest() { + super(CodeInsightSettings.REFORMAT_BLOCK); + } + } +} diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.txt new file mode 100644 index 000000000000..1edea2cd07a8 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.txt @@ -0,0 +1,3 @@ +top: + someKey1: start middle end + someKey2: value2 \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.yml b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.yml new file mode 100644 index 000000000000..c1b46a0b54e7 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/oneLinePaste.yml @@ -0,0 +1,3 @@ +top: + someKey1: start end + someKey2: value2 \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.empty.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.empty.txt new file mode 100644 index 000000000000..633f6452c3c2 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.empty.txt @@ -0,0 +1,6 @@ +top: + someKey: + # use double list for clear indent + - - + + diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.txt new file mode 100644 index 000000000000..af0f0ef16c7c --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.txt @@ -0,0 +1,7 @@ +top: + someKey: + # use double list for clear indent + - - key1: + subKey: val1 + + key2: val2 \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.yml b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.yml new file mode 100644 index 000000000000..fa4adff018bd --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteItem.yml @@ -0,0 +1,4 @@ +top: + someKey: + # use double list for clear indent + - - \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.txt new file mode 100644 index 000000000000..379270a549d3 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.txt @@ -0,0 +1,6 @@ +top: + someKey: value + key1: + subKey: val1 + + key2: val2 \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.yml b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.yml new file mode 100644 index 000000000000..bdd6c093ed30 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteLines.yml @@ -0,0 +1,3 @@ +top: + someKey: value + \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.empty.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.empty.txt new file mode 100644 index 000000000000..f7051eea223b --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.empty.txt @@ -0,0 +1,5 @@ +top: + someKey: + + + diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.txt new file mode 100644 index 000000000000..aeb7110b9c09 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.txt @@ -0,0 +1,6 @@ +top: + someKey: + key1: + subKey: val1 + + key2: val2 \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.yml b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.yml new file mode 100644 index 000000000000..32bd4c56ad8f --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteValue.yml @@ -0,0 +1,3 @@ +top: + someKey: + \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.txt b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.txt new file mode 100644 index 000000000000..aeb7110b9c09 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.txt @@ -0,0 +1,6 @@ +top: + someKey: + key1: + subKey: val1 + + key2: val2 \ No newline at end of file diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.yml b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.yml new file mode 100644 index 000000000000..380bb643c648 --- /dev/null +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/paste/data/pasteWithReplace.yml @@ -0,0 +1,3 @@ +top: + someKey: + someWord \ No newline at end of file