forked from qt-creator/qt-creator
FakeVim: Add emulation for vim-surround plugin
Change-Id: If450d04dd89a1707ab05806522fbf4cc987d454b Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -120,6 +120,7 @@ FakeVimSettings::FakeVimSettings()
|
||||
createAction(ConfigEmulateReplaceWithRegister, false, "ReplaceWithRegister");
|
||||
createAction(ConfigEmulateExchange, false, "exchange");
|
||||
createAction(ConfigEmulateArgTextObj, false, "argtextobj");
|
||||
createAction(ConfigEmulateSurround, false, "surround");
|
||||
}
|
||||
|
||||
FakeVimSettings::~FakeVimSettings()
|
||||
|
@@ -114,6 +114,7 @@ enum FakeVimSettingsCode
|
||||
ConfigEmulateReplaceWithRegister,
|
||||
ConfigEmulateExchange,
|
||||
ConfigEmulateArgTextObj,
|
||||
ConfigEmulateSurround,
|
||||
|
||||
ConfigBlinkingCursor
|
||||
};
|
||||
|
@@ -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 "
|
||||
@@ -206,7 +209,9 @@ enum SubSubMode
|
||||
ZSubSubMode, // Used for zj, zk
|
||||
OpenSquareSubSubMode, // 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
|
||||
};
|
||||
|
||||
@@ -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; } );
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
||||
|
Reference in New Issue
Block a user