/* SPDX-FileCopyrightText: 2008-2009 Erlend Hamberg SPDX-FileCopyrightText: 2008 Evgeniy Ivanov SPDX-FileCopyrightText: 2009 Paul Gideon Dann SPDX-FileCopyrightText: 2011 Svyatoslav Kuzmich SPDX-FileCopyrightText: 2012-2013 Simon St James SPDX-License-Identifier: LGPL-2.0-or-later */ #include "katebuffer.h" #include "katecmd.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katerenderer.h" #include "kateundomanager.h" #include "kateviewhelpers.h" #include "kateviewinternal.h" #include "kateviinputmode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KateVi; NormalViMode::NormalViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : ModeBase() { m_view = view; m_viewInternal = viewInternal; m_viInputModeManager = viInputModeManager; m_stickyColumn = -1; m_lastMotionWasVisualLineUpOrDown = false; m_currentMotionWasVisualLineUpOrDown = false; // FIXME: make configurable m_extraWordCharacters = QString(); m_matchingItems[QStringLiteral("/*")] = QStringLiteral("*/"); m_matchingItems[QStringLiteral("*/")] = QStringLiteral("-/*"); m_matchItemRegex = generateMatchingItemRegex(); m_scroll_count_limit = 1000; // Limit of count for scroll commands. m_pendingResetIsDueToExit = false; m_isRepeatedTFcommand = false; m_lastMotionWasLinewiseInnerBlock = false; m_motionCanChangeWholeVisualModeSelection = false; resetParser(); // initialise with start configuration m_isUndo = false; connect(doc()->undoManager(), &KateUndoManager::undoStart, this, &NormalViMode::undoBeginning); connect(doc()->undoManager(), &KateUndoManager::undoEnd, this, &NormalViMode::undoEnded); updateYankHighlightAttrib(); connect(view, &KTextEditor::View::configChanged, this, &NormalViMode::updateYankHighlightAttrib); connect(doc(), &KTextEditor::DocumentPrivate::aboutToInvalidateMovingInterfaceContent, this, &NormalViMode::clearYankHighlight); } NormalViMode::~NormalViMode() { qDeleteAll(m_highlightedYanks); } /** * parses a key stroke to check if it's a valid (part of) a command * @return true if a command was completed and executed, false otherwise */ bool NormalViMode::handleKeypress(const QKeyEvent *e) { const int keyCode = e->key(); // ignore modifier keys alone if (keyCode == Qt::Key_Shift || keyCode == Qt::Key_Control || keyCode == Qt::Key_Alt || keyCode == Qt::Key_Meta) { return false; } clearYankHighlight(); if (keyCode == Qt::Key_Escape || (keyCode == Qt::Key_C && e->modifiers() == CONTROL_MODIFIER) || (keyCode == Qt::Key_BracketLeft && e->modifiers() == CONTROL_MODIFIER)) { m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Block); m_pendingResetIsDueToExit = true; // Vim in weird as if we e.g. i it claims (in the status bar) to still be in insert mode, // but behaves as if it's in normal mode. I'm treating the status bar thing as a bug and just exiting // insert mode altogether. m_viInputModeManager->setTemporaryNormalMode(false); reset(); return true; } const QChar key = KeyParser::self()->KeyEventToQChar(*e); const QChar lastChar = m_keys.isEmpty() ? QChar::Null : m_keys.at(m_keys.size() - 1); const bool waitingForRegisterOrCharToSearch = this->waitingForRegisterOrCharToSearch(); // Use replace caret when reading a character for "r" if (key == QLatin1Char('r') && !waitingForRegisterOrCharToSearch) { m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Underline); } m_keysVerbatim.append(KeyParser::self()->decodeKeySequence(key)); // allow shifted numbers for Dvorak and Co., bug 388138 if ((keyCode >= Qt::Key_0 && keyCode <= Qt::Key_9 && lastChar != QLatin1Char('"')) // key 0-9 && (m_countTemp != 0 || keyCode != Qt::Key_0) // first digit can't be 0 && (!waitingForRegisterOrCharToSearch) // Not in the middle of "find char" motions or replacing char. && (e->modifiers() | Qt::ShiftModifier) == Qt::ShiftModifier) { m_countTemp *= 10; m_countTemp += keyCode - Qt::Key_0; return true; } else if (m_countTemp != 0) { m_count = getCount() * m_countTemp; m_countTemp = 0; m_iscounted = true; } m_keys.append(key); if (m_viInputModeManager->macroRecorder()->isRecording() && key == QLatin1Char('q')) { // Need to special case this "finish macro" q, as the "begin macro" q // needs a parameter whereas the finish macro does not. m_viInputModeManager->macroRecorder()->stop(); resetParser(); return true; } if ((key == QLatin1Char('/') || key == QLatin1Char('?')) && !waitingForRegisterOrCharToSearch) { // Special case for "/" and "?": these should be motions, but this is complicated by // the fact that the user must interact with the search bar before the range of the // motion can be determined. // We hack around this by showing the search bar immediately, and, when the user has // finished interacting with it, have the search bar send a "synthetic" keypresses // that will either abort everything (if the search was aborted) or "complete" the motion // otherwise. m_positionWhenIncrementalSearchBegan = m_view->cursorPosition(); if (key == QLatin1Char('/')) { commandSearchForward(); } else { commandSearchBackward(); } return true; } // Special case: "cw" and "cW" work the same as "ce" and "cE" if the cursor is // on a non-blank. This is because Vim interprets "cw" as change-word, and a // word does not include the following white space. (:help cw in vim) if ((m_keys == QLatin1String("cw") || m_keys == QLatin1String("cW")) && !getCharUnderCursor().isSpace()) { // Special case of the special case: :-) // If the cursor is at the end of the current word rewrite to "cl" const bool isWORD = (m_keys.at(1) == QLatin1Char('W')); const KTextEditor::Cursor currentPosition(m_view->cursorPosition()); const KTextEditor::Cursor endOfWordOrWORD = (isWORD ? findWORDEnd(currentPosition.line(), currentPosition.column() - 1, true) : findWordEnd(currentPosition.line(), currentPosition.column() - 1, true)); if (currentPosition == endOfWordOrWORD) { m_keys = QStringLiteral("cl"); } else { if (isWORD) { m_keys = QStringLiteral("cE"); } else { m_keys = QStringLiteral("ce"); } } } if (m_keys[0] == char16_t(Qt::Key_QuoteDbl)) { if (m_keys.size() < 2) { return true; // waiting for a register } else { QChar r = m_keys[1].toLower(); if ((r >= QLatin1Char('0') && r <= QLatin1Char('9')) || (r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('-') || r == QLatin1Char('+') || r == QLatin1Char('*') || r == QLatin1Char('#') || r == QLatin1Char('^')) { m_register = m_keys[1]; m_keys.clear(); return true; } else { resetParser(); return true; } } } // if we have any matching commands so far, check which ones still match if (!m_matchingCommands.isEmpty()) { int n = m_matchingCommands.size() - 1; // remove commands not matching anymore for (int i = n; i >= 0; i--) { if (!commands().at(m_matchingCommands.at(i)).matches(m_keys)) { if (commands().at(m_matchingCommands.at(i)).needsMotion()) { // "cache" command needing a motion for later m_motionOperatorIndex = m_matchingCommands.at(i); } m_matchingCommands.remove(i); } } // check if any of the matching commands need a motion/text object, if so // push the current command length to m_awaitingMotionOrTextObject so one // knows where to split the command between the operator and the motion for (int i = 0; i < m_matchingCommands.size(); i++) { if (commands().at(m_matchingCommands.at(i)).needsMotion()) { m_awaitingMotionOrTextObject.push(m_keys.size()); break; } } } else { // go through all registered commands and put possible matches in m_matchingCommands for (size_t i = 0; i < commands().size(); i++) { if (commands().at(i).matches(m_keys)) { m_matchingCommands.push_back(i); if (commands().at(i).needsMotion() && commands().at(i).pattern().length() == m_keys.size()) { m_awaitingMotionOrTextObject.push(m_keys.size()); } } } } // this indicates where in the command string one should start looking for a motion command int checkFrom = (m_awaitingMotionOrTextObject.isEmpty() ? 0 : m_awaitingMotionOrTextObject.top()); // Use operator-pending caret when reading a motion for an operator // in normal mode. We need to check that we are indeed in normal mode // since visual mode inherits from it. if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode && !m_awaitingMotionOrTextObject.isEmpty()) { m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Half); } // look for matching motion commands from position 'checkFrom' // FIXME: if checkFrom hasn't changed, only motions whose index is in // m_matchingMotions should be checked bool motionExecuted = false; if (checkFrom < m_keys.size()) { for (size_t i = 0; i < motions().size(); i++) { const QString motion = m_keys.mid(checkFrom); if (motions().at(i).matches(motion)) { m_lastMotionWasLinewiseInnerBlock = false; m_matchingMotions.push_back(i); // if it matches exact, we have found the motion command to execute if (motions().at(i).matchesExact(motion)) { m_currentMotionWasVisualLineUpOrDown = false; motionExecuted = true; if (checkFrom == 0) { // no command given before motion, just move the cursor to wherever // the motion says it should go to Range r = motions().at(i).execute(this); m_motionCanChangeWholeVisualModeSelection = motions().at(i).canChangeWholeVisualModeSelection(); if (!motions().at(i).canLandInsideFoldingRange()) { // jump over folding regions since we are just moving the cursor // except for motions that can end up inside ranges (e.g. n/N, f/F, %, #) int currLine = m_view->cursorPosition().line(); int delta = r.endLine - currLine; int vline = m_view->textFolding().lineToVisibleLine(currLine); r.endLine = m_view->textFolding().visibleLineToLine(qMax(vline + delta, 0) /* ensure we have a valid line */); } if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } // make sure the position is valid before moving the cursor there // TODO: can this be simplified? :/ if (r.valid && r.endLine >= 0 && (r.endLine == 0 || r.endLine <= doc()->lines() - 1) && r.endColumn >= 0) { if (r.endColumn >= doc()->lineLength(r.endLine) && doc()->lineLength(r.endLine) > 0) { r.endColumn = doc()->lineLength(r.endLine) - 1; } goToPos(r); // in the case of VisualMode we need to remember the motion commands as well. if (!m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->clearCurrentChangeLog(); } } else { qCDebug(LOG_KTE) << "Invalid position: (" << r.endLine << "," << r.endColumn << ")"; } resetParser(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } m_lastMotionWasVisualLineUpOrDown = m_currentMotionWasVisualLineUpOrDown; break; } else { // execute the specified command and supply the position returned from // the motion m_commandRange = motions().at(i).execute(this); m_linewiseCommand = motions().at(i).isLineWise(); // if we didn't get an explicit start position, use the current cursor position if (m_commandRange.startLine == -1) { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); } // special case: When using the "w" motion in combination with an operator and // the last word moved over is at the end of a line, the end of that word // becomes the end of the operated text, not the first word in the next line. if (motions().at(i).pattern() == QLatin1String("w") || motions().at(i).pattern() == QLatin1String("W")) { if (m_commandRange.endLine != m_commandRange.startLine && m_commandRange.endColumn == getFirstNonBlank(m_commandRange.endLine)) { m_commandRange.endLine--; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine); } } m_commandWithMotion = true; if (m_commandRange.valid) { executeCommand(&commands().at(m_motionOperatorIndex)); } else { qCDebug(LOG_KTE) << "Invalid range: " << "from (" << m_commandRange.startLine << "," << m_commandRange.startColumn << ")" << "to (" << m_commandRange.endLine << "," << m_commandRange.endColumn << ")"; } if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Block); } m_commandWithMotion = false; reset(); break; } } } } } if (this->waitingForRegisterOrCharToSearch()) { // If we are waiting for a char to search or a new register, // don't translate next character; we need the actual character so that e.g. // 'ab' is translated to 'fb' if the mappings 'a' -> 'f' and 'b' -> something else // exist. m_viInputModeManager->keyMapper()->setDoNotMapNextKeypress(); } if (motionExecuted) { return true; } // if we have only one match, check if it is a perfect match and if so, execute it // if it's not waiting for a motion or a text object if (m_matchingCommands.size() == 1) { if (commands().at(m_matchingCommands.at(0)).matchesExact(m_keys) && !commands().at(m_matchingCommands.at(0)).needsMotion()) { if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Block); } const Command &cmd = commands().at(m_matchingCommands.at(0)); executeCommand(&cmd); // check if reset() should be called. some commands in visual mode should not end visual mode if (cmd.shouldReset()) { reset(); m_view->setBlockSelection(false); } resetParser(); return true; } } else if (m_matchingCommands.size() == 0 && m_matchingMotions.size() == 0) { resetParser(); // A bit ugly: we haven't made use of the key event, // but don't want "typeable" keypresses (e.g. a, b, 3, etc) to be marked // as unused as they will then be added to the document, but we don't // want to swallow all keys in case this was a shortcut. // So say we made use of it if and only if it was *not* a shortcut. return e->type() != QEvent::ShortcutOverride; } m_matchingMotions.clear(); return true; // TODO - need to check this - it's currently required for making tests pass, but seems odd. } /** * (re)set to start configuration. This is done when a command is completed * executed or when a command is aborted */ void NormalViMode::resetParser() { m_keys.clear(); m_keysVerbatim.clear(); m_count = 0; m_oneTimeCountOverride = -1; m_iscounted = false; m_countTemp = 0; m_register = QChar::Null; m_findWaitingForChar = false; m_matchingCommands.clear(); m_matchingMotions.clear(); m_awaitingMotionOrTextObject.clear(); m_motionOperatorIndex = 0; m_commandWithMotion = false; m_linewiseCommand = true; m_deleteCommand = false; m_commandShouldKeepSelection = false; m_currentChangeEndMarker = KTextEditor::Cursor::invalid(); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Block); } } // reset the command parser void NormalViMode::reset() { resetParser(); m_commandRange.startLine = -1; m_commandRange.startColumn = -1; } void NormalViMode::beginMonitoringDocumentChanges() { connect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &NormalViMode::textInserted); connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &NormalViMode::textRemoved); } void NormalViMode::executeCommand(const Command *cmd) { const ViMode originalViMode = m_viInputModeManager->getCurrentViMode(); cmd->execute(this); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } // if the command was a change, and it didn't enter insert mode, store the key presses so that // they can be repeated with '.' if (m_viInputModeManager->getCurrentViMode() != ViMode::InsertMode && m_viInputModeManager->getCurrentViMode() != ViMode::ReplaceMode) { if (cmd->isChange() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { m_viInputModeManager->storeLastChangeCommand(); } // when we transition to visual mode, remember the command in the keys history (V, v, ctrl-v, ...) // this will later result in buffer filled with something like this "Vjj>" which we can use later with repeat "." const bool commandSwitchedToVisualMode = ((originalViMode == ViMode::NormalMode) && m_viInputModeManager->isAnyVisualMode()); if (!commandSwitchedToVisualMode) { m_viInputModeManager->clearCurrentChangeLog(); } } // make sure the cursor does not end up after the end of the line KTextEditor::Cursor c(m_view->cursorPosition()); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { int lineLength = doc()->lineLength(c.line()); if (c.column() >= lineLength) { if (lineLength == 0) { c.setColumn(0); } else { c.setColumn(lineLength - 1); } } updateCursor(c); } } //////////////////////////////////////////////////////////////////////////////// // COMMANDS AND OPERATORS //////////////////////////////////////////////////////////////////////////////// /** * enter insert mode at the cursor position */ bool NormalViMode::commandEnterInsertMode() { m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode after the current character */ bool NormalViMode::commandEnterInsertModeAppend() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(c.column() + 1); // if empty line, the cursor should start at column 0 if (doc()->lineLength(c.line()) == 0) { c.setColumn(0); } // cursor should never be in a column > number of columns if (c.column() > doc()->lineLength(c.line())) { c.setColumn(doc()->lineLength(c.line())); } updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * start insert mode after the last character of the line */ bool NormalViMode::commandEnterInsertModeAppendEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } bool NormalViMode::commandEnterInsertModeBeforeFirstNonBlankInLine() { KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); cursor.setColumn(c); updateCursor(cursor); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode at the last insert position */ bool NormalViMode::commandEnterInsertModeLast() { KTextEditor::Cursor c = m_viInputModeManager->marks()->getInsertStopped(); if (c.isValid()) { updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandEnterVisualLineMode() { if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { reset(); return true; } return startVisualLineMode(); } bool NormalViMode::commandEnterVisualBlockMode() { if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { reset(); return true; } return startVisualBlockMode(); } bool NormalViMode::commandReselectVisual() { // start last visual mode and set start = `< and cursor = `> KTextEditor::Cursor c1 = m_viInputModeManager->marks()->getSelectionStart(); KTextEditor::Cursor c2 = m_viInputModeManager->marks()->getSelectionFinish(); // we should either get two valid cursors or two invalid cursors Q_ASSERT(c1.isValid() == c2.isValid()); if (c1.isValid() && c2.isValid()) { m_viInputModeManager->getViVisualMode()->setStart(c1); bool returnValue = false; switch (m_viInputModeManager->getViVisualMode()->getLastVisualMode()) { case ViMode::VisualMode: returnValue = commandEnterVisualMode(); break; case ViMode::VisualLineMode: returnValue = commandEnterVisualLineMode(); break; case ViMode::VisualBlockMode: returnValue = commandEnterVisualBlockMode(); break; default: Q_ASSERT("invalid visual mode"); } m_viInputModeManager->getViVisualMode()->goToPos(c2); return returnValue; } else { error(QStringLiteral("No previous visual selection")); } return false; } bool NormalViMode::commandEnterVisualMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode) { reset(); return true; } return startVisualMode(); } bool NormalViMode::commandToOtherEnd() { if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->switchStartEnd(); return true; } return false; } bool NormalViMode::commandEnterReplaceMode() { m_stickyColumn = -1; m_viInputModeManager->getViReplaceMode()->setCount(getCount()); return startReplaceMode(); } bool NormalViMode::commandDeleteLine() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r; r.startLine = c.line(); r.endLine = c.line() + getCount() - 1; int column = c.column(); bool ret = deleteRange(r, LineWise); c = m_view->cursorPosition(); if (column > doc()->lineLength(c.line()) - 1) { column = doc()->lineLength(c.line()) - 1; } if (column < 0) { column = 0; } if (c.line() > doc()->lines() - 1) { c.setLine(doc()->lines() - 1); } c.setColumn(column); m_stickyColumn = -1; updateCursor(c); m_deleteCommand = true; return ret; } bool NormalViMode::commandDelete() { m_deleteCommand = true; return deleteRange(m_commandRange, getOperationMode()); } bool NormalViMode::commandDeleteToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = CharWise; m_commandRange.endColumn = KateVi::EOL; switch (m_viInputModeManager->getCurrentViMode()) { case ViMode::NormalMode: m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); m_commandRange.endLine = c.line() + getCount() - 1; break; case ViMode::VisualMode: case ViMode::VisualLineMode: m = LineWise; break; case ViMode::VisualBlockMode: m_commandRange.normalize(); m = Block; break; default: /* InsertMode and ReplaceMode will never call this method. */ Q_ASSERT(false); } bool r = deleteRange(m_commandRange, m); switch (m) { case CharWise: c.setColumn(doc()->lineLength(c.line()) - 1); break; case LineWise: c.setLine(m_commandRange.startLine); c.setColumn(getFirstNonBlank(qMin(doc()->lastLine(), m_commandRange.startLine))); break; case Block: c.setLine(m_commandRange.startLine); c.setColumn(m_commandRange.startColumn - 1); break; } // make sure cursor position is valid after deletion if (c.line() < 0) { c.setLine(0); } if (c.line() > doc()->lastLine()) { c.setLine(doc()->lastLine()); } if (c.column() > doc()->lineLength(c.line()) - 1) { c.setColumn(doc()->lineLength(c.line()) - 1); } if (c.column() < 0) { c.setColumn(0); } updateCursor(c); m_deleteCommand = true; return r; } bool NormalViMode::commandMakeLowercase() { KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text.chop(1); // don't need '\n' at the end; } QString lowerCase = text.toLower(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, lowerCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeLowercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeLowercase(); } bool NormalViMode::commandMakeUppercase() { if (!m_commandRange.valid) { return false; } KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text.chop(1); // don't need '\n' at the end; } QString upperCase = text.toUpper(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, upperCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeUppercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeUppercase(); } bool NormalViMode::commandChangeCase() { QString text; KTextEditor::Range range; KTextEditor::Cursor c(m_view->cursorPosition()); // in visual mode, the range is from start position to end position... if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode || m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(c2.column() + 1); } else { c.setColumn(c.column() + 1); } range.setRange(c, c2); // ... in visual line mode, the range is from column 0 on the first line to // the line length of the last line... } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(doc()->lineLength(c2.line())); c.setColumn(0); } else { c.setColumn(doc()->lineLength(c.line())); c2.setColumn(0); } range.setRange(c, c2); // ... and in normal mode the range is from the current position to the // current position + count } else { KTextEditor::Cursor c2 = c; c2.setColumn(c.column() + getCount()); if (c2.column() > doc()->lineLength(c.line())) { c2.setColumn(doc()->lineLength(c.line())); } range.setRange(c, c2); } bool block = m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode; // get the text the command should operate on text = doc()->text(range, block); // for every character, switch its case for (int i = 0; i < text.length(); i++) { if (text.at(i).isUpper()) { text[i] = text.at(i).toLower(); } else if (text.at(i).isLower()) { text[i] = text.at(i).toUpper(); } } // replace the old text with the modified text doc()->replaceText(range, text, block); // in normal mode, move the cursor to the right, in visual mode move the // cursor to the start of the selection if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(range.end()); } else { updateCursor(range.start()); } return true; } bool NormalViMode::commandChangeCaseRange() { OperationMode m = getOperationMode(); QString changedCase = getRange(m_commandRange, m); if (m == LineWise) { changedCase.chop(1); // don't need '\n' at the end; } KTextEditor::Range range = KTextEditor::Range(m_commandRange.startLine, m_commandRange.startColumn, m_commandRange.endLine, m_commandRange.endColumn); // get the text the command should operate on // for every character, switch its case for (int i = 0; i < changedCase.length(); i++) { if (changedCase.at(i).isUpper()) { changedCase[i] = changedCase.at(i).toLower(); } else if (changedCase.at(i).isLower()) { changedCase[i] = changedCase.at(i).toUpper(); } } doc()->replaceText(range, changedCase, m == Block); return true; } bool NormalViMode::commandChangeCaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; // -1 is for excluding '\0' if (!commandChangeCaseRange()) { return false; } KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); if (getCount() > 1) { updateCursor(c); } else { updateCursor(start); } return true; } bool NormalViMode::commandOpenNewLineUnder() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); doc()->newLine(m_view); m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandOpenNewLineOver() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() == 0) { doc()->insertLine(0, QString()); c.setColumn(0); c.setLine(0); updateCursor(c); } else { c.setLine(c.line() - 1); c.setColumn(getLine(c.line()).length()); updateCursor(c); doc()->newLine(m_view); } m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandJoinLines() { KTextEditor::Cursor c(m_view->cursorPosition()); unsigned int from = c.line(); unsigned int to = c.line() + ((getCount() == 1) ? 1 : getCount() - 1); // if we were given a range of lines, this information overrides the previous if (m_commandRange.startLine != -1 && m_commandRange.endLine != -1) { m_commandRange.normalize(); c.setLine(m_commandRange.startLine); from = m_commandRange.startLine; to = m_commandRange.endLine; } if (to >= (unsigned int)doc()->lines()) { return false; } bool nonEmptyLineFound = false; for (unsigned int lineNum = from; lineNum <= to; lineNum++) { if (!doc()->line(lineNum).isEmpty()) { nonEmptyLineFound = true; } } const int firstNonWhitespaceOnLastLine = doc()->kateTextLine(to).firstChar(); QString leftTrimmedLastLine; if (firstNonWhitespaceOnLastLine != -1) { leftTrimmedLastLine = doc()->line(to).mid(firstNonWhitespaceOnLastLine); } joinLines(from, to); if (nonEmptyLineFound && leftTrimmedLastLine.isEmpty()) { // joinLines won't have added a trailing " ", whereas Vim does - follow suit. doc()->insertText(KTextEditor::Cursor(from, doc()->lineLength(from)), QStringLiteral(" ")); } // Position cursor just before first non-whitesspace character of what was the last line joined. c.setColumn(doc()->lineLength(from) - leftTrimmedLastLine.length() - 1); if (c.column() >= 0) { updateCursor(c); } m_deleteCommand = true; return true; } bool NormalViMode::commandChange() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = getOperationMode(); doc()->setUndoMergeAllEdits(true); commandDelete(); if (m == LineWise) { // if we deleted several lines, insert an empty line and put the cursor there. doc()->insertLine(m_commandRange.startLine, QString()); c.setLine(m_commandRange.startLine); c.setColumn(0); } else if (m == Block) { // block substitute can be simulated by first deleting the text // (done above) and then starting block prepend. return commandPrependToBlock(); } else { if (m_commandRange.startLine < m_commandRange.endLine) { c.setLine(m_commandRange.startLine); } c.setColumn(m_commandRange.startColumn); } updateCursor(c); setCount(0); // The count was for the motion, not the insertion. commandEnterInsertMode(); // correct indentation level if (m == LineWise) { m_view->align(); } m_deleteCommand = true; return true; } bool NormalViMode::commandChangeToEOL() { commandDeleteToEOL(); if (getOperationMode() == Block) { return commandPrependToBlock(); } m_deleteCommand = true; return commandEnterInsertModeAppend(); } bool NormalViMode::commandChangeLine() { m_deleteCommand = true; KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(0); updateCursor(c); doc()->setUndoMergeAllEdits(true); // if count >= 2 start by deleting the whole lines if (getCount() >= 2) { Range r(c.line(), 0, c.line() + getCount() - 2, 0, InclusiveMotion); deleteRange(r); } // ... then delete the _contents_ of the last line, but keep the line Range r(c.line(), c.column(), c.line(), doc()->lineLength(c.line()) - 1, InclusiveMotion); deleteRange(r, CharWise, true); // ... then enter insert mode if (getOperationMode() == Block) { return commandPrependToBlock(); } commandEnterInsertModeAppend(); // correct indentation level m_view->align(); return true; } bool NormalViMode::commandSubstituteChar() { if (commandDeleteChar()) { // The count is only used for deletion of chars; the inserted text is not repeated setCount(0); return commandEnterInsertMode(); } m_deleteCommand = true; return false; } bool NormalViMode::commandSubstituteLine() { m_deleteCommand = true; return commandChangeLine(); } bool NormalViMode::commandYank() { bool r = false; QString yankedText; OperationMode m = getOperationMode(); yankedText = getRange(m_commandRange, m); highlightYank(m_commandRange, m); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return r; } bool NormalViMode::commandYankLine() { KTextEditor::Cursor c(m_view->cursorPosition()); QString lines; int linenum = c.line(); for (int i = 0; i < getCount(); i++) { lines.append(getLine(linenum + i) + QLatin1Char('\n')); } Range yankRange(linenum, 0, linenum + getCount() - 1, getLine(linenum + getCount() - 1).length(), InclusiveMotion); highlightYank(yankRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, lines, LineWise); yankToClipBoard(chosen_register, lines); return true; } bool NormalViMode::commandYankToEOL() { OperationMode m = CharWise; KTextEditor::Cursor c(m_view->cursorPosition()); MotionType motion = m_commandRange.motionType; m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine) - 1; m_commandRange.motionType = InclusiveMotion; switch (m_viInputModeManager->getCurrentViMode()) { case ViMode::NormalMode: m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); break; case ViMode::VisualMode: case ViMode::VisualLineMode: m = LineWise; { VisualViMode *visual = static_cast(this); visual->setStart(KTextEditor::Cursor(visual->getStart().line(), 0)); } break; case ViMode::VisualBlockMode: m = Block; break; default: /* InsertMode and ReplaceMode will never call this method. */ Q_ASSERT(false); } const QString &yankedText = getRange(m_commandRange, m); m_commandRange.motionType = motion; highlightYank(m_commandRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return true; } // Insert the text in the given register after the cursor position. // This is the non-g version of paste, so the cursor will usually // end up on the last character of the pasted text, unless the text // was multi-line or linewise in which case it will end up // on the *first* character of the pasted text(!) // If linewise, will paste after the current line. bool NormalViMode::commandPaste() { return paste(AfterCurrentPosition, false, false); } // As with commandPaste, except that the text is pasted *at* the cursor position bool NormalViMode::commandPasteBefore() { return paste(AtCurrentPosition, false, false); } // As with commandPaste, except that the cursor will generally be placed *after* the // last pasted character (assuming the last pasted character is not at the end of the line). // If linewise, cursor will be at the beginning of the line *after* the last line of pasted text, // unless that line is the last line of the document; then it will be placed at the beginning of the // last line pasted. bool NormalViMode::commandgPaste() { return paste(AfterCurrentPosition, true, false); } // As with commandgPaste, except that it pastes *at* the current cursor position or, if linewise, // at the current line. bool NormalViMode::commandgPasteBefore() { return paste(AtCurrentPosition, true, false); } bool NormalViMode::commandIndentedPaste() { return paste(AfterCurrentPosition, false, true); } bool NormalViMode::commandIndentedPasteBefore() { return paste(AtCurrentPosition, false, true); } bool NormalViMode::commandDeleteChar() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column(), c.line(), c.column() + getCount(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.endColumn > doc()->lineLength(r.startLine)) { r.endColumn = doc()->lineLength(r.startLine); } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandDeleteCharBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column() - getCount(), c.line(), c.column(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.startColumn < 0) { r.startColumn = 0; } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandReplaceCharacter() { QString key = KeyParser::self()->decodeKeySequence(m_keys.right(1)); // Filter out some special keys. const int keyCode = KeyParser::self()->encoded2qt(m_keys.right(1)); switch (keyCode) { case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Delete: case Qt::Key_Insert: case Qt::Key_Backspace: case Qt::Key_CapsLock: return true; case Qt::Key_Return: case Qt::Key_Enter: key = QStringLiteral("\n"); } bool r; if (m_viInputModeManager->isAnyVisualMode()) { OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text.chop(1); // don't need '\n' at the end; } static const QRegularExpression nonNewlineRegex(QStringLiteral("[^\n]")); text.replace(nonNewlineRegex, key); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); r = doc()->replaceText(range, text, m == Block); } else { KTextEditor::Cursor c1(m_view->cursorPosition()); KTextEditor::Cursor c2(m_view->cursorPosition()); c2.setColumn(c2.column() + getCount()); if (c2.column() > doc()->lineLength(m_view->cursorPosition().line())) { return false; } r = doc()->replaceText(KTextEditor::Range(c1, c2), key.repeated(getCount())); updateCursor(c1); } return r; } bool NormalViMode::commandSwitchToCmdLine() { QString initialText; if (m_viInputModeManager->isAnyVisualMode()) { // if in visual mode, make command range == visual selection m_viInputModeManager->getViVisualMode()->saveRangeMarks(); initialText = QStringLiteral("'<,'>"); } else if (getCount() != 1) { // if a count is given, the range [current line] to [current line] + // count should be prepended to the command line initialText = QLatin1String(".,.+") + QString::number(getCount() - 1); } m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::Command, initialText); m_commandShouldKeepSelection = true; return true; } bool NormalViMode::commandSearchBackward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchBackward); return true; } bool NormalViMode::commandSearchForward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchForward); return true; } bool NormalViMode::commandUndo() { // See BUG #328277 m_viInputModeManager->clearCurrentChangeLog(); if (doc()->undoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) { doc()->editEnd(); } doc()->undo(); if (mapped) { doc()->editStart(); } if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandRedo() { if (doc()->redoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) { doc()->editEnd(); } doc()->redo(); if (mapped) { doc()->editStart(); } if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandSetMark() { KTextEditor::Cursor c(m_view->cursorPosition()); QChar mark = m_keys.at(m_keys.size() - 1); m_viInputModeManager->marks()->setUserMark(mark, c); return true; } bool NormalViMode::commandIndentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), 1); return true; } bool NormalViMode::commandUnindentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), -1); return true; } bool NormalViMode::commandIndentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; int col = getLine(line2).length(); doc()->indent(KTextEditor::Range(line1, 0, line2, col), getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandUnindentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; doc()->indent(KTextEditor::Range(line1, 0, line2, doc()->lineLength(line2)), -getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandScrollPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageDown(); } } return true; } bool NormalViMode::commandScrollPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageUp(); } } return true; } bool NormalViMode::commandScrollHalfPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageUp(false, true); } } return true; } bool NormalViMode::commandScrollHalfPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageDown(false, true); } } return true; } bool NormalViMode::commandCenterView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine() + linesDisplayed() / 2; const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandCenterViewOnNonBlank() { return commandCenterView(true); } bool NormalViMode::commandCenterViewOnCursor() { return commandCenterView(false); } bool NormalViMode::commandTopView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandTopViewOnNonBlank() { return commandTopView(true); } bool NormalViMode::commandTopViewOnCursor() { return commandTopView(false); } bool NormalViMode::commandBottomView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->endLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandBottomViewOnNonBlank() { return commandBottomView(true); } bool NormalViMode::commandBottomViewOnCursor() { return commandBottomView(false); } bool NormalViMode::commandAbort() { m_pendingResetIsDueToExit = true; reset(); return true; } bool NormalViMode::commandPrintCharacterCode() { QChar ch = getCharUnderCursor(); if (ch == QChar::Null) { message(QStringLiteral("NUL")); } else { int code = ch.unicode(); QString dec = QString::number(code); QString hex = QString::number(code, 16); QString oct = QString::number(code, 8); if (oct.length() < 3) { oct.prepend(QLatin1Char('0')); } if (code > 0x80 && code < 0x1000) { hex.prepend((code < 0x100 ? QLatin1String("00") : QLatin1String("0"))); } message(i18n("'%1' %2, Hex %3, Octal %4", ch, dec, hex, oct)); } return true; } bool NormalViMode::commandRepeatLastChange() { const int repeatCount = getCount(); resetParser(); if (repeatCount > 1) { m_oneTimeCountOverride = repeatCount; } doc()->editStart(); m_viInputModeManager->repeatLastChange(); doc()->editEnd(); return true; } bool NormalViMode::commandAlignLine() { const int line = m_view->cursorPosition().line(); KTextEditor::Range alignRange(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, 0)); doc()->align(m_view, alignRange); return true; } bool NormalViMode::commandAlignLines() { m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, 0); KTextEditor::Cursor end(m_commandRange.endLine, 0); doc()->align(m_view, KTextEditor::Range(start, end)); return true; } bool NormalViMode::commandAddToNumber() { addToNumberUnderCursor(getCount()); return true; } bool NormalViMode::commandSubtractFromNumber() { addToNumberUnderCursor(-getCount()); return true; } bool NormalViMode::commandPrependToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); // move cursor to top left corner of selection m_commandRange.normalize(); c.setColumn(m_commandRange.startColumn); c.setLine(m_commandRange.startLine); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setBlockPrependMode(m_commandRange); return startInsertMode(); } bool NormalViMode::commandAppendToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.normalize(); if (m_stickyColumn == (unsigned int)KateVi::EOL) { // append to EOL // move cursor to end of first line c.setLine(m_commandRange.startLine); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, AppendEOL); } else { m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, Append); // move cursor to top right corner of selection c.setColumn(m_commandRange.endColumn + 1); c.setLine(m_commandRange.startLine); updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandGoToNextJump() { KTextEditor::Cursor c = getNextJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandGoToPrevJump() { KTextEditor::Cursor c = getPrevJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandSwitchToLeftView() { switchView(Left); return true; } bool NormalViMode::commandSwitchToDownView() { switchView(Down); return true; } bool NormalViMode::commandSwitchToUpView() { switchView(Up); return true; } bool NormalViMode::commandSwitchToRightView() { switchView(Right); return true; } bool NormalViMode::commandSwitchToNextView() { switchView(Next); return true; } bool NormalViMode::commandSplitHoriz() { return executeKateCommand(QStringLiteral("split")); } bool NormalViMode::commandSplitVert() { return executeKateCommand(QStringLiteral("vsplit")); } bool NormalViMode::commandCloseView() { return executeKateCommand(QStringLiteral("close")); } bool NormalViMode::commandSwitchToNextTab() { QString command = QStringLiteral("bn"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandSwitchToPrevTab() { QString command = QStringLiteral("bp"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandFormatLine() { KTextEditor::Cursor c(m_view->cursorPosition()); reformatLines(c.line(), c.line() + getCount() - 1); return true; } bool NormalViMode::commandFormatLines() { reformatLines(m_commandRange.startLine, m_commandRange.endLine); return true; } bool NormalViMode::commandCollapseToplevelNodes() { m_view->slotFoldToplevelNodes(); return true; } bool NormalViMode::commandStartRecordingMacro() { const QChar reg = m_keys[m_keys.size() - 1]; m_viInputModeManager->macroRecorder()->start(reg); return true; } bool NormalViMode::commandReplayMacro() { // "@" will have been added to the log; it needs to be cleared // *before* we replay the macro keypresses, else it can cause an infinite loop // if the macro contains a "." m_viInputModeManager->clearCurrentChangeLog(); const QChar reg = m_keys[m_keys.size() - 1]; const unsigned int count = getCount(); resetParser(); doc()->editStart(); for (unsigned int i = 0; i < count; i++) { m_viInputModeManager->macroRecorder()->replay(reg); } doc()->editEnd(); return true; } bool NormalViMode::commandCloseNocheck() { return executeKateCommand(QStringLiteral("q!")); } bool NormalViMode::commandCloseWrite() { return executeKateCommand(QStringLiteral("wq")); } bool NormalViMode::commandCollapseLocal() { int line = m_view->cursorPosition().line(); bool actionDone = false; while (!actionDone && line > -1) { actionDone = m_view->foldLine(line--).isValid(); } return true; } bool NormalViMode::commandExpandAll() { // FIXME: is toplevel same as all? m_view->slotExpandToplevelNodes(); return true; } bool NormalViMode::commandExpandLocal() { int line = m_view->cursorPosition().line(); return m_view->unfoldLine(line); } bool NormalViMode::commandToggleRegionVisibility() { // FIXME: is this equivalent to Vim (or a desired change)? m_view->slotToggleFolding(); return true; } //////////////////////////////////////////////////////////////////////////////// // MOTIONS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::motionDown() { return goLineDown(); } Range NormalViMode::motionUp() { return goLineUp(); } Range NormalViMode::motionLeft() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn -= getCount(); if (r.endColumn < 0) { r.endColumn = 0; } return r; } Range NormalViMode::motionRight() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn += getCount(); // make sure end position isn't > line length if (r.endColumn > doc()->lineLength(r.endLine)) { r.endColumn = doc()->lineLength(r.endLine); } return r; } Range NormalViMode::motionPageDown() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine += linesDisplayed(); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } return r; } Range NormalViMode::motionPageUp() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine -= linesDisplayed(); if (r.endLine < 0) { r.endLine = 0; } return r; } Range NormalViMode::motionHalfPageDown() { if (commandScrollHalfPageDown()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionHalfPageUp() { if (commandScrollHalfPageUp()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionDownToFirstNonBlank() { Range r = goLineDown(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionUpToFirstNonBlank() { Range r = goLineUp(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionWordForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; // Special case: If we're already on the very last character in the document, the motion should be // inclusive so the last character gets included if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { r.motionType = InclusiveMotion; } else { for (int i = 0; i < getCount(); i++) { c = findNextWordStart(c.line(), c.column()); // stop when at the last char in the document if (!c.isValid()) { c = doc()->documentEnd(); // if we still haven't "used up the count", make the motion inclusive, so that the last char // is included if (i < getCount()) { r.motionType = InclusiveMotion; } break; } } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWordBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findNextWORDStart(c.line(), c.column()); // stop when at the last char in the document if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWordEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWORDEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfPrevWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEndOfPrevWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } void NormalViMode::stickStickyColumnToEOL() { if (m_keys.size() == 1) { m_stickyColumn = KateVi::EOL; } } Range NormalViMode::motionToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); stickStickyColumnToEOL(); unsigned int line = c.line() + (getCount() - 1); Range r(line, doc()->lineLength(line) - 1, InclusiveMotion); return r; } Range NormalViMode::motionToLastNonBlank() { KTextEditor::Cursor c(m_view->cursorPosition()); stickStickyColumnToEOL(); unsigned int line = c.line() + (getCount() - 1); const auto text_line = doc()->plainKateTextLine(line); Range r(line, text_line.previousNonSpaceChar(text_line.length()), InclusiveMotion); return r; } Range NormalViMode::motionToColumn0() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); Range r(cursor.line(), 0, ExclusiveMotion); return r; } Range NormalViMode::motionToFirstCharacterOfLine() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); Range r(cursor.line(), c, ExclusiveMotion); return r; } Range NormalViMode::motionFindChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = cursor.column(); for (int i = 0; i < getCount(); i++) { matchColumn = line.indexOf(QStringView(m_keys).right(1), matchColumn + 1); if (matchColumn == -1) { break; } } Range r; if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionFindCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = -1; int hits = 0; int i = cursor.column() - 1; while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } Range r(cursor, ExclusiveMotion); if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionToChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; Range r; r.endColumn = -1; r.endLine = -1; int matchColumn = cursor.column() + (m_isRepeatedTFcommand ? 2 : 1); for (int i = 0; i < getCount(); i++) { const int lastColumn = matchColumn; matchColumn = line.indexOf(m_keys.right(1), matchColumn + ((i > 0) ? 1 : 0)); if (matchColumn == -1) { if (m_isRepeatedTFcommand) { matchColumn = lastColumn; } else { return Range::invalid(); } break; } } r.endColumn = matchColumn - 1; r.endLine = cursor.line(); m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionToCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); const int originalColumn = cursor.column(); m_stickyColumn = -1; int matchColumn = originalColumn - 1; int hits = 0; int i = cursor.column() - (m_isRepeatedTFcommand ? 2 : 1); Range r(cursor, ExclusiveMotion); while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } if (hits == getCount()) { r.endColumn = matchColumn + 1; r.endLine = cursor.line(); } else { r.valid = false; } m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionRepeatlastTF() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToChar(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToCharBackward(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionRepeatlastTFBackward() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToCharBackward(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToChar(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionToLineFirst() { Range r(getCount() - 1, 0, InclusiveMotion); m_stickyColumn = -1; if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToLineLast() { Range r(doc()->lines() - 1, 0, InclusiveMotion); m_stickyColumn = -1; // don't use getCount() here, no count and a count of 1 is different here... if (m_count != 0) { r.endLine = m_count - 1; } if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToScreenColumn() { m_stickyColumn = -1; KTextEditor::Cursor c(m_view->cursorPosition()); int column = getCount() - 1; if (doc()->lineLength(c.line()) - 1 < (int)getCount() - 1) { column = doc()->lineLength(c.line()) - 1; } return Range(c.line(), column, ExclusiveMotion); } Range NormalViMode::motionToMark() { Range r; m_stickyColumn = -1; QChar reg = m_keys.at(m_keys.size() - 1); KTextEditor::Cursor c = m_viInputModeManager->marks()->getMarkPosition(reg); if (c.isValid()) { r.endLine = c.line(); r.endColumn = c.column(); } else { error(i18n("Mark not set: %1", m_keys.right(1))); r.valid = false; } r.jump = true; return r; } Range NormalViMode::motionToMarkLine() { Range r = motionToMark(); r.endColumn = getFirstNonBlank(r.endLine); r.jump = true; m_stickyColumn = -1; return r; } Range NormalViMode::motionToMatchingItem() { Range r; int lines = doc()->lines(); // If counted, then it's not a motion to matching item anymore, // but a motion to the N'th percentage of the document if (isCounted()) { int count = getCount(); if (count > 100) { return r; } r.endLine = qRound(lines * count / 100.0) - 1; r.endColumn = 0; return r; } KTextEditor::Cursor c(m_view->cursorPosition()); QString l = getLine(); int n1 = l.indexOf(m_matchItemRegex, c.column()); m_stickyColumn = -1; if (n1 < 0) { return Range::invalid(); } const auto bracketChar = l.at(n1); // use Kate's built-in matching bracket finder for brackets if (bracketChar == QLatin1Char('(') || bracketChar == QLatin1Char(')') || bracketChar == QLatin1Char('{') || bracketChar == QLatin1Char('}') || bracketChar == QLatin1Char('[') || bracketChar == QLatin1Char(']')) { // findMatchingBracket requires us to move the cursor to the // first bracket, but we don't want the cursor to really move // in case this is e.g. a yank, so restore it to its original // position afterwards. c.setColumn(n1); const KTextEditor::Cursor oldCursorPos = m_view->cursorPosition(); updateCursor(c); // find the matching one c = m_viewInternal->findMatchingBracket(); if (c > m_view->cursorPosition()) { c.setColumn(c.column() - 1); } m_view->setCursorPosition(oldCursorPos); } else { // text item we want to find a matching item for static const QRegularExpression boundaryRegex(QStringLiteral("\\b|\\s|$"), QRegularExpression::UseUnicodePropertiesOption); const int n2 = l.indexOf(boundaryRegex, n1); QString item = l.mid(n1, n2 - n1); QString matchingItem = m_matchingItems[item]; int toFind = 1; int line = c.line(); int column = n2 - item.length(); bool reverse = false; if (matchingItem.startsWith(QLatin1Char('-'))) { matchingItem.remove(0, 1); // remove the '-' reverse = true; } // make sure we don't hit the text item we started the search from if (column == 0 && reverse) { column -= item.length(); } int itemIdx; int matchItemIdx; while (toFind > 0) { if (reverse) { itemIdx = l.lastIndexOf(item, column - 1); matchItemIdx = l.lastIndexOf(matchingItem, column - 1); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx > matchItemIdx)) { ++toFind; } } else { itemIdx = l.indexOf(item, column); matchItemIdx = l.indexOf(matchingItem, column); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx < matchItemIdx)) { ++toFind; } } if (matchItemIdx != -1 || itemIdx != -1) { if (!reverse) { column = qMin((unsigned int)itemIdx, (unsigned int)matchItemIdx); } else { column = qMax(itemIdx, matchItemIdx); } } if (matchItemIdx != -1) { // match on current line if (matchItemIdx == column) { --toFind; c.setLine(line); c.setColumn(column); } } else { // no match, advance one line if possible (reverse) ? --line : ++line; column = 0; if ((!reverse && line >= lines) || (reverse && line < 0)) { r.valid = false; break; } else { l = getLine(line); } } } } r.endLine = c.line(); r.endColumn = c.column(); r.jump = true; return r; } Range NormalViMode::motionToNextBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '{' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // With a command, do not include the { or the cursor position. r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '}' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextOccurrence() { const QString word = getWordUnderCursor(); Searcher *searcher = m_viInputModeManager->searcher(); const Range match = searcher->findWordForMotion(word, false, getWordRangeUnderCursor().start(), getCount()); if (searcher->lastSearchWrapped()) { m_view->showSearchWrappedHint(/*isReverseSearch*/ false); } return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToPrevOccurrence() { const QString word = getWordUnderCursor(); Searcher *searcher = m_viInputModeManager->searcher(); const Range match = searcher->findWordForMotion(word, true, getWordRangeUnderCursor().start(), getCount()); if (searcher->lastSearchWrapped()) { m_view->showSearchWrappedHint(/*isReverseSearch*/ true); } return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToFirstLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int)m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() - m_view->cursorPosition().line() + 1; } else { lines_to_go = -m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToMiddleLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int)m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() / 2 - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() / 2 - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToLastLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int)m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToNextVisualLine() { return goVisualLineUpDown(getCount()); } Range NormalViMode::motionToPrevVisualLine() { return goVisualLineUpDown(-getCount()); } Range NormalViMode::motionToPreviousSentence() { KTextEditor::Cursor c = findSentenceStart(); int linenum = c.line(); int column = c.column(); const bool skipSpaces = doc()->line(linenum).isEmpty(); if (skipSpaces) { linenum--; if (linenum >= 0) { column = doc()->line(linenum).size() - 1; } } for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } if (column < 0 && !line.isEmpty()) { column = line.size() - 1; } for (int j = column; j >= 0; j--) { if (skipSpaces || QStringLiteral(".?!").indexOf(line.at(j)) != -1) { c.setLine(i); c.setColumn(j); updateCursor(c); c = findSentenceStart(); return Range(c, InclusiveMotion); } } column = line.size() - 1; } return Range(0, 0, InclusiveMotion); } Range NormalViMode::motionToNextSentence() { KTextEditor::Cursor c = findSentenceEnd(); int linenum = c.line(); int column = c.column() + 1; const bool skipSpaces = doc()->line(linenum).isEmpty(); for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } for (int j = column; j < line.size(); j++) { if (!line.at(j).isSpace()) { return Range(i, j, InclusiveMotion); } } column = 0; } c = doc()->documentEnd(); return Range(c, InclusiveMotion); } Range NormalViMode::motionToBeforeParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line--; } while (line >= 0 && getLine(line + 1).length() == 0); while (line > 0 && getLine(line).length() != 0) { line--; } } if (line < 0) { line = 0; } Range r(line, 0, InclusiveMotion); return r; } Range NormalViMode::motionToAfterParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line++; } while (line <= doc()->lines() - 1 && getLine(line - 1).length() == 0); while (line < doc()->lines() - 1 && getLine(line).length() != 0) { line++; } } if (line >= doc()->lines()) { line = doc()->lines() - 1; } // if we ended up on the last line, the cursor should be placed on the last column int column = (line == doc()->lines() - 1) ? qMax(getLine(line).length() - 1, 0) : 0; return Range(line, column, InclusiveMotion); } Range NormalViMode::motionToIncrementalSearchMatch() { return Range(m_positionWhenIncrementalSearchBegan.line(), m_positionWhenIncrementalSearchBegan.column(), m_view->cursorPosition().line(), m_view->cursorPosition().column(), ExclusiveMotion); } //////////////////////////////////////////////////////////////////////////////// // TEXT OBJECTS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::textObjectAWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWordEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } // need to start search in column-1 because it might be a one-character word KTextEditor::Cursor c2(c.line(), c.column() - 1); for (int i = 0; i < getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } Range NormalViMode::textObjectAWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWORDEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } KTextEditor::Cursor c2(c); for (int i = 0; i < getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } KTextEditor::Cursor NormalViMode::findSentenceStart() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(); int column = c.column(); int prev = column; for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); const int lineLength = line.size(); if (i != linenum) { column = lineLength; } // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor((i != linenum) ? i + 1 : i, prev); } prev = column; for (int j = column; j >= 0; j--) { if (j == lineLength || line.at(j).isSpace()) { int lastSpace = j--; for (; j >= 0 && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j--) { ; } if (j >= 0 && QStringLiteral(".!?").indexOf(line.at(j)) != -1) { if (lastSpace == lineLength) { // If the line ends with one of .!?, then the sentence starts from the next line. return KTextEditor::Cursor(i + 1, 0); } return KTextEditor::Cursor(i, prev); } j = lastSpace; } else { prev = j; } } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findSentenceEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(); int column = c.column(); int j = 0; int prev = 0; for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor(linenum, j); } // Iterating over the line to reach any '.', '!', '?' for (j = column; j < line.size(); j++) { if (QStringLiteral(".!?").indexOf(line.at(j)) != -1) { prev = j++; // Skip possible closing characters. for (; j < line.size() && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j++) { ; } if (j >= line.size()) { return KTextEditor::Cursor(i, j - 1); } // And hopefully we're done... if (line.at(j).isSpace()) { return KTextEditor::Cursor(i, j - 1); } j = prev; } } linenum = i; prev = column; column = 0; } return KTextEditor::Cursor(linenum, j - 1); } KTextEditor::Cursor NormalViMode::findParagraphStart() { KTextEditor::Cursor c(m_view->cursorPosition()); const bool firstBlank = doc()->line(c.line()).isEmpty(); int prev = c.line(); for (int i = prev; i >= 0; i--) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i + 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i--; for (; i >= 0 && doc()->line(i).isEmpty(); i--, prev--) { ; } } return KTextEditor::Cursor(prev, 0); } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findParagraphEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int prev = c.line(); int lines = doc()->lines(); const bool firstBlank = doc()->line(prev).isEmpty(); for (int i = prev; i < lines; i++) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i - 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i++; for (; i < lines && doc()->line(i).isEmpty(); i++, prev++) { ; } } int length = doc()->lineLength(prev); return KTextEditor::Cursor(prev, (length <= 0) ? 0 : length - 1); } } return doc()->documentEnd(); } Range NormalViMode::textObjectInnerSentence() { Range r; KTextEditor::Cursor c1 = findSentenceStart(); KTextEditor::Cursor c2 = findSentenceEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectASentence() { int i; Range r = textObjectInnerSentence(); const QString &line = doc()->line(r.endLine); // Skip whitespaces and tabs. for (i = r.endColumn + 1; i < line.size(); i++) { if (!line.at(i).isSpace()) { break; } } r.endColumn = i - 1; // Remove preceding spaces. if (r.startColumn != 0) { if (r.endColumn == line.size() - 1 && !line.at(r.endColumn).isSpace()) { const QString &line = doc()->line(r.startLine); for (i = r.startColumn - 1; i >= 0; i--) { if (!line.at(i).isSpace()) { break; } } r.startColumn = i + 1; } } return r; } Range NormalViMode::textObjectInnerParagraph() { Range r; KTextEditor::Cursor c1 = findParagraphStart(); KTextEditor::Cursor c2 = findParagraphEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectAParagraph() { Range r = textObjectInnerParagraph(); int lines = doc()->lines(); if (r.endLine + 1 < lines) { // If the next line is empty, remove all subsequent empty lines. // Otherwise we'll grab the next paragraph. if (doc()->line(r.endLine + 1).isEmpty()) { for (int i = r.endLine + 1; i < lines && doc()->line(i).isEmpty(); i++) { r.endLine++; } r.endColumn = 0; } else { KTextEditor::Cursor prev = m_view->cursorPosition(); KTextEditor::Cursor c(r.endLine + 1, 0); updateCursor(c); c = findParagraphEnd(); updateCursor(prev); r.endLine = c.line(); r.endColumn = c.column(); } } else if (doc()->lineLength(r.startLine) > 0) { // We went too far, but maybe we can grab previous empty lines. for (int i = r.startLine - 1; i >= 0 && doc()->line(i).isEmpty(); i--) { r.startLine--; } r.startColumn = 0; updateCursor(KTextEditor::Cursor(r.startLine, r.startColumn)); } else { // We went too far and we're on empty lines, do nothing. return Range::invalid(); } return r; } Range NormalViMode::textObjectAQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), false); } Range NormalViMode::textObjectInnerQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), true); } Range NormalViMode::textObjectAQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), false); } Range NormalViMode::textObjectInnerQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), true); } Range NormalViMode::textObjectABackQuote() { return findSurroundingQuotes(QLatin1Char('`'), false); } Range NormalViMode::textObjectInnerBackQuote() { return findSurroundingQuotes(QLatin1Char('`'), true); } Range NormalViMode::textObjectAParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), false, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectInnerParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), true, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectABracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), false, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectInnerBracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), true, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectACurlyBracket() { return findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), false, QLatin1Char('{'), QLatin1Char('}')); } Range NormalViMode::textObjectInnerCurlyBracket() { const Range allBetweenCurlyBrackets = findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), true, QLatin1Char('{'), QLatin1Char('}')); // Emulate the behaviour of vim, which tries to leave the closing bracket on its own line // if it was originally on a line different to that of the opening bracket. Range innerCurlyBracket(allBetweenCurlyBrackets); if (innerCurlyBracket.startLine != innerCurlyBracket.endLine) { const bool openingBraceIsLastCharOnLine = innerCurlyBracket.startColumn == doc()->line(innerCurlyBracket.startLine).length(); const bool stuffToDeleteIsAllOnEndLine = openingBraceIsLastCharOnLine && innerCurlyBracket.endLine == innerCurlyBracket.startLine + 1; const QString textLeadingClosingBracket = doc()->line(innerCurlyBracket.endLine).mid(0, innerCurlyBracket.endColumn + 1); const bool closingBracketHasLeadingNonWhitespace = !textLeadingClosingBracket.trimmed().isEmpty(); if (stuffToDeleteIsAllOnEndLine) { if (!closingBracketHasLeadingNonWhitespace) { // Nothing there to select - abort. return Range::invalid(); } else { // Shift the beginning of the range to the start of the line containing the closing bracket. innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; } } else { if (openingBraceIsLastCharOnLine && !closingBracketHasLeadingNonWhitespace) { innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; m_lastMotionWasLinewiseInnerBlock = true; } { // The line containing the end bracket is left alone if the end bracket is preceded by just whitespace, // else we need to delete everything (i.e. end up with "{}") if (!closingBracketHasLeadingNonWhitespace) { // Shrink the endpoint of the range so that it ends at the end of the line above, // leaving the closing bracket on its own line. innerCurlyBracket.endLine--; innerCurlyBracket.endColumn = doc()->line(innerCurlyBracket.endLine).length(); } } } } return innerCurlyBracket; } Range NormalViMode::textObjectAInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), false, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectInnerInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), true, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectAComma() { return textObjectComma(false); } Range NormalViMode::textObjectInnerComma() { return textObjectComma(true); } QRegularExpression NormalViMode::generateMatchingItemRegex() const { QString pattern(QStringLiteral("\\[|\\]|\\{|\\}|\\(|\\)|")); for (QString s : std::as_const(m_matchingItems)) { if (s.startsWith(QLatin1Char('-'))) { s.remove(0, 1); } s.replace(QLatin1Char('*'), QStringLiteral("\\*")); s.replace(QLatin1Char('+'), QStringLiteral("\\+")); s.replace(QLatin1Char('['), QStringLiteral("\\[")); s.replace(QLatin1Char(']'), QStringLiteral("\\]")); s.replace(QLatin1Char('('), QStringLiteral("\\(")); s.replace(QLatin1Char(')'), QStringLiteral("\\)")); s.replace(QLatin1Char('{'), QStringLiteral("\\{")); s.replace(QLatin1Char('}'), QStringLiteral("\\}")); s.append(QLatin1Char('|')); pattern.append(s); } // remove extra "|" at the end pattern.chop(1); return QRegularExpression(pattern, QRegularExpression::UseUnicodePropertiesOption); } // returns the operation mode that should be used. this is decided by using the following heuristic: // 1. if we're in visual block mode, it should be Block // 2. if we're in visual line mode OR the range spans several lines, it should be LineWise // 3. if neither of these is true, CharWise is returned // 4. there are some motion that makes all operator charwise, if we have one of them mode will be CharWise OperationMode NormalViMode::getOperationMode() const { OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { m = Block; } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode || (m_commandRange.startLine != m_commandRange.endLine && m_viInputModeManager->getCurrentViMode() != ViMode::VisualMode)) { m = LineWise; } if (m_commandWithMotion && !m_linewiseCommand) { m = CharWise; } if (m_lastMotionWasLinewiseInnerBlock) { m = LineWise; } return m; } bool NormalViMode::paste(PasteLocation pasteLocation, bool isgPaste, bool isIndentedPaste) { KTextEditor::Cursor pasteAt(m_view->cursorPosition()); KTextEditor::Cursor cursorAfterPaste = pasteAt; QChar reg = getChosenRegister(UnnamedRegister); OperationMode m = getRegisterFlag(reg); QString textToInsert = getRegisterContent(reg); const bool isTextMultiLine = textToInsert.count(QLatin1Char('\n')) > 0; // In temporary normal mode, p/P act as gp/gP. isgPaste |= m_viInputModeManager->getTemporaryNormalMode(); if (textToInsert.isEmpty()) { error(i18n("Nothing in register %1", reg.toLower())); return false; } if (getCount() > 1) { textToInsert = textToInsert.repeated(getCount()); // FIXME: does this make sense for blocks? } if (m == LineWise) { pasteAt.setColumn(0); if (isIndentedPaste) { // Note that this does indeed work if there is no non-whitespace on the current line or if // the line is empty! static const QRegularExpression nonWhitespaceRegex(QStringLiteral("[^\\s]")); const QString pasteLineString = doc()->line(pasteAt.line()); const QString leadingWhiteSpaceOnCurrentLine = pasteLineString.mid(0, pasteLineString.indexOf(nonWhitespaceRegex)); const QString leadingWhiteSpaceOnFirstPastedLine = textToInsert.mid(0, textToInsert.indexOf(nonWhitespaceRegex)); // QString has no "left trim" method, bizarrely. while (textToInsert[0].isSpace()) { textToInsert = textToInsert.mid(1); } textToInsert.prepend(leadingWhiteSpaceOnCurrentLine); // Remove the last \n, temporarily: we're going to alter the indentation of each pasted line // by doing a search and replace on '\n's, but don't want to alter this one. textToInsert.chop(1); textToInsert.replace(QLatin1Char('\n') + leadingWhiteSpaceOnFirstPastedLine, QLatin1Char('\n') + leadingWhiteSpaceOnCurrentLine); textToInsert.append(QLatin1Char('\n')); // Re-add the temporarily removed last '\n'. } if (pasteLocation == AfterCurrentPosition) { textToInsert.chop(1); // remove the last \n pasteAt.setColumn(doc()->lineLength(pasteAt.line())); // paste after the current line and ... textToInsert.prepend(QLatin1Char('\n')); // ... prepend a \n, so the text starts on a new line cursorAfterPaste.setLine(cursorAfterPaste.line() + 1); } if (isgPaste) { cursorAfterPaste.setLine(cursorAfterPaste.line() + textToInsert.count(QLatin1Char('\n'))); } } else { if (pasteLocation == AfterCurrentPosition) { // Move cursor forward one before we paste. The position after the paste must also // be updated accordingly. if (getLine(pasteAt.line()).length() > 0) { pasteAt.setColumn(pasteAt.column() + 1); } cursorAfterPaste = pasteAt; } const bool leaveCursorAtStartOfPaste = isTextMultiLine && !isgPaste; if (!leaveCursorAtStartOfPaste) { cursorAfterPaste = cursorPosAtEndOfPaste(pasteAt, textToInsert); if (!isgPaste) { cursorAfterPaste.setColumn(cursorAfterPaste.column() - 1); } } } doc()->editStart(); if (m_view->selection()) { pasteAt = m_view->selectionRange().start(); doc()->removeText(m_view->selectionRange()); } doc()->insertText(pasteAt, textToInsert, m == Block); doc()->editEnd(); if (cursorAfterPaste.line() >= doc()->lines()) { cursorAfterPaste.setLine(doc()->lines() - 1); } updateCursor(cursorAfterPaste); return true; } KTextEditor::Cursor NormalViMode::cursorPosAtEndOfPaste(const KTextEditor::Cursor pasteLocation, const QString &pastedText) { KTextEditor::Cursor cAfter = pasteLocation; const int lineCount = pastedText.count(QLatin1Char('\n')) + 1; if (lineCount == 1) { cAfter.setColumn(cAfter.column() + pastedText.length()); } else { const int lastLineLength = pastedText.size() - (pastedText.lastIndexOf(QLatin1Char('\n')) + 1); cAfter.setColumn(lastLineLength); cAfter.setLine(cAfter.line() + lineCount - 1); } return cAfter; } void NormalViMode::joinLines(unsigned int from, unsigned int to) const { // make sure we don't try to join lines past the document end if (to >= (unsigned int)(doc()->lines())) { to = doc()->lines() - 1; } // joining one line is a no-op if (from == to) { return; } doc()->joinLines(from, to); } void NormalViMode::reformatLines(unsigned int from, unsigned int to) const { // BUG #340550: Do not remove empty lines when reformatting KTextEditor::DocumentPrivate *document = doc(); auto isNonEmptyLine = [](QStringView text) { for (int i = 0; i < text.length(); ++i) { if (!text.at(i).isSpace()) { return true; } } return false; }; for (; from < to; ++from) { if (isNonEmptyLine(document->line(from))) { break; } } for (; to > from; --to) { if (isNonEmptyLine(document->line(to))) { break; } } document->editStart(); joinLines(from, to); document->wrapText(from, to); document->editEnd(); } int NormalViMode::getFirstNonBlank(int line) const { if (line < 0) { line = m_view->cursorPosition().line(); } // doc()->plainKateTextLine returns NULL if the line is out of bounds. Kate::TextLine l = doc()->plainKateTextLine(line); int c = l.firstChar(); return (c < 0) ? 0 : c; } // Tries to shrinks toShrink so that it fits tightly around rangeToShrinkTo. void NormalViMode::shrinkRangeAroundCursor(Range &toShrink, const Range &rangeToShrinkTo) const { if (!toShrink.valid || !rangeToShrinkTo.valid) { return; } KTextEditor::Cursor cursorPos = m_view->cursorPosition(); if (rangeToShrinkTo.startLine >= cursorPos.line()) { if (rangeToShrinkTo.startLine > cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.startLine == cursorPos.line()); if (rangeToShrinkTo.startColumn > cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (rangeToShrinkTo.endLine <= cursorPos.line()) { if (rangeToShrinkTo.endLine < cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.endLine == cursorPos.line()); if (rangeToShrinkTo.endColumn < cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (toShrink.startLine <= rangeToShrinkTo.startLine) { if (toShrink.startLine < rangeToShrinkTo.startLine) { toShrink.startLine = rangeToShrinkTo.startLine; toShrink.startColumn = rangeToShrinkTo.startColumn; } Q_ASSERT(toShrink.startLine == rangeToShrinkTo.startLine); if (toShrink.startColumn < rangeToShrinkTo.startColumn) { toShrink.startColumn = rangeToShrinkTo.startColumn; } } if (toShrink.endLine >= rangeToShrinkTo.endLine) { if (toShrink.endLine > rangeToShrinkTo.endLine) { toShrink.endLine = rangeToShrinkTo.endLine; toShrink.endColumn = rangeToShrinkTo.endColumn; } Q_ASSERT(toShrink.endLine == rangeToShrinkTo.endLine); if (toShrink.endColumn > rangeToShrinkTo.endColumn) { toShrink.endColumn = rangeToShrinkTo.endColumn; } } } Range NormalViMode::textObjectComma(bool inner) const { // Basic algorithm: look left and right of the cursor for all combinations // of enclosing commas and the various types of brackets, and pick the pair // closest to the cursor that surrounds the cursor. Range r(0, 0, m_view->doc()->lines(), m_view->doc()->line(m_view->doc()->lastLine()).length(), InclusiveMotion); shrinkRangeAroundCursor(r, findSurroundingQuotes(QLatin1Char(','), inner)); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(']'), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(','), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('['), QLatin1Char(','), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char(','), inner, QLatin1Char('{'), QLatin1Char('}'))); return r; } void NormalViMode::updateYankHighlightAttrib() { if (!m_highlightYankAttribute) { m_highlightYankAttribute = new KTextEditor::Attribute; } const QColor &yankedColor = m_view->rendererConfig()->savedLineColor(); m_highlightYankAttribute->setBackground(yankedColor); KTextEditor::Attribute::Ptr mouseInAttribute(new KTextEditor::Attribute()); mouseInAttribute->setFontBold(true); m_highlightYankAttribute->setDynamicAttribute(KTextEditor::Attribute::ActivateMouseIn, mouseInAttribute); m_highlightYankAttribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)->setBackground(yankedColor); } void NormalViMode::highlightYank(const Range &range, const OperationMode mode) { clearYankHighlight(); // current MovingRange doesn't support block mode selection so split the // block range into per-line ranges if (mode == Block) { for (int i = range.startLine; i <= range.endLine; i++) { addHighlightYank(KTextEditor::Range(i, range.startColumn, i, range.endColumn)); } } else { addHighlightYank(KTextEditor::Range(range.startLine, range.startColumn, range.endLine, range.endColumn)); } } void NormalViMode::addHighlightYank(KTextEditor::Range yankRange) { KTextEditor::MovingRange *highlightedYank = m_view->doc()->newMovingRange(yankRange, Kate::TextRange::DoNotExpand); highlightedYank->setView(m_view); // show only in this view highlightedYank->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlightedYank->setZDepth(-10000.0); highlightedYank->setAttribute(m_highlightYankAttribute); highlightedYankForDocument().insert(highlightedYank); } void NormalViMode::clearYankHighlight() { QSet &pHighlightedYanks = highlightedYankForDocument(); qDeleteAll(pHighlightedYanks); pHighlightedYanks.clear(); } QSet &NormalViMode::highlightedYankForDocument() { // Work around the fact that both Normal and Visual mode will have their own m_highlightedYank - // make Normal's the canonical one. return m_viInputModeManager->getViNormalMode()->m_highlightedYanks; } bool NormalViMode::waitingForRegisterOrCharToSearch() { // r, q, @ are never preceded by operators. There will always be a keys size of 1 for them. // f, t, F, T can be preceded by a delete/replace/yank/indent operator. size = 2 in that case. // f, t, F, T can be preceded by 'g' case/formatting operators. size = 3 in that case. const int keysSize = m_keys.size(); if (keysSize < 1) { // Just being defensive there. return false; } if (keysSize > 1) { // Multi-letter operation. QChar cPrefix = m_keys[0]; if (keysSize == 2) { // delete/replace/yank/indent operator? if (cPrefix != QLatin1Char('c') && cPrefix != QLatin1Char('d') && cPrefix != QLatin1Char('y') && cPrefix != QLatin1Char('=') && cPrefix != QLatin1Char('>') && cPrefix != QLatin1Char('<')) { return false; } } else if (keysSize == 3) { // We need to look deeper. Is it a g motion? QChar cNextfix = m_keys[1]; if (cPrefix != QLatin1Char('g') || (cNextfix != QLatin1Char('U') && cNextfix != QLatin1Char('u') && cNextfix != QLatin1Char('~') && cNextfix != QLatin1Char('q') && cNextfix != QLatin1Char('w') && cNextfix != QLatin1Char('@'))) { return false; } } else { return false; } } QChar ch = m_keys[keysSize - 1]; return (ch == QLatin1Char('f') || ch == QLatin1Char('t') || ch == QLatin1Char('F') || ch == QLatin1Char('T') // c/d prefix unapplicable for the following cases. || (keysSize == 1 && (ch == QLatin1Char('r') || ch == QLatin1Char('q') || ch == QLatin1Char('@')))); } void NormalViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range) { if (m_viInputModeManager->view()->viewInputMode() != KTextEditor::View::ViInputMode) { return; } Q_UNUSED(document) const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); const bool continuesInsertion = range.start().line() == m_currentChangeEndMarker.line() && range.start().column() == m_currentChangeEndMarker.column(); const bool beginsWithNewline = doc()->text(range).at(0) == QLatin1Char('\n'); if (!continuesInsertion) { KTextEditor::Cursor newBeginMarkerPos = range.start(); if (beginsWithNewline && !isInsertReplaceMode) { // Presumably a linewise paste, in which case we ignore the leading '\n' newBeginMarkerPos = KTextEditor::Cursor(newBeginMarkerPos.line() + 1, 0); } m_viInputModeManager->marks()->setStartEditYanked(newBeginMarkerPos); } m_viInputModeManager->marks()->setLastChange(range.start()); KTextEditor::Cursor editEndMarker = range.end(); if (!isInsertReplaceMode) { editEndMarker.setColumn(editEndMarker.column() - 1); } m_viInputModeManager->marks()->setFinishEditYanked(editEndMarker); m_currentChangeEndMarker = range.end(); if (m_isUndo) { const bool addsMultipleLines = range.start().line() != range.end().line(); m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line(), 0)); if (addsMultipleLines) { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + 1, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + 1, 0)); } else { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line(), 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line(), 0)); } } } void NormalViMode::textRemoved(KTextEditor::Document *document, KTextEditor::Range range) { if (m_viInputModeManager->view()->viewInputMode() != KTextEditor::View::ViInputMode) { return; } Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); m_viInputModeManager->marks()->setLastChange(range.start()); if (!isInsertReplaceMode) { // Don't go resetting [ just because we did a Ctrl-h! m_viInputModeManager->marks()->setStartEditYanked(range.start()); } else { // Don't go disrupting our continued insertion just because we did a Ctrl-h! m_currentChangeEndMarker = range.start(); } m_viInputModeManager->marks()->setFinishEditYanked(range.start()); if (m_isUndo) { // Slavishly follow Vim's weird rules: if an undo removes several lines, then all markers should // be at the beginning of the line after the last line removed, else they should at the beginning // of the line above that. const int markerLineAdjustment = (range.start().line() != range.end().line()) ? 1 : 0; m_viInputModeManager->marks()->setStartEditYanked( KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setFinishEditYanked( KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + markerLineAdjustment, 0)); } } void NormalViMode::undoBeginning() { m_isUndo = true; } void NormalViMode::undoEnded() { m_isUndo = false; } bool NormalViMode::executeKateCommand(const QString &command) { KTextEditor::Command *p = KateCmd::self()->queryCommand(command); if (!p) { return false; } QString msg; return p->exec(m_view, command, msg); } #define ADDCMD(STR, FUNC, FLGS) Command(QStringLiteral(STR), &NormalViMode::FUNC, FLGS) #define ADDMOTION(STR, FUNC, FLGS) Motion(QStringLiteral(STR), &NormalViMode::FUNC, FLGS) const std::vector &NormalViMode::commands() { // init once, is expensive static const std::vector global{ ADDCMD("a", commandEnterInsertModeAppend, IS_CHANGE), ADDCMD("A", commandEnterInsertModeAppendEOL, IS_CHANGE), ADDCMD("i", commandEnterInsertMode, IS_CHANGE), ADDCMD("", commandEnterInsertMode, IS_CHANGE), ADDCMD("I", commandEnterInsertModeBeforeFirstNonBlankInLine, IS_CHANGE), ADDCMD("gi", commandEnterInsertModeLast, IS_CHANGE), ADDCMD("v", commandEnterVisualMode, 0), ADDCMD("V", commandEnterVisualLineMode, 0), ADDCMD("", commandEnterVisualBlockMode, 0), ADDCMD("gv", commandReselectVisual, SHOULD_NOT_RESET), ADDCMD("o", commandOpenNewLineUnder, IS_CHANGE), ADDCMD("O", commandOpenNewLineOver, IS_CHANGE), ADDCMD("J", commandJoinLines, IS_CHANGE), ADDCMD("c", commandChange, IS_CHANGE | NEEDS_MOTION), ADDCMD("C", commandChangeToEOL, IS_CHANGE), ADDCMD("cc", commandChangeLine, IS_CHANGE), ADDCMD("s", commandSubstituteChar, IS_CHANGE), ADDCMD("S", commandSubstituteLine, IS_CHANGE), ADDCMD("dd", commandDeleteLine, IS_CHANGE), ADDCMD("d", commandDelete, IS_CHANGE | NEEDS_MOTION), ADDCMD("D", commandDeleteToEOL, IS_CHANGE), ADDCMD("x", commandDeleteChar, IS_CHANGE), ADDCMD("", commandDeleteChar, IS_CHANGE), ADDCMD("X", commandDeleteCharBackward, IS_CHANGE), ADDCMD("gu", commandMakeLowercase, IS_CHANGE | NEEDS_MOTION), ADDCMD("guu", commandMakeLowercaseLine, IS_CHANGE), ADDCMD("gU", commandMakeUppercase, IS_CHANGE | NEEDS_MOTION), ADDCMD("gUU", commandMakeUppercaseLine, IS_CHANGE), ADDCMD("y", commandYank, NEEDS_MOTION), ADDCMD("yy", commandYankLine, 0), ADDCMD("Y", commandYankToEOL, 0), ADDCMD("p", commandPaste, IS_CHANGE), ADDCMD("P", commandPasteBefore, IS_CHANGE), ADDCMD("gp", commandgPaste, IS_CHANGE), ADDCMD("gP", commandgPasteBefore, IS_CHANGE), ADDCMD("]p", commandIndentedPaste, IS_CHANGE), ADDCMD("[p", commandIndentedPasteBefore, IS_CHANGE), ADDCMD("r.", commandReplaceCharacter, IS_CHANGE | REGEX_PATTERN), ADDCMD("R", commandEnterReplaceMode, IS_CHANGE), ADDCMD(":", commandSwitchToCmdLine, 0), ADDCMD("u", commandUndo, 0), ADDCMD("", commandRedo, 0), ADDCMD("U", commandRedo, 0), ADDCMD("m.", commandSetMark, REGEX_PATTERN), ADDCMD(">>", commandIndentLine, IS_CHANGE), ADDCMD("<<", commandUnindentLine, IS_CHANGE), ADDCMD(">", commandIndentLines, IS_CHANGE | NEEDS_MOTION), ADDCMD("<", commandUnindentLines, IS_CHANGE | NEEDS_MOTION), ADDCMD("", commandScrollPageDown, 0), ADDCMD("", commandScrollPageDown, 0), ADDCMD("", commandScrollPageUp, 0), ADDCMD("", commandScrollPageUp, 0), ADDCMD("", commandScrollHalfPageUp, 0), ADDCMD("", commandScrollHalfPageDown, 0), ADDCMD("z.", commandCenterViewOnNonBlank, 0), ADDCMD("zz", commandCenterViewOnCursor, 0), ADDCMD("z", commandTopViewOnNonBlank, 0), ADDCMD("zt", commandTopViewOnCursor, 0), ADDCMD("z-", commandBottomViewOnNonBlank, 0), ADDCMD("zb", commandBottomViewOnCursor, 0), ADDCMD("ga", commandPrintCharacterCode, SHOULD_NOT_RESET), ADDCMD(".", commandRepeatLastChange, 0), ADDCMD("==", commandAlignLine, IS_CHANGE), ADDCMD("=", commandAlignLines, IS_CHANGE | NEEDS_MOTION), ADDCMD("~", commandChangeCase, IS_CHANGE), ADDCMD("g~", commandChangeCaseRange, IS_CHANGE | NEEDS_MOTION), ADDCMD("g~~", commandChangeCaseLine, IS_CHANGE), ADDCMD("", commandAddToNumber, IS_CHANGE), ADDCMD("", commandSubtractFromNumber, IS_CHANGE), ADDCMD("", commandGoToPrevJump, CAN_LAND_INSIDE_FOLDING_RANGE), ADDCMD("", commandGoToNextJump, CAN_LAND_INSIDE_FOLDING_RANGE), ADDCMD("h", commandSwitchToLeftView, 0), ADDCMD("", commandSwitchToLeftView, 0), ADDCMD("", commandSwitchToLeftView, 0), ADDCMD("j", commandSwitchToDownView, 0), ADDCMD("", commandSwitchToDownView, 0), ADDCMD("", commandSwitchToDownView, 0), ADDCMD("k", commandSwitchToUpView, 0), ADDCMD("", commandSwitchToUpView, 0), ADDCMD("", commandSwitchToUpView, 0), ADDCMD("l", commandSwitchToRightView, 0), ADDCMD("", commandSwitchToRightView, 0), ADDCMD("", commandSwitchToRightView, 0), ADDCMD("w", commandSwitchToNextView, 0), ADDCMD("", commandSwitchToNextView, 0), ADDCMD("s", commandSplitHoriz, 0), ADDCMD("S", commandSplitHoriz, 0), ADDCMD("", commandSplitHoriz, 0), ADDCMD("v", commandSplitVert, 0), ADDCMD("", commandSplitVert, 0), ADDCMD("c", commandCloseView, 0), ADDCMD("gt", commandSwitchToNextTab, 0), ADDCMD("gT", commandSwitchToPrevTab, 0), ADDCMD("gqq", commandFormatLine, IS_CHANGE), ADDCMD("gq", commandFormatLines, IS_CHANGE | NEEDS_MOTION), ADDCMD("zo", commandExpandLocal, 0), ADDCMD("zc", commandCollapseLocal, 0), ADDCMD("za", commandToggleRegionVisibility, 0), ADDCMD("zr", commandExpandAll, 0), ADDCMD("zm", commandCollapseToplevelNodes, 0), ADDCMD("q.", commandStartRecordingMacro, REGEX_PATTERN), ADDCMD("@.", commandReplayMacro, REGEX_PATTERN), ADDCMD("ZZ", commandCloseWrite, 0), ADDCMD("ZQ", commandCloseNocheck, 0), }; return global; } const std::vector &NormalViMode::motions() { // init once, is expensive static const std::vector global{ // regular motions ADDMOTION("h", motionLeft, 0), ADDMOTION("", motionLeft, 0), ADDMOTION("", motionLeft, 0), ADDMOTION("j", motionDown, 0), ADDMOTION("", motionDown, 0), ADDMOTION("", motionDownToFirstNonBlank, 0), ADDMOTION("", motionDownToFirstNonBlank, 0), ADDMOTION("k", motionUp, 0), ADDMOTION("", motionUp, 0), ADDMOTION("-", motionUpToFirstNonBlank, 0), ADDMOTION("+", motionDownToFirstNonBlank, 0), ADDMOTION("l", motionRight, 0), ADDMOTION("", motionRight, 0), ADDMOTION(" ", motionRight, 0), ADDMOTION("$", motionToEOL, 0), ADDMOTION("g_", motionToLastNonBlank, 0), ADDMOTION("", motionToEOL, 0), ADDMOTION("0", motionToColumn0, 0), ADDMOTION("", motionToColumn0, 0), ADDMOTION("^", motionToFirstCharacterOfLine, 0), ADDMOTION("f.", motionFindChar, REGEX_PATTERN | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("F.", motionFindCharBackward, REGEX_PATTERN | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("t.", motionToChar, REGEX_PATTERN | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("T.", motionToCharBackward, REGEX_PATTERN | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION(";", motionRepeatlastTF, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION(",", motionRepeatlastTFBackward, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("n", motionFindNext, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("N", motionFindPrev, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("gg", motionToLineFirst, 0), ADDMOTION("G", motionToLineLast, 0), ADDMOTION("w", motionWordForward, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("W", motionWORDForward, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("", motionWordForward, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("", motionWordBackward, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("b", motionWordBackward, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("B", motionWORDBackward, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("e", motionToEndOfWord, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("E", motionToEndOfWORD, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("ge", motionToEndOfPrevWord, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("gE", motionToEndOfPrevWORD, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("|", motionToScreenColumn, 0), ADDMOTION("%", motionToMatchingItem, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("`[a-zA-Z^><\\.\\[\\]]", motionToMark, REGEX_PATTERN | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("'[a-zA-Z^><]", motionToMarkLine, REGEX_PATTERN | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("[[", motionToPreviousBraceBlockStart, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("]]", motionToNextBraceBlockStart, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("[]", motionToPreviousBraceBlockEnd, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("][", motionToNextBraceBlockEnd, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("*", motionToNextOccurrence, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("#", motionToPrevOccurrence, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("H", motionToFirstLineOfWindow, 0), ADDMOTION("M", motionToMiddleLineOfWindow, 0), ADDMOTION("L", motionToLastLineOfWindow, 0), ADDMOTION("gj", motionToNextVisualLine, 0), ADDMOTION("g", motionToNextVisualLine, 0), ADDMOTION("gk", motionToPrevVisualLine, 0), ADDMOTION("g", motionToPrevVisualLine, 0), ADDMOTION("(", motionToPreviousSentence, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION(")", motionToNextSentence, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("{", motionToBeforeParagraph, CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("}", motionToAfterParagraph, CAN_LAND_INSIDE_FOLDING_RANGE), // text objects ADDMOTION("iw", textObjectInnerWord, 0), ADDMOTION("aw", textObjectAWord, IS_NOT_LINEWISE), ADDMOTION("iW", textObjectInnerWORD, 0), ADDMOTION("aW", textObjectAWORD, IS_NOT_LINEWISE), ADDMOTION("is", textObjectInnerSentence, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("as", textObjectASentence, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("ip", textObjectInnerParagraph, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("ap", textObjectAParagraph, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i\"", textObjectInnerQuoteDouble, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a\"", textObjectAQuoteDouble, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i'", textObjectInnerQuoteSingle, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a'", textObjectAQuoteSingle, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i`", textObjectInnerBackQuote, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a`", textObjectABackQuote, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i[()b]", textObjectInnerParen, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a[()b]", textObjectAParen, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i[{}B]", textObjectInnerCurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a[{}B]", textObjectACurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i[><]", textObjectInnerInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a[><]", textObjectAInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i[\\[\\]]", textObjectInnerBracket, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a[\\[\\]]", textObjectABracket, REGEX_PATTERN | IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("i,", textObjectInnerComma, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("a,", textObjectAComma, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("/", motionToIncrementalSearchMatch, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), ADDMOTION("?", motionToIncrementalSearchMatch, IS_NOT_LINEWISE | CAN_LAND_INSIDE_FOLDING_RANGE), }; return global; }