Skip to content

Commit

Permalink
#2894 Improved heredoc content update process
Browse files Browse the repository at this point in the history
- No fancy indentation adjustments.
- One line updates will work by themselves, because injection is already indented
- One line to multiline updates are now properly indented
- Added tests
  • Loading branch information
hurricup committed Sep 15, 2024
1 parent eab1cd3 commit e9e5263
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2015-2020 Alexandr Evstigneev
* Copyright 2015-2024 Alexandr Evstigneev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,14 +16,10 @@

package com.perl5.lang.perl.idea.manipulators;

import com.intellij.application.options.CodeStyle;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.AbstractElementManipulator;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.util.IncorrectOperationException;
import com.perl5.lang.perl.PerlLanguage;
import com.perl5.lang.perl.psi.impl.PerlHeredocElementImpl;
import com.perl5.lang.perl.psi.utils.PerlElementFactory;
import org.jetbrains.annotations.NotNull;
Expand All @@ -34,58 +30,29 @@ public class PerlHeredocElementManipulator extends AbstractElementManipulator<Pe
@Override
public PerlHeredocElementImpl handleContentChange(@NotNull PerlHeredocElementImpl element, @NotNull TextRange range, String newContent)
throws IncorrectOperationException {
StringBuilder sb = new StringBuilder(newContent);
if (element.getTextLength() == range.getEndOffset() && !StringUtil.endsWith(newContent, "\n")) {
sb.append("\n");
newContent += "\n";
}

Project project = element.getProject();
var elementText = element.getText();
String indent = "";
if (range.getStartOffset() > 0) {
sb.insert(0, getIndenter(project, range.getStartOffset()));
range = TextRange.from(0, range.getEndOffset());
}
var lineStart = StringUtil.skipWhitespaceBackward(elementText, range.getStartOffset() - 1);
if (lineStart < range.getStartOffset()) {
indent = elementText.substring(lineStart, range.getStartOffset());
range = TextRange.create(lineStart, range.getEndOffset());

normalizeIndentation(project, sb, element.getRealIndentSize());
newContent = prependLines(newContent, indent);
}
}

String newElementText = range.replace(element.getText(), sb.toString());
String newElementText = range.replace(elementText, newContent);
PerlHeredocElementImpl replacement = PerlElementFactory.createHeredocBodyReplacement(element, newElementText);

return (PerlHeredocElementImpl)element.replace(replacement);
}

private static @NotNull String getIndenter(@NotNull Project project, int indentSize) {
CommonCodeStyleSettings.IndentOptions indentOptions =
CodeStyle.getSettings(project).getCommonSettings(PerlLanguage.INSTANCE).getIndentOptions();

return StringUtil.repeat(indentOptions != null && indentOptions.USE_TAB_CHARACTER ? "\t" : " ", indentSize);
}

private static void normalizeIndentation(@NotNull Project project, @NotNull StringBuilder sb, int indentSize) {
int offset = 0;
int currentLineStart = 0;
int currentLineIndentSize = 0;
boolean hasNonSpaces = false;

while (offset < sb.length()) {
char currentChar = sb.charAt(offset++);
if (currentChar == '\n') {
if (hasNonSpaces && currentLineIndentSize < indentSize) {
int indentAdjustment = indentSize - currentLineIndentSize;
sb.insert(currentLineStart, getIndenter(project, indentAdjustment));
offset += indentAdjustment;
}
currentLineStart = offset;
hasNonSpaces = false;
currentLineIndentSize = 0;
}
else if (!hasNonSpaces) {
if (Character.isWhitespace(currentChar)) {
currentLineIndentSize++;
}
else {
hasNonSpaces = true;
}
}
}
private static @NotNull String prependLines(@NotNull String newContent, @NotNull String prefix) {
return prefix + String.join(prefix, StringUtil.split(newContent, "\n", false, true));
}
}
108 changes: 95 additions & 13 deletions plugin/src/test/java/intellilang/PerlQuickEditTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package intellilang

import base.PerlLightTestCase
import com.intellij.codeInsight.intention.impl.QuickEditAction
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.InjectionTestFixture
import org.jetbrains.annotations.NonNls
import org.junit.Test
Expand All @@ -27,42 +29,88 @@ class PerlQuickEditTest : PerlLightTestCase() {
private val injectionTestFixture: InjectionTestFixture get() = InjectionTestFixture(myFixture)

@Test
fun testEditHeredoc() {
initWithTextSmart(
fun testEditHeredocMiddle() {
val (originalEditor, fragmentFile) = initFileWithTestSample()

myFixture.editor.caretModel.moveToOffset(fragmentFile.text.indexAfter("<html>"))
myFixture.type("\nhello there")
assertFalse(myFixture.editor.isDisposed)
assertEquals(
"""
<div>
<html>
hello there
</html>
</div>""".trimIndent(), myFixture.editor.document.text.trim().replace(Regex("[ \t]+\n"), "\n")
)

assertEquals(
"""
use v5.36;
sub foo{
say <<~HTML;
<div>
<html><caret>
<html>
hello there
</html>
</div>
HTML
}""".trimIndent()
}""".trimIndent(), originalEditor.document.text
)
val originalEditor = injectionTestFixture.topLevelEditor
val quickEditHandler = QuickEditAction().invokeImpl(project, injectionTestFixture.topLevelEditor, injectionTestFixture.topLevelFile)
val fragmentFile = quickEditHandler.newFile
}

@Test
fun testEditHeredocEnd() {
val (originalEditor, fragmentFile) = initFileWithTestSample()

myFixture.editor.caretModel.moveToOffset(fragmentFile.text.indexAfter("</div>"))
myFixture.type("\n\n\nhello there\n")
assertFalse(myFixture.editor.isDisposed)
assertEquals(
"""
<div>
<html>
</html>
</div>
""".trimIndent(), fragmentFile.text.trim()
hello there
""".trimIndent(), myFixture.editor.document.text.trim().replace(Regex("[ \t]+\n"), "\n")
)

myFixture.openFileInEditor(fragmentFile.virtualFile)
assertEquals(
"""
use v5.36;
sub foo{
say <<~HTML;
<div>
<html>
</html>
</div>
hello there
HTML
}""".trimIndent(), originalEditor.document.text
)
}

myFixture.editor.caretModel.moveToOffset(fragmentFile.text.indexAfter("<html>"))
myFixture.type("\nhello there")
@Test
fun testEditHeredocStart() {
val (originalEditor, fragmentFile) = initFileWithTestSample()

myFixture.editor.caretModel.moveToOffset(fragmentFile.text.indexOf("<div>"))
myFixture.type("\n\n\nhello there\n\n")
assertFalse(myFixture.editor.isDisposed)
assertEquals(
"""
hello there
<div>
<html>
hello there
</html>
</div>""".trimIndent(), myFixture.editor.document.text.trim().replace(Regex("[ \t]+\n"), "\n")
)
Expand All @@ -73,16 +121,50 @@ class PerlQuickEditTest : PerlLightTestCase() {
sub foo{
say <<~HTML;
hello there
<div>
<html>
hello there
</html>
</div>
HTML
}""".trimIndent(), originalEditor.document.text
)
}

private fun initFileWithTestSample(): Pair<Editor, PsiFile> {
initWithTextSmart(
"""
use v5.36;
sub foo{
say <<~HTML;
<div>
<html><caret>
</html>
</div>
HTML
}""".trimIndent()
)
val originalEditor = injectionTestFixture.topLevelEditor
val quickEditHandler = QuickEditAction().invokeImpl(project, injectionTestFixture.topLevelEditor, injectionTestFixture.topLevelFile)
val fragmentFile = quickEditHandler.newFile
assertEquals(
"""
<div>
<html>
</html>
</div>
""".trimIndent(), fragmentFile.text.trim()
)

myFixture.openFileInEditor(fragmentFile.virtualFile)
return Pair(originalEditor, fragmentFile)
}

private fun String.indexAfter(string: String): Int {
val r = indexOf(string)
return if (r == -1) -1 else r + string.length
Expand Down

0 comments on commit e9e5263

Please sign in to comment.