diff options
author | Tom Praschan <tom@praschan.de> | 2021-01-30 14:16:02 +0100 |
---|---|---|
committer | Tom Praschan <tom@praschan.de> | 2021-02-17 08:50:21 +0000 |
commit | 1085d6b508b13faf8c9cc73b6d80fa18d6757c29 (patch) | |
tree | b83f702a26b6811611c9a2bc3fdaf111bb51787d | |
parent | b3686d410dc02db3461e1b2514df65dae76c1278 (diff) | |
download | qt-creator-1085d6b508b13faf8c9cc73b6d80fa18d6757c29.tar.gz |
FakeVim: Add emulation for vim-surround plugin
Change-Id: If450d04dd89a1707ab05806522fbf4cc987d454b
Reviewed-by: hjk <hjk@qt.io>
-rw-r--r-- | doc/qtcreator/src/editors/creator-only/creator-fakevim.qdoc | 5 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevim_test.cpp | 70 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevimactions.cpp | 1 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevimactions.h | 1 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevimhandler.cpp | 243 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevimoptions.ui | 29 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevimplugin.cpp | 1 | ||||
-rw-r--r-- | src/plugins/fakevim/fakevimplugin.h | 1 |
8 files changed, 328 insertions, 23 deletions
diff --git a/doc/qtcreator/src/editors/creator-only/creator-fakevim.qdoc b/doc/qtcreator/src/editors/creator-only/creator-fakevim.qdoc index fc6838b161..120411684d 100644 --- a/doc/qtcreator/src/editors/creator-only/creator-fakevim.qdoc +++ b/doc/qtcreator/src/editors/creator-only/creator-fakevim.qdoc @@ -128,11 +128,12 @@ \li \c [count]["x]gr{motion} to replace \c {motion} with the contents of register \c x. \li ["x]grr to replace the current line. - \l{https://github.com/tommcdo/vim-exchange}{vim-exchange} \endlist + \li \l{https://github.com/tommcdo/vim-exchange}{vim-exchange} \li \l{https://github.com/vim-scripts/argtextobj.vim}{argtextobj.vim}: Defines the \c ia and \c aa text objects for function parameters. - \endlist + \li \l{https://github.com/tpope/vim-surround}{vim-surround}: + Adds mappings for deleting, adding and changing surroundings. \endlist \section2 Insert Mode diff --git a/src/plugins/fakevim/fakevim_test.cpp b/src/plugins/fakevim/fakevim_test.cpp index 6048e083f7..2335288734 100644 --- a/src/plugins/fakevim/fakevim_test.cpp +++ b/src/plugins/fakevim/fakevim_test.cpp @@ -4341,6 +4341,76 @@ void FakeVimPlugin::test_vim_arg_text_obj_emulation() KEYS("dia", "foo()"); } +void FakeVimPlugin::test_vim_surround_emulation() +{ + TestData data; + setup(&data); + data.doCommand("set surround"); + + // ys and ds + data.setText("abc"); + KEYS(R"(ysawb)", R"((abc))"); + KEYS(R"(ysabB)", R"({(abc)})"); + KEYS(R"(ysaB])", R"([{(abc)}])"); + KEYS(R"(ysa]>)", R"(<[{(abc)}]>)"); + KEYS(R"(ysa>")", R"("<[{(abc)}]>")"); + KEYS(R"(ysa"')", R"('"<[{(abc)}]>"')"); + KEYS(R"(ds')", R"("<[{(abc)}]>")"); + KEYS(R"(ds")", R"(<[{(abc)}]>)"); + KEYS(R"(ds>)", R"([{(abc)}])"); + KEYS(R"(ds])", R"({(abc)})"); + KEYS(R"(ds})", R"((abc))"); + KEYS(R"(ds))", R"(abc)"); + + data.setText("abc d|ef ghi"); + KEYS("ysiWb", "abc (def) ghi"); + KEYS(".", "abc ((def)) ghi"); + KEYS("dsb", "abc (def) ghi"); + KEYS(".", "abc def ghi"); + KEYS("ysaWb", "abc (def) ghi"); + KEYS(".", "abc ((def)) ghi"); + KEYS("dsb", "abc (def) ghi"); + KEYS(".", "abc def ghi"); + + // yss + data.setText("\t" "abc"); + KEYS("yssb", "\t" "(abc)"); + KEYS(".", "\t" "((abc))"); + + // Surround with function + data.setText("abc"); + KEYS("ysiWftest<CR>", "test(abc)"); + KEYS(".", "test(test(abc))"); + + // yS puts text on a new line + data.setText("abc"); + KEYS("ySsB", "{" N + "abc" N + "}"); + + // cs + data.setText("(abc)"); + KEYS(R"(csbB)", R"({abc})"); + KEYS(R"(csB])", R"([abc])"); + KEYS(R"(cs]>)", R"(<abc>)"); + KEYS(R"(cs>")", R"("abc")"); + KEYS(R"(cs"')", R"('abc')"); + + // Visual line mode + data.setText("abc" N); + KEYS("VSB", "{" N + "abc" N + "}" N); + + // Visual char mode + data.setText("abc"); + KEYS("vlSB", "{ab}c"); + + // Visual block mode + data.setText("abc" N "def"); + KEYS("<C-v>ljSB", "{ab}c" N "{de}f"); +} + void FakeVimPlugin::test_macros() { TestData data; diff --git a/src/plugins/fakevim/fakevimactions.cpp b/src/plugins/fakevim/fakevimactions.cpp index 62ab12d709..3d549c176c 100644 --- a/src/plugins/fakevim/fakevimactions.cpp +++ b/src/plugins/fakevim/fakevimactions.cpp @@ -120,6 +120,7 @@ FakeVimSettings::FakeVimSettings() createAction(ConfigEmulateReplaceWithRegister, false, "ReplaceWithRegister"); createAction(ConfigEmulateExchange, false, "exchange"); createAction(ConfigEmulateArgTextObj, false, "argtextobj"); + createAction(ConfigEmulateSurround, false, "surround"); } FakeVimSettings::~FakeVimSettings() diff --git a/src/plugins/fakevim/fakevimactions.h b/src/plugins/fakevim/fakevimactions.h index ad78761ae5..419232aeb8 100644 --- a/src/plugins/fakevim/fakevimactions.h +++ b/src/plugins/fakevim/fakevimactions.h @@ -114,6 +114,7 @@ enum FakeVimSettingsCode ConfigEmulateReplaceWithRegister, ConfigEmulateExchange, ConfigEmulateArgTextObj, + ConfigEmulateSurround, ConfigBlinkingCursor }; diff --git a/src/plugins/fakevim/fakevimhandler.cpp b/src/plugins/fakevim/fakevimhandler.cpp index fe3fc44f1a..8e90b0ad5b 100644 --- a/src/plugins/fakevim/fakevimhandler.cpp +++ b/src/plugins/fakevim/fakevimhandler.cpp @@ -171,6 +171,9 @@ enum SubMode ChangeSubMode, // Used for c DeleteSubMode, // Used for d ExchangeSubMode, // Used for cx + DeleteSurroundingSubMode, // Used for ds + ChangeSurroundingSubMode, // Used for cs + AddSurroundingSubMode, // Used for ys FilterSubMode, // Used for ! IndentSubMode, // Used for = RegisterSubMode, // Used for " @@ -198,16 +201,18 @@ enum SubMode enum SubSubMode { NoSubSubMode, - FtSubSubMode, // Used for f, F, t, T. - MarkSubSubMode, // Used for m. - BackTickSubSubMode, // Used for `. - TickSubSubMode, // Used for '. - TextObjectSubSubMode, // Used for thing like iw, aW, as etc. - ZSubSubMode, // Used for zj, zk - OpenSquareSubSubMode, // Used for [{, {(, [z - CloseSquareSubSubMode, // Used for ]}, ]), ]z - SearchSubSubMode, - CtrlVUnicodeSubSubMode // Used for Ctrl-v based unicode input + FtSubSubMode, // Used for f, F, t, T. + MarkSubSubMode, // Used for m. + BackTickSubSubMode, // Used for `. + TickSubSubMode, // Used for '. + TextObjectSubSubMode, // Used for thing like iw, aW, as etc. + ZSubSubMode, // Used for zj, zk + OpenSquareSubSubMode, // Used for [{, {(, [z + CloseSquareSubSubMode, // Used for ]}, ]), ]z + SearchSubSubMode, // Used for /, ? + SurroundSubSubMode, // Used for cs, ds, ys + SurroundWithFunctionSubSubMode, // Used for ys{motion}f + CtrlVUnicodeSubSubMode // Used for Ctrl-v based unicode input }; enum VisualMode @@ -1349,6 +1354,12 @@ QString dotCommandFromSubMode(SubMode submode) return QLatin1String("d"); if (submode == CommentSubMode) return QLatin1String("gc"); + if (submode == DeleteSurroundingSubMode) + return QLatin1String("ds"); + if (submode == ChangeSurroundingSubMode) + return QLatin1String("c"); + if (submode == AddSurroundingSubMode) + return QLatin1String("y"); if (submode == ExchangeSubMode) return QLatin1String("cx"); if (submode == ReplaceWithRegisterSubMode) @@ -1836,6 +1847,8 @@ public: bool handleCommentSubMode(const Input &); bool handleReplaceWithRegisterSubMode(const Input &); bool handleExchangeSubMode(const Input &); + bool handleDeleteChangeSurroundingSubMode(const Input &); + bool handleAddSurroundingSubMode(const Input &); bool handleFilterSubMode(const Input &); bool handleRegisterSubMode(const Input &); bool handleShiftSubMode(const Input &); @@ -2101,6 +2114,7 @@ public: || g.submode == ExchangeSubMode || g.submode == CommentSubMode || g.submode == ReplaceWithRegisterSubMode + || g.submode == AddSurroundingSubMode || g.submode == FilterSubMode || g.submode == IndentSubMode || g.submode == ShiftLeftSubMode @@ -2183,6 +2197,8 @@ public: void replaceWithRegister(const Range &range); + void surroundCurrentRange(const Input &input, const QString &prefix = {}); + void upCase(const Range &range); void downCase(const Range &range); @@ -2423,6 +2439,9 @@ public: // If empty, cx{motion} will store the range defined by {motion} here. // If non-empty, cx{motion} replaces the {motion} with selectText(*exchangeData) Utils::optional<Range> exchangeRange; + + bool surroundUpperCaseS; // True for yS and cS, false otherwise + QString surroundFunction; // Used for storing the function name provided to ys{motion}f } g; }; @@ -3613,6 +3632,7 @@ void FakeVimHandler::Private::finishMovement(const QString &dotCommandMovement) || g.submode == CommentSubMode || g.submode == ExchangeSubMode || g.submode == ReplaceWithRegisterSubMode + || g.submode == AddSurroundingSubMode || g.submode == YankSubMode || g.submode == InvertCaseSubMode || g.submode == DownCaseSubMode @@ -3644,6 +3664,15 @@ void FakeVimHandler::Private::finishMovement(const QString &dotCommandMovement) beginEditBlock(); toggleComment(currentRange()); endEditBlock(); + } else if (g.submode == AddSurroundingSubMode) { + g.subsubmode = SurroundSubSubMode; + g.dotCommand = dotCommandMovement; + + // We now only know the region that should be surrounded, but not the actual + // character that should surround it. We thus do NOT want to finish the + // movement yet here, so we return early. + // The next character entered will be used by the SurroundSubSubMode. + return; } else if (g.submode == ExchangeSubMode) { exchangeRange(currentRange()); } else if (g.submode == ReplaceWithRegisterSubMode @@ -3749,6 +3778,8 @@ void FakeVimHandler::Private::clearCurrentMode() g.subsubmode = NoSubSubMode; g.movetype = MoveInclusive; g.gflag = false; + g.surroundUpperCaseS = false; + g.surroundFunction.clear(); m_register = '"'; g.rangemode = RangeCharMode; g.currentCommand.clear(); @@ -3908,6 +3939,11 @@ bool FakeVimHandler::Private::handleCommandSubSubMode(const Input &input) .arg(g.semicolonKey)); } } else if (g.subsubmode == TextObjectSubSubMode) { + // vim-surround treats aw and aW the same as iw and iW, respectively + if ((input.is('w') || input.is('W')) + && g.submode == AddSurroundingSubMode && g.subsubdata.is('a')) + g.subsubdata = Input('i'); + if (input.is('w')) selectWordTextObject(g.subsubdata.is('i')); else if (input.is('W')) @@ -3987,6 +4023,37 @@ bool FakeVimHandler::Private::handleCommandSubSubMode(const Input &input) .arg(g.subsubmode == OpenSquareSubSubMode ? '[' : ']') .arg(input.text())); } + } else if (g.subsubmode == SurroundWithFunctionSubSubMode) { + if (input.isReturn()) { + pushUndoState(false); + beginEditBlock(); + + const QString dotCommand = "ys" + g.dotCommand + "f" + g.surroundFunction + "<CR>"; + + surroundCurrentRange(Input(')'), g.surroundFunction); + + g.dotCommand = dotCommand; + + endEditBlock(); + leaveCurrentMode(); + } else { + g.surroundFunction += input.asChar(); + } + return true; + } else if (g.subsubmode == SurroundSubSubMode) { + if (input.is('f') && g.submode == AddSurroundingSubMode) { + g.subsubmode = SurroundWithFunctionSubSubMode; + g.commandBuffer.setContents(""); + return true; + } + + pushUndoState(false); + beginEditBlock(); + + surroundCurrentRange(input); + + endEditBlock(); + leaveCurrentMode(); } else { handled = false; } @@ -4306,6 +4373,25 @@ EventResult FakeVimHandler::Private::handleCommandMode(const Input &input) // Exchange submode is "cx", so we need to switch over from ChangeSubMode here g.submode = ExchangeSubMode; handled = true; + } else if (g.submode == DeleteSurroundingSubMode + || g.submode == ChangeSurroundingSubMode) { + handled = handleDeleteChangeSurroundingSubMode(input); + } else if (g.submode == AddSurroundingSubMode) { + handled = handleAddSurroundingSubMode(input); + } else if (g.submode == ChangeSubMode && (input.is('s') || input.is('S')) + && hasConfig(ConfigEmulateSurround)) { + g.submode = ChangeSurroundingSubMode; + g.surroundUpperCaseS = input.is('S'); + handled = true; + } else if (g.submode == DeleteSubMode && input.is('s') && hasConfig(ConfigEmulateSurround)) { + g.submode = DeleteSurroundingSubMode; + handled = true; + } else if (g.submode == YankSubMode && (input.is('s') || input.is('S')) + && hasConfig(ConfigEmulateSurround)) { + g.submode = AddSurroundingSubMode; + g.movetype = MoveInclusive; + g.surroundUpperCaseS = input.is('S'); + handled = true; } else if (g.submode == ChangeSubMode || g.submode == DeleteSubMode || g.submode == YankSubMode) { @@ -4640,6 +4726,9 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) int repeat = count(); while (--repeat >= 0) redo(); + } else if (input.is('S') && isVisualMode() && hasConfig(ConfigEmulateSurround)) { + g.submode = AddSurroundingSubMode; + g.subsubmode = SurroundSubSubMode; } else if (input.is('s')) { handleAs("c%1l"); } else if (input.is('S')) { @@ -4891,6 +4980,69 @@ bool FakeVimHandler::Private::handleExchangeSubMode(const Input &input) return false; } +bool FakeVimHandler::Private::handleDeleteChangeSurroundingSubMode(const Input &input) +{ + if (g.submode != ChangeSurroundingSubMode && g.submode != DeleteSurroundingSubMode) + return false; + + bool handled = false; + + if (input.is('(') || input.is(')') || input.is('b')) { + handled = selectBlockTextObject(false, '(', ')'); + } else if (input.is('{') || input.is('}') || input.is('B')) { + handled = selectBlockTextObject(false, '{', '}'); + } else if (input.is('[') || input.is(']')) { + handled = selectBlockTextObject(false, '[', ']'); + } else if (input.is('<') || input.is('>') || input.is('t')) { + handled = selectBlockTextObject(false, '<', '>'); + } else if (input.is('"') || input.is('\'') || input.is('`')) { + handled = selectQuotedStringTextObject(false, input.asChar()); + } + + if (handled) { + if (g.submode == DeleteSurroundingSubMode) { + pushUndoState(false); + beginEditBlock(); + + // Surround is always one character, so just delete the first and last one + transformText(currentRange(), [](const QString &text) { + return text.mid(1, text.size() - 2); + }); + + endEditBlock(); + clearCurrentMode(); + + g.dotCommand = "ds" + input.asChar(); + } else if (g.submode == ChangeSurroundingSubMode) { + g.subsubmode = SurroundSubSubMode; + } + } + + return handled; +} + +bool FakeVimHandler::Private::handleAddSurroundingSubMode(const Input &input) +{ + if (!input.is('s')) + return false; + + g.subsubmode = SurroundSubSubMode; + + int anc = firstPositionInLine(cursorLine() + 1); + const int pos = lastPositionInLine(cursorLine() + 1); + + // Ignore leading spaces + while ((characterAt(anc) == " " || characterAt(anc) == "\t") && anc != pos) { + anc++; + } + + setAnchorAndPosition(anc, pos); + + finishMovement("s"); + + return true; +} + bool FakeVimHandler::Private::handleFilterSubMode(const Input &) { return false; @@ -7564,6 +7716,77 @@ void FakeVimHandler::Private::replaceWithRegister(const Range &range) replaceText(range, registerContents(m_register)); } +void FakeVimHandler::Private::surroundCurrentRange(const Input &input, const QString &prefix) +{ + QString dotCommand; + if (isVisualMode()) + dotCommand = visualDotCommand() + "S" + input.asChar(); + + const bool wasVisualCharMode = isVisualCharMode(); + const bool wasVisualLineMode = isVisualLineMode(); + leaveVisualMode(); + + if (dotCommand.isEmpty()) { // i.e. we came from normal mode + dotCommand = dotCommandFromSubMode(g.submode) + (g.surroundUpperCaseS ? "S" : "s") + + g.dotCommand + input.asChar(); + } + + if (wasVisualCharMode) + setPosition(position() + 1); + + QString newFront, newBack; + + if (input.is('(') || input.is(')') || input.is('b')) { + newFront = '('; + newBack = ')'; + } else if (input.is('{') || input.is('}') || input.is('B')) { + newFront = '{'; + newBack = '}'; + } else if (input.is('[') || input.is(']')) { + newFront = '['; + newBack = ']'; + } else if (input.is('<') || input.is('>') || input.is('t')) { + newFront = '<'; + newBack = '>'; + } else if (input.is('"') || input.is('\'') || input.is('`')) { + newFront = input.asChar(); + newBack = input.asChar(); + } + + if (g.surroundUpperCaseS || wasVisualLineMode) { + // yS and cS add a new line before and after the surrounded text + newFront += "\n"; + if (wasVisualLineMode) + newBack += "\n"; + else + newBack = "\n" + newBack; + } else if (input.is('(') || input.is('{') || input.is('[') || input.is('[')) { + // Opening characters add an extra space + newFront = newFront + " "; + newBack = " " + newBack; + } + + + if (!newFront.isEmpty()) { + transformText(currentRange(), [&](QString text) -> QString { + if (newFront == QChar()) + return text.mid(1, text.size() - 2); + + const QString newMiddle = (g.submode == ChangeSurroundingSubMode) ? + text.mid(1, text.size() - 2) : text; + + return prefix + newFront + newMiddle + newBack; + }); + } + + // yS, cS and VS also indent the surrounded text + if (g.surroundUpperCaseS || wasVisualLineMode) + replay("=a" + input.asChar()); + + // Indenting has changed the dotCommand, so now set it back to the correct one + g.dotCommand = dotCommand; +} + void FakeVimHandler::Private::replaceText(const Range &range, const QString &str) { transformText(range, [&str](const QString &) { return str; } ); diff --git a/src/plugins/fakevim/fakevimoptions.ui b/src/plugins/fakevim/fakevimoptions.ui index 7bbdb72e61..b9f7a1845f 100644 --- a/src/plugins/fakevim/fakevimoptions.ui +++ b/src/plugins/fakevim/fakevimoptions.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>580</width> - <height>543</height> + <height>568</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_2"> @@ -162,6 +162,20 @@ <string>Plugin Emulation</string> </property> <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="0"> + <widget class="QCheckBox" name="checkBoxExchange"> + <property name="text"> + <string>vim-exchange</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="checkBoxArgTextObj"> + <property name="text"> + <string>argtextobj.vim</string> + </property> + </widget> + </item> <item row="1" column="0"> <widget class="QCheckBox" name="checkBoxReplaceWithRegister"> <property name="text"> @@ -179,17 +193,10 @@ </property> </widget> </item> - <item row="2" column="0"> - <widget class="QCheckBox" name="checkBoxArgTextObj"> + <item row="4" column="0"> + <widget class="QCheckBox" name="checkBoxVimSurround"> <property name="text"> - <string>argtextobj.vim</string> - </property> - </widget> - </item> - <item row="2" column="0"> - <widget class="QCheckBox" name="checkBoxExchange"> - <property name="text"> - <string>vim-exchange</string> + <string>vim-surround</string> </property> </widget> </item> diff --git a/src/plugins/fakevim/fakevimplugin.cpp b/src/plugins/fakevim/fakevimplugin.cpp index 4712b6da69..c14927769a 100644 --- a/src/plugins/fakevim/fakevimplugin.cpp +++ b/src/plugins/fakevim/fakevimplugin.cpp @@ -433,6 +433,7 @@ QWidget *FakeVimOptionPage::widget() m_group.insert(theFakeVimSetting(ConfigEmulateReplaceWithRegister), m_ui.checkBoxReplaceWithRegister); m_group.insert(theFakeVimSetting(ConfigEmulateExchange), m_ui.checkBoxExchange); m_group.insert(theFakeVimSetting(ConfigEmulateArgTextObj), m_ui.checkBoxArgTextObj); + m_group.insert(theFakeVimSetting(ConfigEmulateSurround), m_ui.checkBoxVimSurround); connect(m_ui.pushButtonCopyTextEditorSettings, &QAbstractButton::clicked, this, &FakeVimOptionPage::copyTextEditorSettings); diff --git a/src/plugins/fakevim/fakevimplugin.h b/src/plugins/fakevim/fakevimplugin.h index 84414c20e0..5539fa79eb 100644 --- a/src/plugins/fakevim/fakevimplugin.h +++ b/src/plugins/fakevim/fakevimplugin.h @@ -163,6 +163,7 @@ private slots: void test_vim_replace_with_register_emulation(); void test_vim_exchange_emulation(); void test_vim_arg_text_obj_emulation(); + void test_vim_surround_emulation(); void test_macros(); |