Wednesday, May 27, 2015

Plugin for IntelliJ to permute a list of enything

Many times I encounter the situation, where I want to reorder a list: enum values, DataSet test cases, parameters of a function, elements of a list, etc. Cut and paste is just too much effort, and extra care has to be taken to keep formatting. So I've written a small code snippet to rearrange these lists. I use IntelliJ's LivePlugin (on github) to use it quickly (but it could be converted to a full Java plugin). But unfortunatelly it handles Groovy script. Testing groovy script I find it pain in the ass, so I've created a Java barebone with test, and than converted it to Groovyscript (I myself find this approach a bit stupid, though).

How it works? If your carret is sitting in between any type of brackets, it finds the innermost it sits in. It finds the list element (text, that can contain spaces, other closed brackets, comas inside those brackets). With Ctrl+Alt+L it swaps it to the right, with Ctrl+Alt+K, it swaps it to the left. The carret moves with the item.

Known limitations. If there is an unclosed backet (syntactically incorret code), it doesn't handle it well. If the carret is not inside a backet, the behaviour is unpredicted.

The rest of the post is just source code. The Java fragment:

int start;
    int end;
    int mid;
    int midLeft;
    int midRight;
    public String doTheFlop(final String input, final int carretPos) {
        start = carretPos - 1;
        mid = carretPos;

        travelToBeginning(input);

        travelToMid(input);

        if (isRightLimiter(input.charAt(mid))) {
            return null;
        }

        midLeft = mid ;
        midRight = mid ;
        travelToMidLeft(input);

        travelToMidRight(input);

        end = midRight;
        travelToEnd(input);

        return input.substring(midRight, end) + input.substring(midLeft, midRight) + input.substring(start, midLeft);
    }

    private void travelToBeginning(final String input) {
        int groupDepth = 0;
        while (!((input.charAt(start) == ',' && groupDepth == 0)
               || (isLeftLimiter(input.charAt(start)) && groupDepth == 0))) {
            if (isRightLimiter(input.charAt(start))) {
                groupDepth++;
            }
            if (isLeftLimiter(input.charAt(start))) {
                groupDepth--;
            }
            start--;
        }
        start++;
        while (Character.isWhitespace(input.charAt(start))) {
            start++;
        }
    }

    private void travelToMid(final String input) {
        int groupDepth = 0;
        while (!((input.charAt(mid) == ',' && groupDepth == 0)
                 || (isRightLimiter(input.charAt(mid)) && groupDepth == 0))) {
            if (isLeftLimiter(input.charAt(mid))) {
                groupDepth++;
            }
            if (isRightLimiter(input.charAt(mid))) {
                groupDepth--;
            }
            mid++;
        }
    }

    private void travelToMidLeft(final String input) {
        // this currently points to a coma or bracket
        midLeft--;
        while (Character.isWhitespace(input.charAt(midLeft))) {
            midLeft--;
        }
        midLeft++;
    }

    private void travelToMidRight(final String input) {
        // this currently points to a coma or bracket
        midRight++;
        while (Character.isWhitespace(input.charAt(midRight))) {
            midRight++;
        }
    }

    private void travelToEnd(final String input) {
        int groupDepth = 0;
        while (!((input.charAt(end) == ',' && groupDepth == 0)
                 || (isRightLimiter(input.charAt(end)) && groupDepth == 0))) {
            if (isLeftLimiter(input.charAt(end))) {
                groupDepth++;
            }
            if (isRightLimiter(input.charAt(end))) {
                groupDepth--;
            }
            end++;
        }

        end--;
        while (Character.isWhitespace(input.charAt(end))) {
            end--;
        }
        end++;
    }

    private boolean isLeftLimiter(final char c) {
        return c == '(' || c == '<' || c == '[' || c == '{';
    }

    private boolean isRightLimiter(final char c) {
        return c == ')' || c == '>' || c == ']' || c == '}';
    }

Test for this:

public static class Stuff extends SimpleTestVectors {
        @Override
        protected Object[][] generateTestVectors() {
            return new Object[][] {
                    //"(  final Broad broadcast,  final  Frame   frame,final   Logger logger)"
                    //(Logger logger,    Frame< String, Integer  > frame, final   @Named ( "quickDepositStatusIntentTranslator" ) Broad broadcast)
                    {"(  ", "final Broad broadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                    {"(  final Broad br", "oadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                    {"(  final Broad br", "oadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                    {"(  final Broad broadcast", ",  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                    {"(  final Broad broadcast,", "  final  Frame   frame,final   Logger logger)", "final   Logger logger,final  Frame   frame"},  //, "(  final Broad broadcast,  final   Logger logger,final  Frame   frame)"
                    {"(  final Broad broadcast,  final  Fr", "ame   frame,final   Logger logger)", "final   Logger logger,final  Frame   frame"},  //, "(  final Broad broadcast,  final   Logger logger,final  Frame   frame)"
                    {"(  final Broad broadcast,  final  Frame   frame,final   ", "Logger logger)", null},  //, "(  final Broad broadcast,  final  Frame   frame,final   Logger logger)"

                    {"(Broad broadcast, Fr", "ame frame      , Logger logger   )", "Logger logger      , Frame frame"},  //, "(final Broad broadcast, final Logger logger      , final Frame frame   )"

                    {"(final   @", "Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", "Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast"}, //"(Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger)"},
                    {"(final   @Named ( \"quickDepositS", "tatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", null}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)"},
                    {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ", ") Broad broadcast, Frame< String, Integer  > frame, Logger logger)", null}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)"},
                    {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad", " broadcast, Frame< String, Integer  > frame, Logger logger)", "Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast"}, //"(Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger)"},
                    {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, ", "Frame< String, Integer  > frame, Logger logger)", "Logger logger, Frame< String, Integer  > frame"}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger, Frame< String, Integer  > frame)"},
                    {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< S", "tring, Integer  > frame, Logger logger)", "Integer, String"}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< Integer, String  > frame, Logger logger)"},

                    {"(final   @Named ( \"", " foo () \" , \" bar () \" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", "\" bar () \" , \" foo () \""}, //"(final   @Named( \" bar () \" , \" foo () \" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)""},
            };
        }
    }

    @Test
    @DataSet(testData = Stuff.class)
    public void dummy_FIXME() throws InvalidDataSetException {
        // init

        // run
        final String result = sut.doTheFlop(rule.getString(0) + rule.getString(1), rule.getString(0).length());

        // verify
        assertThat(result, equalTo(rule.getString(2)));
    }


And the final Groovyscript refined:

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.SelectionModel
import com.intellij.openapi.util.TextRange

import static liveplugin.PluginUtil.*
import static liveplugin.PluginUtil.showInConsole
import static liveplugin.PluginUtil.showInConsole
import static liveplugin.PluginUtil.showInConsole


registerAction("swap to right", "alt ctrl L") { AnActionEvent event ->
    def editor = currentEditorIn(event.project)
    def input = editor.getDocument().getChars()
    def caretModel = editor.getCaretModel()
    def selectionModel = editor.getSelectionModel()


    int start = caretModel.offset - 1;
    if (start < 0) return;
    int mid = start + 1;
    int end;
    int midLeft;
    int midRight;

    //travelToBeginning
    int groupDepth = 0;
    while (!((input[start] == ',' && groupDepth == 0)
            || (isLeftLimiter(input[start]) && groupDepth == 0))) {
        if (isRightLimiter(input[start])) {
            groupDepth++;
        }
        if (isLeftLimiter(input[start])) {
            groupDepth--;
        }
        start--;
    }
    // fallback to first non-whitespace char
    start++;
    while (isWhitespace(input[start])) {
        start++;
    }

    //travelToMid
    groupDepth = 0;
    while (!((input[mid] == ',' && groupDepth == 0)
            || (isRightLimiter(input[mid]) && groupDepth == 0))) {
        if (isLeftLimiter(input[mid])) {
            groupDepth++;
        }
        if (isRightLimiter(input[mid])) {
            groupDepth--;
        }
        mid++;
    }

    // this is already the last element
    if (isRightLimiter(input[mid])) {
        return;
    }

    midLeft = mid;
    midRight = mid;

    //travelToMidLeft
    // this currently points to a coma or bracket
    midLeft--;
    while (isWhitespace(input[midLeft])) {
        midLeft--;
    }
    midLeft++;

    //travelToMidRight
    // this currently points to a coma or bracket
    midRight++;
    while (isWhitespace(input[midRight])) {
        midRight++;
    }

    //travelToEnd
    end = midRight;
    groupDepth = 0;
    while (!((input[end] == ',' && groupDepth == 0)
            || (isRightLimiter(input[end]) && groupDepth == 0))) {
        if (isLeftLimiter(input[end])) {
            groupDepth++;
        }
        if (isRightLimiter(input[end])) {
            groupDepth--;
        }
        end++;
    }

    // fallback to last non-whitespace char
    end--;
    while (isWhitespace(input[end])) {
        end--;
    }
    end++;


    // do the swap
    String text = editor.document.getText(new TextRange(start, end))
    String swappedText = text.substring(midRight - start, end - start) + text.substring(midLeft - start, midRight - start) + text.substring(0, midLeft - start)

    runDocumentWriteAction(event.project, editor.document, "Swap to right a list element", "Permute plugin (mnw)") {
        editor.document.replaceString(start, end, swappedText)
    }

    caretModel.moveToOffset(end)
    editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}

registerAction("swap to left", "alt ctrl K") { AnActionEvent event ->
    def editor = currentEditorIn(event.project)
    def input = editor.getDocument().getChars()
    def caretModel = editor.getCaretModel()
    def selectionModel = editor.getSelectionModel()


    int start = caretModel.offset - 1;
    if (start < 0) return;
    int carret = start;
    int lastComa;
    int mid;
    int end;
    int midLeft;
    int midRight;

    //travelToBeginning
    int groupDepth = 0;
    while (!(isLeftLimiter(input[start]) && groupDepth == 0)) {
        if (isRightLimiter(input[start])) {
            groupDepth++;
        }
        if (isLeftLimiter(input[start])) {
            groupDepth--;
        }
        start--;
    }
    lastComa = start;

    //travel backwards ToMid from start keeping track if we find coma (only the last is important)
    groupDepth = 0;
    mid = start + 1;
    while (mid <= carret) {
        if (isLeftLimiter(input[mid])) {
            groupDepth++;
        }
        if (isRightLimiter(input[mid])) {
            groupDepth--;
        }
        if (input[mid] == ',' && groupDepth == 0) {
            start = lastComa;
            lastComa = mid;
        }
        mid++;
    }

    // this is already the first element
    if (lastComa == start) {
        return;
    }

    // skip whitespaces from start
    start++;
    while (isWhitespace(input[start])) {
        start++;
    }

    midLeft = lastComa;
    midRight = lastComa;

    // skipp whitespaces around the coma
    // this currently points to a coma or bracket
    midLeft--;
    while (isWhitespace(input[midLeft])) {
        midLeft--;
    }
    midLeft++;
    // this currently points to a coma or bracket
    midRight++;
    while (isWhitespace(input[midRight])) {
        midRight++;
    }

    //travelToEnd
    end = midRight;
    groupDepth = 0;
    while (!((input[end] == ',' && groupDepth == 0)
            || (isRightLimiter(input[end]) && groupDepth == 0))) {
        if (isLeftLimiter(input[end])) {
            groupDepth++;
        }
        if (isRightLimiter(input[end])) {
            groupDepth--;
        }
        end++;
    }

    // skip ending whitespaces from end
    end--;
    while (isWhitespace(input[end])) {
        end--;
    }
    end++;


    // do the swap
    String text = editor.document.getText(new TextRange(start, end))
    String swappedText = text.substring(midRight - start, end - start) + text.substring(midLeft - start, midRight - start) + text.substring(0, midLeft - start)

    runDocumentWriteAction(event.project, editor.document, "Swap to left a list element", "Permute plugin (mnw)") {
        editor.document.replaceString(start, end, swappedText)
    }


    caretModel.moveToOffset(start + end - midRight)
    editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}

private boolean isWhitespace(char charAt) {
    return charAt == " " || charAt == "\t" || charAt == "\n" || charAt == "\r" || charAt == "\f";
}

private boolean isLeftLimiter(final char c) {
    return c == '(' || c == '<' || c == '[' || c == '{';
}

private boolean isRightLimiter(final char c) {
    return c == ')' || c == '>' || c == ']' || c == '}';
}

show("permute Loaded")

No comments: