FakeVim: Add emulation for vim-surround plugin

Change-Id: If450d04dd89a1707ab05806522fbf4cc987d454b
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Tom Praschan
2021-01-30 14:16:02 +01:00
parent b3686d410d
commit 1085d6b508
8 changed files with 328 additions and 23 deletions

View File

@@ -128,11 +128,12 @@
\li \c [count]["x]gr{motion} to replace \c {motion} with the contents of \li \c [count]["x]gr{motion} to replace \c {motion} with the contents of
register \c x. register \c x.
\li ["x]grr to replace the current line. \li ["x]grr to replace the current line.
\l{https://github.com/tommcdo/vim-exchange}{vim-exchange}
\endlist \endlist
\li \l{https://github.com/tommcdo/vim-exchange}{vim-exchange}
\li \l{https://github.com/vim-scripts/argtextobj.vim}{argtextobj.vim}: \li \l{https://github.com/vim-scripts/argtextobj.vim}{argtextobj.vim}:
Defines the \c ia and \c aa text objects for function parameters. 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 \endlist
\section2 Insert Mode \section2 Insert Mode

View File

@@ -4341,6 +4341,76 @@ void FakeVimPlugin::test_vim_arg_text_obj_emulation()
KEYS("dia", "foo()"); 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() void FakeVimPlugin::test_macros()
{ {
TestData data; TestData data;

View File

@@ -120,6 +120,7 @@ FakeVimSettings::FakeVimSettings()
createAction(ConfigEmulateReplaceWithRegister, false, "ReplaceWithRegister"); createAction(ConfigEmulateReplaceWithRegister, false, "ReplaceWithRegister");
createAction(ConfigEmulateExchange, false, "exchange"); createAction(ConfigEmulateExchange, false, "exchange");
createAction(ConfigEmulateArgTextObj, false, "argtextobj"); createAction(ConfigEmulateArgTextObj, false, "argtextobj");
createAction(ConfigEmulateSurround, false, "surround");
} }
FakeVimSettings::~FakeVimSettings() FakeVimSettings::~FakeVimSettings()

View File

@@ -114,6 +114,7 @@ enum FakeVimSettingsCode
ConfigEmulateReplaceWithRegister, ConfigEmulateReplaceWithRegister,
ConfigEmulateExchange, ConfigEmulateExchange,
ConfigEmulateArgTextObj, ConfigEmulateArgTextObj,
ConfigEmulateSurround,
ConfigBlinkingCursor ConfigBlinkingCursor
}; };

View File

@@ -171,6 +171,9 @@ enum SubMode
ChangeSubMode, // Used for c ChangeSubMode, // Used for c
DeleteSubMode, // Used for d DeleteSubMode, // Used for d
ExchangeSubMode, // Used for cx ExchangeSubMode, // Used for cx
DeleteSurroundingSubMode, // Used for ds
ChangeSurroundingSubMode, // Used for cs
AddSurroundingSubMode, // Used for ys
FilterSubMode, // Used for ! FilterSubMode, // Used for !
IndentSubMode, // Used for = IndentSubMode, // Used for =
RegisterSubMode, // Used for " RegisterSubMode, // Used for "
@@ -206,7 +209,9 @@ enum SubSubMode
ZSubSubMode, // Used for zj, zk ZSubSubMode, // Used for zj, zk
OpenSquareSubSubMode, // Used for [{, {(, [z OpenSquareSubSubMode, // Used for [{, {(, [z
CloseSquareSubSubMode, // Used for ]}, ]), ]z CloseSquareSubSubMode, // Used for ]}, ]), ]z
SearchSubSubMode, SearchSubSubMode, // Used for /, ?
SurroundSubSubMode, // Used for cs, ds, ys
SurroundWithFunctionSubSubMode, // Used for ys{motion}f
CtrlVUnicodeSubSubMode // Used for Ctrl-v based unicode input CtrlVUnicodeSubSubMode // Used for Ctrl-v based unicode input
}; };
@@ -1349,6 +1354,12 @@ QString dotCommandFromSubMode(SubMode submode)
return QLatin1String("d"); return QLatin1String("d");
if (submode == CommentSubMode) if (submode == CommentSubMode)
return QLatin1String("gc"); return QLatin1String("gc");
if (submode == DeleteSurroundingSubMode)
return QLatin1String("ds");
if (submode == ChangeSurroundingSubMode)
return QLatin1String("c");
if (submode == AddSurroundingSubMode)
return QLatin1String("y");
if (submode == ExchangeSubMode) if (submode == ExchangeSubMode)
return QLatin1String("cx"); return QLatin1String("cx");
if (submode == ReplaceWithRegisterSubMode) if (submode == ReplaceWithRegisterSubMode)
@@ -1836,6 +1847,8 @@ public:
bool handleCommentSubMode(const Input &); bool handleCommentSubMode(const Input &);
bool handleReplaceWithRegisterSubMode(const Input &); bool handleReplaceWithRegisterSubMode(const Input &);
bool handleExchangeSubMode(const Input &); bool handleExchangeSubMode(const Input &);
bool handleDeleteChangeSurroundingSubMode(const Input &);
bool handleAddSurroundingSubMode(const Input &);
bool handleFilterSubMode(const Input &); bool handleFilterSubMode(const Input &);
bool handleRegisterSubMode(const Input &); bool handleRegisterSubMode(const Input &);
bool handleShiftSubMode(const Input &); bool handleShiftSubMode(const Input &);
@@ -2101,6 +2114,7 @@ public:
|| g.submode == ExchangeSubMode || g.submode == ExchangeSubMode
|| g.submode == CommentSubMode || g.submode == CommentSubMode
|| g.submode == ReplaceWithRegisterSubMode || g.submode == ReplaceWithRegisterSubMode
|| g.submode == AddSurroundingSubMode
|| g.submode == FilterSubMode || g.submode == FilterSubMode
|| g.submode == IndentSubMode || g.submode == IndentSubMode
|| g.submode == ShiftLeftSubMode || g.submode == ShiftLeftSubMode
@@ -2183,6 +2197,8 @@ public:
void replaceWithRegister(const Range &range); void replaceWithRegister(const Range &range);
void surroundCurrentRange(const Input &input, const QString &prefix = {});
void upCase(const Range &range); void upCase(const Range &range);
void downCase(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 empty, cx{motion} will store the range defined by {motion} here.
// If non-empty, cx{motion} replaces the {motion} with selectText(*exchangeData) // If non-empty, cx{motion} replaces the {motion} with selectText(*exchangeData)
Utils::optional<Range> exchangeRange; 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; } g;
}; };
@@ -3613,6 +3632,7 @@ void FakeVimHandler::Private::finishMovement(const QString &dotCommandMovement)
|| g.submode == CommentSubMode || g.submode == CommentSubMode
|| g.submode == ExchangeSubMode || g.submode == ExchangeSubMode
|| g.submode == ReplaceWithRegisterSubMode || g.submode == ReplaceWithRegisterSubMode
|| g.submode == AddSurroundingSubMode
|| g.submode == YankSubMode || g.submode == YankSubMode
|| g.submode == InvertCaseSubMode || g.submode == InvertCaseSubMode
|| g.submode == DownCaseSubMode || g.submode == DownCaseSubMode
@@ -3644,6 +3664,15 @@ void FakeVimHandler::Private::finishMovement(const QString &dotCommandMovement)
beginEditBlock(); beginEditBlock();
toggleComment(currentRange()); toggleComment(currentRange());
endEditBlock(); 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) { } else if (g.submode == ExchangeSubMode) {
exchangeRange(currentRange()); exchangeRange(currentRange());
} else if (g.submode == ReplaceWithRegisterSubMode } else if (g.submode == ReplaceWithRegisterSubMode
@@ -3749,6 +3778,8 @@ void FakeVimHandler::Private::clearCurrentMode()
g.subsubmode = NoSubSubMode; g.subsubmode = NoSubSubMode;
g.movetype = MoveInclusive; g.movetype = MoveInclusive;
g.gflag = false; g.gflag = false;
g.surroundUpperCaseS = false;
g.surroundFunction.clear();
m_register = '"'; m_register = '"';
g.rangemode = RangeCharMode; g.rangemode = RangeCharMode;
g.currentCommand.clear(); g.currentCommand.clear();
@@ -3908,6 +3939,11 @@ bool FakeVimHandler::Private::handleCommandSubSubMode(const Input &input)
.arg(g.semicolonKey)); .arg(g.semicolonKey));
} }
} else if (g.subsubmode == TextObjectSubSubMode) { } 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')) if (input.is('w'))
selectWordTextObject(g.subsubdata.is('i')); selectWordTextObject(g.subsubdata.is('i'));
else if (input.is('W')) else if (input.is('W'))
@@ -3987,6 +4023,37 @@ bool FakeVimHandler::Private::handleCommandSubSubMode(const Input &input)
.arg(g.subsubmode == OpenSquareSubSubMode ? '[' : ']') .arg(g.subsubmode == OpenSquareSubSubMode ? '[' : ']')
.arg(input.text())); .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 { } else {
handled = false; 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 // Exchange submode is "cx", so we need to switch over from ChangeSubMode here
g.submode = ExchangeSubMode; g.submode = ExchangeSubMode;
handled = true; 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 } else if (g.submode == ChangeSubMode
|| g.submode == DeleteSubMode || g.submode == DeleteSubMode
|| g.submode == YankSubMode) { || g.submode == YankSubMode) {
@@ -4640,6 +4726,9 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input)
int repeat = count(); int repeat = count();
while (--repeat >= 0) while (--repeat >= 0)
redo(); redo();
} else if (input.is('S') && isVisualMode() && hasConfig(ConfigEmulateSurround)) {
g.submode = AddSurroundingSubMode;
g.subsubmode = SurroundSubSubMode;
} else if (input.is('s')) { } else if (input.is('s')) {
handleAs("c%1l"); handleAs("c%1l");
} else if (input.is('S')) { } else if (input.is('S')) {
@@ -4891,6 +4980,69 @@ bool FakeVimHandler::Private::handleExchangeSubMode(const Input &input)
return false; 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 &) bool FakeVimHandler::Private::handleFilterSubMode(const Input &)
{ {
return false; return false;
@@ -7564,6 +7716,77 @@ void FakeVimHandler::Private::replaceWithRegister(const Range &range)
replaceText(range, registerContents(m_register)); 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) void FakeVimHandler::Private::replaceText(const Range &range, const QString &str)
{ {
transformText(range, [&str](const QString &) { return str; } ); transformText(range, [&str](const QString &) { return str; } );

View File

@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>580</width> <width>580</width>
<height>543</height> <height>568</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
@@ -162,6 +162,20 @@
<string>Plugin Emulation</string> <string>Plugin Emulation</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <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"> <item row="1" column="0">
<widget class="QCheckBox" name="checkBoxReplaceWithRegister"> <widget class="QCheckBox" name="checkBoxReplaceWithRegister">
<property name="text"> <property name="text">
@@ -179,17 +193,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="4" column="0">
<widget class="QCheckBox" name="checkBoxArgTextObj"> <widget class="QCheckBox" name="checkBoxVimSurround">
<property name="text"> <property name="text">
<string>argtextobj.vim</string> <string>vim-surround</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkBoxExchange">
<property name="text">
<string>vim-exchange</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@@ -433,6 +433,7 @@ QWidget *FakeVimOptionPage::widget()
m_group.insert(theFakeVimSetting(ConfigEmulateReplaceWithRegister), m_ui.checkBoxReplaceWithRegister); m_group.insert(theFakeVimSetting(ConfigEmulateReplaceWithRegister), m_ui.checkBoxReplaceWithRegister);
m_group.insert(theFakeVimSetting(ConfigEmulateExchange), m_ui.checkBoxExchange); m_group.insert(theFakeVimSetting(ConfigEmulateExchange), m_ui.checkBoxExchange);
m_group.insert(theFakeVimSetting(ConfigEmulateArgTextObj), m_ui.checkBoxArgTextObj); m_group.insert(theFakeVimSetting(ConfigEmulateArgTextObj), m_ui.checkBoxArgTextObj);
m_group.insert(theFakeVimSetting(ConfigEmulateSurround), m_ui.checkBoxVimSurround);
connect(m_ui.pushButtonCopyTextEditorSettings, &QAbstractButton::clicked, connect(m_ui.pushButtonCopyTextEditorSettings, &QAbstractButton::clicked,
this, &FakeVimOptionPage::copyTextEditorSettings); this, &FakeVimOptionPage::copyTextEditorSettings);

View File

@@ -163,6 +163,7 @@ private slots:
void test_vim_replace_with_register_emulation(); void test_vim_replace_with_register_emulation();
void test_vim_exchange_emulation(); void test_vim_exchange_emulation();
void test_vim_arg_text_obj_emulation(); void test_vim_arg_text_obj_emulation();
void test_vim_surround_emulation();
void test_macros(); void test_macros();