diff --git a/src/plugins/fakevim/fakevim_test.cpp b/src/plugins/fakevim/fakevim_test.cpp index 037eb3ef27a..4687c5b76f2 100644 --- a/src/plugins/fakevim/fakevim_test.cpp +++ b/src/plugins/fakevim/fakevim_test.cpp @@ -644,19 +644,6 @@ void FakeVimPlugin::test_vim_insert() KEYS("iXY", "aXY" X "c" N "def"); KEYS("j.", "aXYc" N "dX" X "Yf"); - // insert in visual block mode - data.setText("abc" N "d" X "ef" N "jkl" N "mno" N "pqr"); - KEYS("2j" "2I" "XYZ", "abc" N "d" X "XYZXYZef" N "jXYZXYZkl" N "mXYZXYZno" N "pqr"); - INTEGRITY(false); - - data.setText("abc" N "d" X "ef" N "jkl" N "mno" N "pqr"); - KEYS("2j" "2A" "XYZ", "abc" N "d" X "eXYZXYZf" N "jkXYZXYZl" N "mnXYZXYZo" N "pqr"); - INTEGRITY(false); - - data.setText("abc" N "de" X "f" N "" N "jkl" N "mno"); - KEYS("2jh" "2I" "XYZ", "abc" N "d" X "XYZXYZef" N "" N "jXYZXYZkl" N "mno"); - INTEGRITY(false); - // insert in visual mode data.setText(" a" X "bcde" N " fghij" N " klmno"); KEYS("v2l" "2Ixyz", "xyzxy" X "z abcde" N " fghij" N " klmno"); @@ -1199,12 +1186,36 @@ void FakeVimPlugin::test_vim_change_replace() INTEGRITY(false); // insert in visual block mode - data.setText("abc" N "d" X "ef" N "jkl" N "mno" N "pqr"); - KEYS("2j" "2s" "XYZ", "abc" N "d" X "XYZf" N "jXYZl" N "mXYZo" N "pqr"); + data.setText( + "abc" N + "d" X "ef" N + "" N + "jkl" N + "mno" N + ); + KEYS("2j2sXYZ", + "abc" N + "dXY" X "Zf" N + "" N + "jXYZl" N + "mno" N + ); INTEGRITY(false); - data.setText("abc" N "de" X "f" N "" N "jkl" N "mno"); - KEYS("2jh" "2s" "XYZ", "abc" N "d" X "XYZ" N "" N "jXYZ" N "mno"); + data.setText( + "abc" N + "de" X "f" N + "" N + "jkl" N + "mno" N + ); + KEYS("2jh2sXYZ", + "abc" N + "dXY" X "Z" N + "" N + "jXYZ" N + "mno" N + ); INTEGRITY(false); // change with copy to a register @@ -1322,6 +1333,76 @@ void FakeVimPlugin::test_vim_block_selection() KEYS("u", "\"abc\"\"" X "def\""); } +void FakeVimPlugin::test_vim_block_selection_insert() +{ + TestData data; + setup(&data); + + // insert in visual block mode + data.setText("abc" N "d" X "ef" N "jkl" N "mno" N "pqr"); + KEYS("2j" "2I" "XYZ", "abc" N "d" X "XYZXYZef" N "jXYZXYZkl" N "mXYZXYZno" N "pqr"); + INTEGRITY(false); + + data.setText("abc" N "d" X "ef" N "jkl" N "mno" N "pqr"); + KEYS("2j" "2A" "XYZ", "abc" N "d" X "eXYZXYZf" N "jkXYZXYZl" N "mnXYZXYZo" N "pqr"); + INTEGRITY(false); + + data.setText("abc" N "de" X "f" N "" N "jkl" N "mno"); + KEYS("2jh" "2I" "XYZ", "abc" N "d" X "XYZXYZef" N "" N "jXYZXYZkl" N "mno"); + INTEGRITY(false); + + /* QTCREATORBUG-11378 */ + data.setText( + " abcd" N + " efgh" N + " ijkl" N + " mnop" N + ""); + KEYS("3j$AXYZ", + X " abcdXYZ" N + " efghXYZ" N + " ijklXYZ" N + " mnopXYZ" N + ""); + + data.setText( + " abcd" N + " ef" N + " ghijk" N + " lm" N + ""); + KEYS("3j$AXYZ", + X " abcdXYZ" N + " efXYZ" N + " ghijkXYZ" N + " lmXYZ" N + ""); + + data.setText( + "a" N + "" N + "b" N + "" N + ); + KEYS("j$jAXYZ", + "a" N + "|XYZ" N + "bXYZ" N + "" N + ); + + data.setText( + "abc" N + "" N + "def" N + ); + KEYS("l2jAXYZ", + "a" X "bXYZc" N + " XYZ" N + "deXYZf" N + ); +} + void FakeVimPlugin::test_vim_repeat() { TestData data; diff --git a/src/plugins/fakevim/fakevimhandler.cpp b/src/plugins/fakevim/fakevimhandler.cpp index 3e508a7cc5c..502aa94c06a 100644 --- a/src/plugins/fakevim/fakevimhandler.cpp +++ b/src/plugins/fakevim/fakevimhandler.cpp @@ -158,6 +158,15 @@ enum Mode ExMode }; +enum BlockInsertMode +{ + NoneBlockInsertMode, + AppendBlockInsertMode, + AppendToEndOfLineBlockInsertMode, + InsertBlockInsertMode, + ChangeBlockInsertMode +}; + /*! A \e SubMode is used for things that require one more data item and are 'nested' behind a \l Mode. */ @@ -268,20 +277,45 @@ struct CursorPosition int column; // Position on line. }; -struct Mark +class Mark { +public: Mark(const CursorPosition &pos = CursorPosition(), const QString &fileName = QString()) - : position(pos), fileName(fileName) {} + : m_position(pos), m_fileName(fileName) {} - bool isValid() const { return position.isValid(); } + bool isValid() const { return m_position.isValid(); } bool isLocal(const QString &localFileName) const { - return fileName.isEmpty() || fileName == localFileName; + return m_fileName.isEmpty() || m_fileName == localFileName; } - CursorPosition position; - QString fileName; + /* Return position of mark within given document. + * If saved line number is too big, mark position is at the end of document. + * If line number is in document but column is too big, mark position is at the end of line. + */ + CursorPosition position(const QTextDocument *document) const + { + QTextBlock block = document->findBlockByNumber(m_position.line); + CursorPosition pos; + if (block.isValid()) { + pos.line = m_position.line; + pos.column = qMax(0, qMin(m_position.column, block.length() - 2)); + } else if (document->isEmpty()) { + pos.line = 0; + pos.column = 0; + } else { + pos.line = document->blockCount() - 1; + pos.column = qMax(0, document->lastBlock().length() - 2); + } + return pos; + } + + const QString &fileName() const { return m_fileName; } + +private: + CursorPosition m_position; + QString m_fileName; }; typedef QHash Marks; typedef QHashIterator MarksIterator; @@ -1561,6 +1595,7 @@ public: EventResult handleInsertOrReplaceMode(const Input &); void handleInsertMode(const Input &); void handleReplaceMode(const Input &); + void finishInsertMode(); EventResult handleCommandMode(const Input &); @@ -1755,7 +1790,7 @@ public: bool handleFfTt(QString key); - void initVisualInsertMode(QChar command); + void enterVisualInsertMode(QChar command); void enterReplaceMode(); void enterInsertMode(); void enterInsertOrReplaceMode(Mode mode); @@ -1843,7 +1878,7 @@ public: int m_oldInternalPosition; // copy from last event to check for external changes int m_oldInternalAnchor; int m_register; - bool m_visualBlockInsert; + BlockInsertMode m_visualBlockInsert; // Insert state to get last inserted text. struct InsertState { @@ -1949,6 +1984,8 @@ public: // update marks on undo/redo void updateMarks(const Marks &newMarks); Marks m_marks; // local marks + CursorPosition markLessPosition() const { return mark(QLatin1Char('<')).position(document()); } + CursorPosition markGreaterPosition() const { return mark(QLatin1Char('>')).position(document()); } // vi style configuration QVariant config(int code) const { return theFakeVimSetting(code)->value(); } @@ -2136,7 +2173,7 @@ void FakeVimHandler::Private::init() { m_inFakeVim = false; m_findStartPosition = -1; - m_visualBlockInsert = false; + m_visualBlockInsert = NoneBlockInsertMode; m_fakeEnd = false; m_positionPastEnd = false; m_anchorPastEnd = false; @@ -2406,8 +2443,10 @@ void FakeVimHandler::Private::exportSelection() const int col2 = pos - document()->findBlock(pos).position(); if (col1 > col2) ++anc; - else + else if (!atEndOfLine()) ++pos; + // FIXME: After '$' command (i.e. m_visualTargetColumn == -1), end of selected lines + // should be selected. setAnchorAndPosition(anc, pos); commitCursor(); emit q->requestSetBlockSelection(false); @@ -2435,8 +2474,8 @@ void FakeVimHandler::Private::exportSelection() setAnchorAndPosition(anc, pos); - setMark(QLatin1Char('<'), mark(QLatin1Char('<')).position); - setMark(QLatin1Char('>'), mark(QLatin1Char('>')).position); + setMark(QLatin1Char('<'), markLessPosition()); + setMark(QLatin1Char('>'), markGreaterPosition()); } else { if (g.subsubmode == SearchSubSubMode && !m_searchCursor.isNull()) m_cursor = m_searchCursor; @@ -2914,8 +2953,8 @@ void FakeVimHandler::Private::pushUndoState(bool overwrite) m_redo.clear(); m_lastChangePosition = CursorPosition(document(), pos); if (isVisualMode()) { - setMark(QLatin1Char('<'), mark(QLatin1Char('<')).position); - setMark(QLatin1Char('>'), mark(QLatin1Char('>')).position); + setMark(QLatin1Char('<'), markLessPosition()); + setMark(QLatin1Char('>'), markGreaterPosition()); } m_undoState = State(revision(), m_lastChangePosition, m_marks, m_lastVisualMode, m_lastVisualModeInverted); @@ -3333,7 +3372,7 @@ void FakeVimHandler::Private::updateSelection() it.next(); QTextEdit::ExtraSelection sel; sel.cursor = m_cursor; - setCursorPosition(&sel.cursor, it.value().position); + setCursorPosition(&sel.cursor, it.value().position(document())); sel.cursor.setPosition(sel.cursor.position(), MoveAnchor); sel.cursor.movePosition(Right, KeepAnchor); sel.format = m_cursor.blockCharFormat(); @@ -4046,13 +4085,13 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) } } else if ((!isVisualMode() && input.is('a')) || (isVisualMode() && input.is('A'))) { if (isVisualMode()) { - initVisualInsertMode(QLatin1Char('A')); + enterVisualInsertMode(QLatin1Char('A')); } else { setDotCommand(_("%1a"), count()); moveRight(qMin(rightDist(), 1)); + breakEditBlock(); + enterInsertMode(); } - breakEditBlock(); - enterInsertMode(); } else if (input.is('A')) { breakEditBlock(); moveBehindEndOfLine(); @@ -4076,6 +4115,9 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) leaveVisualMode(); g.submode = ChangeSubMode; finishMovement(); + } else if ((input.is('c') || input.is('s')) && isVisualBlockMode()) { + resetCount(); + enterVisualInsertMode(input.asChar()); } else if (input.is('C')) { setAnchor(); moveToEndOfLine(); @@ -4150,7 +4192,7 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) moveLeft(); } else if (input.is('I')) { if (isVisualMode()) { - initVisualInsertMode(QLatin1Char('I')); + enterVisualInsertMode(QLatin1Char('I')); } else { if (g.gflag) { setDotCommand(_("%1gI"), count()); @@ -4159,11 +4201,9 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) setDotCommand(_("%1I"), count()); moveToFirstNonBlankOnLine(); } + breakEditBlock(); + enterInsertMode(); } - pushUndoState(); - breakEditBlock(); - enterInsertMode(); - setTargetColumn(); } else if (input.isControl('i')) { jump(count()); } else if (input.is('J')) { @@ -4251,13 +4291,6 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) int repeat = count(); while (--repeat >= 0) redo(); - } else if (input.is('s') && isVisualBlockMode()) { - resetCount(); - pushUndoState(); - beginEditBlock(); - initVisualInsertMode(QLatin1Char('s')); - endEditBlock(); - enterInsertMode(); } else if (input.is('s')) { pushUndoState(); leaveVisualMode(); @@ -4299,8 +4332,8 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input) scrollToLine(cursorLine() - sline); } else if (g.gflag && input.is('v')) { if (m_lastVisualMode != NoVisualMode) { - CursorPosition from = mark(QLatin1Char('<')).position; - CursorPosition to = mark(QLatin1Char('>')).position; + CursorPosition from = markLessPosition(); + CursorPosition to = markGreaterPosition(); toggleVisualMode(m_lastVisualMode); setCursorPosition(m_lastVisualModeInverted ? to : from); setAnchor(); @@ -4679,6 +4712,7 @@ EventResult FakeVimHandler::Private::handleInsertOrReplaceMode(const Input &inpu commitInsertState(); invalidateInsertState(); breakEditBlock(); + m_visualBlockInsert = NoneBlockInsertMode; } else if (m_oldInternalPosition == position()) { setTargetColumn(); } @@ -4724,73 +4758,94 @@ void FakeVimHandler::Private::handleReplaceMode(const Input &input) } } -void FakeVimHandler::Private::handleInsertMode(const Input &input) +void FakeVimHandler::Private::finishInsertMode() { - if (input.isEscape()) { - bool newLineAfter = m_insertState.newLineAfter; - bool newLineBefore = m_insertState.newLineBefore; + bool newLineAfter = m_insertState.newLineAfter; + bool newLineBefore = m_insertState.newLineBefore; - // Repeat insertion [count] times. - // One instance was already physically inserted while typing. - if (!m_breakEditBlock && isInsertStateValid()) { - commitInsertState(); + // Repeat insertion [count] times. + // One instance was already physically inserted while typing. + if (!m_breakEditBlock && isInsertStateValid()) { + commitInsertState(); - QString text = m_lastInsertion; - const QString dotCommand = g.dotCommand; - const int repeat = count() - 1; - m_lastInsertion.clear(); - joinPreviousEditBlock(); + QString text = m_lastInsertion; + const QString dotCommand = g.dotCommand; + const int repeat = count() - 1; + m_lastInsertion.clear(); + joinPreviousEditBlock(); - if (newLineAfter) { - text.chop(1); - text.prepend(_("\n")); - } else if (newLineBefore) { - text.prepend(_("")); - } + if (newLineAfter) { + text.chop(1); + text.prepend(_("\n")); + } else if (newLineBefore) { + text.prepend(_("")); + } - replay(text, repeat); + replay(text, repeat); - if (m_visualBlockInsert && !text.contains(QLatin1Char('\n'))) { - const CursorPosition lastAnchor = mark(QLatin1Char('<')).position; - const CursorPosition lastPosition = mark(QLatin1Char('>')).position; - CursorPosition startPos(lastAnchor.line, - qMin(lastPosition.column, lastAnchor.column)); - CursorPosition pos = startPos; - if (dotCommand.endsWith(QLatin1Char('A'))) - pos.column = qMax(lastPosition.column, lastAnchor.column) + 1; - while (pos.line < lastPosition.line) { - ++pos.line; - QTextCursor tc = m_cursor; - setCursorPosition(&tc, pos); - if (pos.line != tc.blockNumber()) - break; - m_cursor = tc; - if (tc.positionInBlock() == pos.column) - replay(text, repeat + 1); + if (m_visualBlockInsert != NoneBlockInsertMode && !text.contains(QLatin1Char('\n'))) { + const CursorPosition lastAnchor = markLessPosition(); + const CursorPosition lastPosition = markGreaterPosition(); + bool change = m_visualBlockInsert == ChangeBlockInsertMode; + const int insertColumn = (m_visualBlockInsert == InsertBlockInsertMode || change) + ? qMin(lastPosition.column, lastAnchor.column) + : qMax(lastPosition.column, lastAnchor.column) + 1; + + CursorPosition pos(lastAnchor.line, insertColumn); + + if (change) + pos.column = m_insertState.pos1 - document()->findBlock(m_insertState.pos1).position(); + + // Cursor position after block insert is on the first selected line, + // last selected column for 's' command, otherwise first selected column. + const int endColumn = change ? qMax(0, m_cursor.positionInBlock() - 1) + : qMin(lastPosition.column, lastAnchor.column); + + while (pos.line < lastPosition.line) { + ++pos.line; + setCursorPosition(&m_cursor, pos); + if (m_visualBlockInsert == AppendToEndOfLineBlockInsertMode) { + moveToEndOfLine(); + } else if (m_visualBlockInsert == AppendBlockInsertMode) { + // Prepend spaces if necessary. + int spaces = pos.column - m_cursor.positionInBlock(); + if (spaces > 0) { + setAnchor(); + m_cursor.insertText(QString(_(" ")).repeated(spaces)); + } + } else if (m_cursor.positionInBlock() != pos.column) { + continue; } - - setCursorPosition(startPos); - } else { - moveLeft(qMin(1, leftDist())); + replay(text, repeat + 1); } - endEditBlock(); - breakEditBlock(); - - m_lastInsertion = text; - g.dotCommand = dotCommand; + setCursorPosition(CursorPosition(lastAnchor.line, endColumn)); } else { moveLeft(qMin(1, leftDist())); } - if (newLineBefore || newLineAfter) - m_lastInsertion.remove(0, m_lastInsertion.indexOf(QLatin1Char('\n')) + 1); - g.dotCommand.append(m_lastInsertion + _("")); + endEditBlock(); + breakEditBlock(); - enterCommandMode(); - setTargetColumn(); - m_ctrlVActive = false; - m_visualBlockInsert = false; + m_lastInsertion = text; + g.dotCommand = dotCommand; + } else { + moveLeft(qMin(1, leftDist())); + } + + if (newLineBefore || newLineAfter) + m_lastInsertion.remove(0, m_lastInsertion.indexOf(QLatin1Char('\n')) + 1); + g.dotCommand.append(m_lastInsertion + _("")); + + enterCommandMode(); + setTargetColumn(); + m_ctrlVActive = false; +} + +void FakeVimHandler::Private::handleInsertMode(const Input &input) +{ + if (input.isEscape()) { + finishInsertMode(); } else if (m_ctrlVActive) { insertInInsertMode(input.raw()); } else if (input.isControl('o')) { @@ -5109,7 +5164,7 @@ int FakeVimHandler::Private::parseLineAddress(QString *cmd) return -1; } cmd->remove(0, 1); - result = m.position.line; + result = m.position(document()).line; } else if (c.isDigit()) { // line with given number result = 0; } else if (c == QLatin1Char('-') || c == QLatin1Char('+')) { // add or subtract from current line number @@ -5644,8 +5699,8 @@ bool FakeVimHandler::Private::handleExMoveCommand(const ExCommand &cmd) return true; } - CursorPosition lastAnchor = mark(QLatin1Char('<')).position; - CursorPosition lastPosition = mark(QLatin1Char('>')).position; + CursorPosition lastAnchor = markLessPosition(); + CursorPosition lastPosition = markGreaterPosition(); recordJump(); setPosition(cmd.range.beginPos); @@ -7397,8 +7452,8 @@ void FakeVimHandler::Private::leaveVisualMode() if (!isVisualMode()) return; - setMark(QLatin1Char('<'), mark(QLatin1Char('<')).position); - setMark(QLatin1Char('>'), mark(QLatin1Char('>')).position); + setMark(QLatin1Char('<'), markLessPosition()); + setMark(QLatin1Char('>'), markGreaterPosition()); m_lastVisualModeInverted = anchor() > position(); if (isVisualLineMode()) { g.rangemode = RangeLineMode; @@ -7655,28 +7710,41 @@ void FakeVimHandler::Private::enterInsertOrReplaceMode(Mode mode) clearLastInsertion(); } -void FakeVimHandler::Private::initVisualInsertMode(QChar command) +void FakeVimHandler::Private::enterVisualInsertMode(QChar command) { - m_visualBlockInsert = isVisualBlockMode(); + if (isVisualBlockMode()) { + bool append = command == QLatin1Char('A'); + bool change = command == QLatin1Char('s') || command == QLatin1Char('c'); - if (m_visualBlockInsert) { setDotCommand(visualDotCommand() + QString::number(count()) + command); leaveVisualMode(); - const CursorPosition lastAnchor = mark(QLatin1Char('<')).position; - const CursorPosition lastPosition = mark(QLatin1Char('>')).position; + const CursorPosition lastAnchor = markLessPosition(); + const CursorPosition lastPosition = markGreaterPosition(); CursorPosition pos(lastAnchor.line, - command == QLatin1Char('A') ? qMax(lastPosition.column, lastAnchor.column) + 1 - : qMin(lastPosition.column, lastAnchor.column)); + append ? qMax(lastPosition.column, lastAnchor.column) + 1 + : qMin(lastPosition.column, lastAnchor.column)); - if (command == QLatin1Char('s')) { + if (append) { + m_visualBlockInsert = m_visualTargetColumn == -1 ? AppendToEndOfLineBlockInsertMode + : AppendBlockInsertMode; + } else if (change) { + m_visualBlockInsert = ChangeBlockInsertMode; + pushUndoState(); + beginEditBlock(); Range range(position(), anchor(), RangeBlockMode); yankText(range, m_register); removeText(range); + endEditBlock(); + } else { + m_visualBlockInsert = InsertBlockInsertMode; } setCursorPosition(pos); + if (m_visualBlockInsert == AppendToEndOfLineBlockInsertMode) + moveBehindEndOfLine(); } else { + m_visualBlockInsert = NoneBlockInsertMode; leaveVisualMode(); if (command == QLatin1Char('I')) { setDotCommand(_("%1i"), count()); @@ -7697,6 +7765,9 @@ void FakeVimHandler::Private::initVisualInsertMode(QChar command) } setAnchor(); + if (m_visualBlockInsert != ChangeBlockInsertMode) + breakEditBlock(); + enterInsertMode(); } void FakeVimHandler::Private::enterCommandMode(Mode returnToMode) @@ -8129,14 +8200,14 @@ bool FakeVimHandler::Private::jumpToMark(QChar mark, bool backTickMode) return false; } if (!m.isLocal(m_currentFileName)) { - emit q->jumpToGlobalMark(mark, backTickMode, m.fileName); + emit q->jumpToGlobalMark(mark, backTickMode, m.fileName()); return false; } if ((mark == QLatin1Char('\'') || mark == QLatin1Char('`')) && !m_jumpListUndo.isEmpty()) m_jumpListUndo.pop(); recordJump(); - setCursorPosition(m.position); + setCursorPosition(m.position(document())); if (!backTickMode) moveToFirstNonBlankOnLine(); if (g.submode == NoSubMode) diff --git a/src/plugins/fakevim/fakevimplugin.h b/src/plugins/fakevim/fakevimplugin.h index db1ced99f0c..c3addea26b4 100644 --- a/src/plugins/fakevim/fakevimplugin.h +++ b/src/plugins/fakevim/fakevimplugin.h @@ -70,6 +70,7 @@ private slots: void test_vim_change_a_word(); void test_vim_change_replace(); void test_vim_block_selection(); + void test_vim_block_selection_insert(); void test_vim_repeat(); void test_vim_search(); void test_vim_indent();