1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 //
27 // ATTENTION:
28 //
29 // 1 Please do not add any direct dependencies to other Qt Creator code here.
30 // Instead emit signals and let the FakeVimPlugin channel the information to
31 // Qt Creator. The idea is to keep this file here in a "clean" state that
32 // allows easy reuse with any QTextEdit or QPlainTextEdit derived class.
33 //
34 // 2 There are a few auto tests located in ../../../tests/auto/fakevim.
35 // Commands that are covered there are marked as "// tested" below.
36 //
37 // 3 Some conventions:
38 //
39 // Use 1 based line numbers and 0 based column numbers. Even though
40 // the 1 based line are not nice it matches vim's and QTextEdit's 'line'
41 // concepts.
42 //
43 // Do not pass QTextCursor etc around unless really needed. Convert
44 // early to line/column.
45 //
46 // A QTextCursor is always between characters, whereas vi's cursor is always
47 // over a character. FakeVim interprets the QTextCursor to be over the character
48 // to the right of the QTextCursor's position().
49 //
50 // A current "region of interest"
51 // spans between anchor(), (i.e. the character below anchor()), and
52 // position(). The character below position() is not included
53 // if the last movement command was exclusive (MoveExclusive).
54 //
55
56 #include "fakevimhandler.h"
57
58 #include "fakevimactions.h"
59 #include "fakevimtr.h"
60
61 #include <utils/optional.h>
62
63 #include <QDebug>
64 #include <QFile>
65 #include <QObject>
66 #include <QPointer>
67 #include <QProcess>
68 #include <QRegularExpression>
69 #include <QTextStream>
70 #include <QTimer>
71 #include <QStack>
72
73 #include <QApplication>
74 #include <QClipboard>
75 #include <QInputMethodEvent>
76 #include <QKeyEvent>
77 #include <QLineEdit>
78 #include <QPlainTextEdit>
79 #include <QScrollBar>
80 #include <QTextBlock>
81 #include <QTextCursor>
82 #include <QTextDocumentFragment>
83 #include <QTextEdit>
84 #include <QMimeData>
85 #include <QSharedPointer>
86 #include <QDir>
87
88 #include <algorithm>
89 #include <climits>
90 #include <ctype.h>
91 #include <functional>
92
93 //#define DEBUG_KEY 1
94 #ifdef DEBUG_KEY
95 # define KEY_DEBUG(s) qDebug() << s
96 #else
97 # define KEY_DEBUG(s)
98 #endif
99
100 //#define DEBUG_UNDO 1
101 #ifdef DEBUG_UNDO
102 # define UNDO_DEBUG(s) qDebug() << "REV" << revision() << s
103 #else
104 # define UNDO_DEBUG(s)
105 #endif
106
107 #if QT_VERSION < QT_VERSION_CHECK(5,10,0)
108 using QStringView = QStringRef;
109 #endif
110
111 namespace FakeVim {
112 namespace Internal {
113
114 ///////////////////////////////////////////////////////////////////////
115 //
116 // FakeVimHandler
117 //
118 ///////////////////////////////////////////////////////////////////////
119
120 #define StartOfLine QTextCursor::StartOfLine
121 #define EndOfLine QTextCursor::EndOfLine
122 #define MoveAnchor QTextCursor::MoveAnchor
123 #define KeepAnchor QTextCursor::KeepAnchor
124 #define Up QTextCursor::Up
125 #define Down QTextCursor::Down
126 #define Right QTextCursor::Right
127 #define Left QTextCursor::Left
128 #define EndOfDocument QTextCursor::End
129 #define StartOfDocument QTextCursor::Start
130 #define NextBlock QTextCursor::NextBlock
131
132 #define ParagraphSeparator QChar::ParagraphSeparator
133
134 #define EDITOR(s) (m_textedit ? m_textedit->s : m_plaintextedit->s)
135
136
137 #ifdef Q_OS_DARWIN
138 #define ControlModifier Qt::MetaModifier
139 #else
140 #define ControlModifier Qt::ControlModifier
141 #endif
142
143 /* Clipboard MIME types used by Vim. */
144 static const QString vimMimeText = "_VIM_TEXT";
145 static const QString vimMimeTextEncoded = "_VIMENC_TEXT";
146
147 using namespace Qt;
148
149 /*! A \e Mode represents one of the basic modes of operation of FakeVim.
150 */
151
152 enum Mode
153 {
154 InsertMode,
155 ReplaceMode,
156 CommandMode,
157 ExMode
158 };
159
160 enum BlockInsertMode
161 {
162 NoneBlockInsertMode,
163 AppendBlockInsertMode,
164 AppendToEndOfLineBlockInsertMode,
165 InsertBlockInsertMode,
166 ChangeBlockInsertMode
167 };
168
169 /*! A \e SubMode is used for things that require one more data item
170 and are 'nested' behind a \l Mode.
171 */
172 enum SubMode
173 {
174 NoSubMode,
175 ChangeSubMode, // Used for c
176 DeleteSubMode, // Used for d
177 ExchangeSubMode, // Used for cx
178 DeleteSurroundingSubMode, // Used for ds
179 ChangeSurroundingSubMode, // Used for cs
180 AddSurroundingSubMode, // Used for ys
181 FilterSubMode, // Used for !
182 IndentSubMode, // Used for =
183 RegisterSubMode, // Used for "
184 ShiftLeftSubMode, // Used for <
185 ShiftRightSubMode, // Used for >
186 CommentSubMode, // Used for gc
187 ReplaceWithRegisterSubMode, // Used for gr
188 InvertCaseSubMode, // Used for g~
189 DownCaseSubMode, // Used for gu
190 UpCaseSubMode, // Used for gU
191 WindowSubMode, // Used for Ctrl-w
192 YankSubMode, // Used for y
193 ZSubMode, // Used for z
194 CapitalZSubMode, // Used for Z
195 ReplaceSubMode, // Used for r
196 MacroRecordSubMode, // Used for q
197 MacroExecuteSubMode, // Used for @
198 CtrlVSubMode, // Used for Ctrl-v in insert mode
199 CtrlRSubMode // Used for Ctrl-r in insert mode
200 };
201
202 /*! A \e SubSubMode is used for things that require one more data item
203 and are 'nested' behind a \l SubMode.
204 */
205 enum SubSubMode
206 {
207 NoSubSubMode,
208 FtSubSubMode, // Used for f, F, t, T.
209 MarkSubSubMode, // Used for m.
210 BackTickSubSubMode, // Used for `.
211 TickSubSubMode, // Used for '.
212 TextObjectSubSubMode, // Used for thing like iw, aW, as etc.
213 ZSubSubMode, // Used for zj, zk
214 OpenSquareSubSubMode, // Used for [{, {(, [z
215 CloseSquareSubSubMode, // Used for ]}, ]), ]z
216 SearchSubSubMode, // Used for /, ?
217 SurroundSubSubMode, // Used for cs, ds, ys
218 SurroundWithFunctionSubSubMode, // Used for ys{motion}f
219 CtrlVUnicodeSubSubMode // Used for Ctrl-v based unicode input
220 };
221
222 enum VisualMode
223 {
224 NoVisualMode,
225 VisualCharMode,
226 VisualLineMode,
227 VisualBlockMode
228 };
229
230 enum MoveType
231 {
232 MoveExclusive,
233 MoveInclusive,
234 MoveLineWise
235 };
236
237 /*!
238 \enum RangeMode
239
240 The \e RangeMode serves as a means to define how the "Range" between
241 the \l cursor and the \l anchor position is to be interpreted.
242
243 \value RangeCharMode Entered by pressing \key v. The range includes
244 all characters between cursor and anchor.
245 \value RangeLineMode Entered by pressing \key V. The range includes
246 all lines between the line of the cursor and
247 the line of the anchor.
248 \value RangeLineModeExclusive Like \l RangeLineMode, but keeps one
249 newline when deleting.
250 \value RangeBlockMode Entered by pressing \key Ctrl-v. The range includes
251 all characters with line and column coordinates
252 between line and columns coordinates of cursor and
253 anchor.
254 \value RangeBlockAndTailMode Like \l RangeBlockMode, but also includes
255 all characters in the affected lines up to the end
256 of these lines.
257 */
258
259 enum EventResult
260 {
261 EventHandled,
262 EventUnhandled,
263 EventCancelled, // Event is handled but a sub mode was cancelled.
264 EventPassedToCore
265 };
266
267 struct CursorPosition
268 {
269 CursorPosition() = default;
CursorPositionFakeVim::Internal::CursorPosition270 CursorPosition(int block, int column) : line(block), column(column) {}
CursorPositionFakeVim::Internal::CursorPosition271 explicit CursorPosition(const QTextCursor &tc)
272 : line(tc.block().blockNumber()), column(tc.positionInBlock()) {}
CursorPositionFakeVim::Internal::CursorPosition273 CursorPosition(const QTextDocument *document, int position)
274 {
275 QTextBlock block = document->findBlock(position);
276 line = block.blockNumber();
277 column = position - block.position();
278 }
isValidFakeVim::Internal::CursorPosition279 bool isValid() const { return line >= 0 && column >= 0; }
operator >FakeVim::Internal::CursorPosition280 bool operator>(const CursorPosition &other) const
281 { return line > other.line || column > other.column; }
operator ==FakeVim::Internal::CursorPosition282 bool operator==(const CursorPosition &other) const
283 { return line == other.line && column == other.column; }
operator !=FakeVim::Internal::CursorPosition284 bool operator!=(const CursorPosition &other) const { return !operator==(other); }
285
286 int line = -1; // Line in document (from 0, folded lines included).
287 int column = -1; // Position on line.
288 };
289
operator <<(QDebug ts,const CursorPosition & pos)290 QDebug operator<<(QDebug ts, const CursorPosition &pos)
291 {
292 return ts << "(line: " << pos.line << ", column: " << pos.column << ")";
293 }
294
295 // vi style configuration
296
297 class Mark
298 {
299 public:
Mark(const CursorPosition & pos=CursorPosition (),const QString & fileName=QString ())300 Mark(const CursorPosition &pos = CursorPosition(), const QString &fileName = QString())
301 : m_position(pos), m_fileName(fileName) {}
302
isValid() const303 bool isValid() const { return m_position.isValid(); }
304
isLocal(const QString & localFileName) const305 bool isLocal(const QString &localFileName) const
306 {
307 return m_fileName.isEmpty() || m_fileName == localFileName;
308 }
309
310 /* Return position of mark within given document.
311 * If saved line number is too big, mark position is at the end of document.
312 * If line number is in document but column is too big, mark position is at the end of line.
313 */
position(const QTextDocument * document) const314 CursorPosition position(const QTextDocument *document) const
315 {
316 QTextBlock block = document->findBlockByNumber(m_position.line);
317 CursorPosition pos;
318 if (block.isValid()) {
319 pos.line = m_position.line;
320 pos.column = qMax(0, qMin(m_position.column, block.length() - 2));
321 } else if (document->isEmpty()) {
322 pos.line = 0;
323 pos.column = 0;
324 } else {
325 pos.line = document->blockCount() - 1;
326 pos.column = qMax(0, document->lastBlock().length() - 2);
327 }
328 return pos;
329 }
330
fileName() const331 const QString &fileName() const { return m_fileName; }
332
setFileName(const QString & fileName)333 void setFileName(const QString &fileName) { m_fileName = fileName; }
334
335 private:
336 CursorPosition m_position;
337 QString m_fileName;
338 };
339 using Marks = QHash<QChar, Mark>;
340
341 struct State
342 {
343 State() = default;
StateFakeVim::Internal::State344 State(int revision, const CursorPosition &position, const Marks &marks,
345 VisualMode lastVisualMode, bool lastVisualModeInverted) : revision(revision),
346 position(position), marks(marks), lastVisualMode(lastVisualMode),
347 lastVisualModeInverted(lastVisualModeInverted) {}
348
isValidFakeVim::Internal::State349 bool isValid() const { return position.isValid(); }
350
351 int revision = -1;
352 CursorPosition position;
353 Marks marks;
354 VisualMode lastVisualMode = NoVisualMode;
355 bool lastVisualModeInverted = false;
356 };
357
358 struct Column
359 {
ColumnFakeVim::Internal::Column360 Column(int p, int l) : physical(p), logical(l) {}
361 int physical; // Number of characters in the data.
362 int logical; // Column on screen.
363 };
364
operator <<(QDebug ts,const Column & col)365 QDebug operator<<(QDebug ts, const Column &col)
366 {
367 return ts << "(p: " << col.physical << ", l: " << col.logical << ")";
368 }
369
370 struct Register
371 {
372 Register() = default;
RegisterFakeVim::Internal::Register373 Register(const QString &c) : contents(c) {}
RegisterFakeVim::Internal::Register374 Register(const QString &c, RangeMode m) : contents(c), rangemode(m) {}
375 QString contents;
376 RangeMode rangemode = RangeCharMode;
377 };
378
operator <<(QDebug ts,const Register & reg)379 QDebug operator<<(QDebug ts, const Register ®)
380 {
381 return ts << reg.contents;
382 }
383
384 struct SearchData
385 {
386 QString needle;
387 bool forward = true;
388 bool highlightMatches = true;
389 };
390
replaceTildeWithHome(QString str)391 static QString replaceTildeWithHome(QString str)
392 {
393 str.replace("~", QDir::homePath());
394 return str;
395 }
396
397 // If string begins with given prefix remove it with trailing spaces and return true.
eatString(const QString & prefix,QString * str)398 static bool eatString(const QString &prefix, QString *str)
399 {
400 if (!str->startsWith(prefix))
401 return false;
402 *str = str->mid(prefix.size()).trimmed();
403 return true;
404 }
405
vimPatternToQtPattern(const QString & needle)406 static QRegularExpression vimPatternToQtPattern(const QString &needle)
407 {
408 /* Transformations (Vim regexp -> QRegularExpression):
409 * \a -> [A-Za-z]
410 * \A -> [^A-Za-z]
411 * \h -> [A-Za-z_]
412 * \H -> [^A-Za-z_]
413 * \l -> [a-z]
414 * \L -> [^a-z]
415 * \o -> [0-7]
416 * \O -> [^0-7]
417 * \u -> [A-Z]
418 * \U -> [^A-Z]
419 * \x -> [0-9A-Fa-f]
420 * \X -> [^0-9A-Fa-f]
421 *
422 * \< -> \b
423 * \> -> \b
424 * [] -> \[\]
425 * \= -> ?
426 *
427 * (...) <-> \(...\)
428 * {...} <-> \{...\}
429 * | <-> \|
430 * ? <-> \?
431 * + <-> \+
432 * \{...} -> {...}
433 *
434 * \c - set ignorecase for rest
435 * \C - set noignorecase for rest
436 */
437
438 // FIXME: Option smartcase should be used only if search was typed by user.
439 const bool ignoreCaseOption = fakeVimSettings()->ignoreCase.value();
440 const bool smartCaseOption = fakeVimSettings()->smartCase.value();
441 const bool initialIgnoreCase = ignoreCaseOption
442 && !(smartCaseOption && needle.contains(QRegularExpression("[A-Z]")));
443
444 bool ignorecase = initialIgnoreCase;
445
446 QString pattern;
447 pattern.reserve(2 * needle.size());
448
449 bool escape = false;
450 bool brace = false;
451 bool embraced = false;
452 bool range = false;
453 bool curly = false;
454 for (const QChar &c : needle) {
455 if (brace) {
456 brace = false;
457 if (c == ']') {
458 pattern.append("\\[\\]");
459 continue;
460 }
461 pattern.append('[');
462 escape = true;
463 embraced = true;
464 }
465 if (embraced) {
466 if (range) {
467 QChar c2 = pattern[pattern.size() - 2];
468 pattern.remove(pattern.size() - 2, 2);
469 pattern.append(c2.toUpper() + '-' + c.toUpper());
470 pattern.append(c2.toLower() + '-' + c.toLower());
471 range = false;
472 } else if (escape) {
473 escape = false;
474 pattern.append(c);
475 } else if (c == '\\') {
476 escape = true;
477 } else if (c == ']') {
478 pattern.append(']');
479 embraced = false;
480 } else if (c == '-') {
481 range = ignorecase && pattern[pattern.size() - 1].isLetter();
482 pattern.append('-');
483 } else if (c.isLetter() && ignorecase) {
484 pattern.append(c.toLower()).append(c.toUpper());
485 } else {
486 pattern.append(c);
487 }
488 } else if (QString("(){}+|?").indexOf(c) != -1) {
489 if (c == '{') {
490 curly = escape;
491 } else if (c == '}' && curly) {
492 curly = false;
493 escape = true;
494 }
495
496 if (escape)
497 escape = false;
498 else
499 pattern.append('\\');
500 pattern.append(c);
501 } else if (escape) {
502 // escape expression
503 escape = false;
504 if (c == '<' || c == '>')
505 pattern.append("\\b");
506 else if (c == 'a')
507 pattern.append("[a-zA-Z]");
508 else if (c == 'A')
509 pattern.append("[^a-zA-Z]");
510 else if (c == 'h')
511 pattern.append("[A-Za-z_]");
512 else if (c == 'H')
513 pattern.append("[^A-Za-z_]");
514 else if (c == 'c' || c == 'C')
515 ignorecase = (c == 'c');
516 else if (c == 'l')
517 pattern.append("[a-z]");
518 else if (c == 'L')
519 pattern.append("[^a-z]");
520 else if (c == 'o')
521 pattern.append("[0-7]");
522 else if (c == 'O')
523 pattern.append("[^0-7]");
524 else if (c == 'u')
525 pattern.append("[A-Z]");
526 else if (c == 'U')
527 pattern.append("[^A-Z]");
528 else if (c == 'x')
529 pattern.append("[0-9A-Fa-f]");
530 else if (c == 'X')
531 pattern.append("[^0-9A-Fa-f]");
532 else if (c == '=')
533 pattern.append("?");
534 else {
535 pattern.append('\\');
536 pattern.append(c);
537 }
538 } else {
539 // unescaped expression
540 if (c == '\\')
541 escape = true;
542 else if (c == '[')
543 brace = true;
544 else if (c.isLetter() && ignorecase)
545 pattern.append('[').append(c.toLower()).append(c.toUpper()).append(']');
546 else
547 pattern.append(c);
548 }
549 }
550 if (escape)
551 pattern.append('\\');
552 else if (brace)
553 pattern.append('[');
554
555 return QRegularExpression(pattern, initialIgnoreCase ? QRegularExpression::CaseInsensitiveOption
556 : QRegularExpression::NoPatternOption);
557 }
558
afterEndOfLine(const QTextDocument * doc,int position)559 static bool afterEndOfLine(const QTextDocument *doc, int position)
560 {
561 return doc->characterAt(position) == ParagraphSeparator
562 && doc->findBlock(position).length() > 1;
563 }
564
searchForward(QTextCursor * tc,const QRegularExpression & needleExp,int * repeat)565 static void searchForward(QTextCursor *tc, const QRegularExpression &needleExp, int *repeat)
566 {
567 const QTextDocument *doc = tc->document();
568 const int startPos = tc->position();
569
570 QTextDocument::FindFlags flags = {};
571 if (!(needleExp.patternOptions() & QRegularExpression::CaseInsensitiveOption))
572 flags |= QTextDocument::FindCaseSensitively;
573
574 // Search from beginning of line so that matched text is the same.
575 tc->movePosition(StartOfLine);
576
577 // forward to current position
578 *tc = doc->find(needleExp, *tc, flags);
579 while (!tc->isNull() && tc->anchor() < startPos) {
580 if (!tc->hasSelection())
581 tc->movePosition(Right);
582 if (tc->atBlockEnd())
583 tc->movePosition(NextBlock);
584 *tc = doc->find(needleExp, *tc, flags);
585 }
586
587 if (tc->isNull())
588 return;
589
590 --*repeat;
591
592 while (*repeat > 0) {
593 if (!tc->hasSelection())
594 tc->movePosition(Right);
595 if (tc->atBlockEnd())
596 tc->movePosition(NextBlock);
597 *tc = doc->find(needleExp, *tc, flags);
598 if (tc->isNull())
599 return;
600 --*repeat;
601 }
602
603 if (!tc->isNull() && afterEndOfLine(doc, tc->anchor()))
604 tc->movePosition(Left);
605 }
606
searchBackward(QTextCursor * tc,const QRegularExpression & needleExp,int * repeat)607 static void searchBackward(QTextCursor *tc, const QRegularExpression &needleExp, int *repeat)
608 {
609 // Search from beginning of line so that matched text is the same.
610 QTextBlock block = tc->block();
611 QString line = block.text();
612
613 QRegularExpressionMatch match;
614 int i = line.indexOf(needleExp, 0, &match);
615 while (i != -1 && i < tc->positionInBlock()) {
616 --*repeat;
617 const int offset = i + qMax(1, match.capturedLength());
618 i = line.indexOf(needleExp, offset, &match);
619 if (i == line.size())
620 i = -1;
621 }
622
623 if (i == tc->positionInBlock())
624 --*repeat;
625
626 while (*repeat > 0) {
627 block = block.previous();
628 if (!block.isValid())
629 break;
630 line = block.text();
631 i = line.indexOf(needleExp, 0, &match);
632 while (i != -1) {
633 --*repeat;
634 const int offset = i + qMax(1, match.capturedLength());
635 i = line.indexOf(needleExp, offset, &match);
636 if (i == line.size())
637 i = -1;
638 }
639 }
640
641 if (!block.isValid()) {
642 *tc = QTextCursor();
643 return;
644 }
645
646 i = line.indexOf(needleExp, 0, &match);
647 while (*repeat < 0) {
648 const int offset = i + qMax(1, match.capturedLength());
649 i = line.indexOf(needleExp, offset, &match);
650 ++*repeat;
651 }
652 tc->setPosition(block.position() + i);
653 tc->setPosition(tc->position() + match.capturedLength(), KeepAnchor);
654 }
655
656 // Commands [[, []
bracketSearchBackward(QTextCursor * tc,const QString & needleExp,int repeat)657 static void bracketSearchBackward(QTextCursor *tc, const QString &needleExp, int repeat)
658 {
659 const QRegularExpression re(needleExp);
660 QTextCursor tc2 = *tc;
661 tc2.setPosition(tc2.position() - 1);
662 searchBackward(&tc2, re, &repeat);
663 if (repeat <= 1)
664 tc->setPosition(tc2.isNull() ? 0 : tc2.position(), KeepAnchor);
665 }
666
667 // Commands ][, ]]
668 // When ]] is used after an operator, then also stops below a '}' in the first column.
bracketSearchForward(QTextCursor * tc,const QString & needleExp,int repeat,bool searchWithCommand)669 static void bracketSearchForward(QTextCursor *tc, const QString &needleExp, int repeat,
670 bool searchWithCommand)
671 {
672 QRegularExpression re(searchWithCommand ? QString("^\\}|^\\{") : needleExp);
673 QTextCursor tc2 = *tc;
674 tc2.setPosition(tc2.position() + 1);
675 searchForward(&tc2, re, &repeat);
676 if (repeat <= 1) {
677 if (tc2.isNull()) {
678 tc->setPosition(tc->document()->characterCount() - 1, KeepAnchor);
679 } else {
680 tc->setPosition(tc2.position() - 1, KeepAnchor);
681 if (searchWithCommand && tc->document()->characterAt(tc->position()).unicode() == '}') {
682 QTextBlock block = tc->block().next();
683 if (block.isValid())
684 tc->setPosition(block.position(), KeepAnchor);
685 }
686 }
687 }
688 }
689
backslashed(char t)690 static char backslashed(char t)
691 {
692 switch (t) {
693 case 'e': return 27;
694 case 't': return '\t';
695 case 'r': return '\r';
696 case 'n': return '\n';
697 case 'b': return 8;
698 }
699 return t;
700 }
701
702 enum class Modifier {NONE,UPPERCASE,LOWERCASE};
703
applyReplacementLetterCases(QString repl,Modifier & toggledModifier,Modifier & nextCharacterModifier)704 static QString applyReplacementLetterCases(QString repl,
705 Modifier &toggledModifier,
706 Modifier &nextCharacterModifier)
707 {
708 if (toggledModifier == Modifier::UPPERCASE)
709 repl = repl.toUpper();
710 else if (toggledModifier == Modifier::LOWERCASE)
711 repl = repl.toLower();
712
713 if (nextCharacterModifier == Modifier::UPPERCASE) {
714 repl.replace(0, 1, repl.at(0).toUpper());
715 nextCharacterModifier = Modifier::NONE;
716 } else if (nextCharacterModifier == Modifier::LOWERCASE) {
717 repl.replace(0, 1, repl.at(0).toLower());
718 nextCharacterModifier = Modifier::NONE;
719 }
720 return repl;
721 }
722
applyReplacementLetterCases(QChar repl,Modifier & toggledModifier,Modifier & nextCharacterModifier)723 static QChar applyReplacementLetterCases(QChar repl,
724 Modifier &toggledModifier,
725 Modifier &nextCharacterModifier)
726 {
727 if (nextCharacterModifier == Modifier::UPPERCASE){
728 nextCharacterModifier = Modifier::NONE;
729 return repl.toUpper();
730 }
731 else if (nextCharacterModifier == Modifier::LOWERCASE) {
732 nextCharacterModifier = Modifier::NONE;
733 return repl.toLower();
734 }
735 else if (toggledModifier == Modifier::UPPERCASE)
736 return repl.toUpper();
737 else if (toggledModifier == Modifier::LOWERCASE)
738 return repl.toLower();
739 else
740 return repl;
741 }
742
substituteText(QString * text,const QRegularExpression & pattern,const QString & replacement,bool global)743 static bool substituteText(QString *text,
744 const QRegularExpression &pattern,
745 const QString &replacement,
746 bool global)
747 {
748 bool substituted = false;
749 int pos = 0;
750 int right = -1;
751 while (true) {
752 const QRegularExpressionMatch match = pattern.match(*text, pos);
753 if (!match.hasMatch())
754 break;
755
756 pos = match.capturedStart();
757
758 // ensure that substitution is advancing towards end of line
759 if (right == text->size() - pos) {
760 ++pos;
761 if (pos == text->size())
762 break;
763 continue;
764 }
765
766 right = text->size() - pos;
767
768 substituted = true;
769 QString matched = text->mid(pos, match.captured(0).size());
770 QString repl;
771 bool escape = false;
772 Modifier toggledModifier = Modifier::NONE;
773 Modifier nextCharacterModifier = Modifier::NONE;
774 // insert captured texts
775 for (int i = 0; i < replacement.size(); ++i) {
776 const QChar &c = replacement[i];
777 if (escape) {
778 escape = false;
779 if (c.isDigit()) {
780 if (c.digitValue() <= match.lastCapturedIndex()) {
781 repl += applyReplacementLetterCases(match.captured(c.digitValue()),
782 toggledModifier,
783 nextCharacterModifier);
784
785 }
786 } else if (c == 'u') {
787 nextCharacterModifier = Modifier::UPPERCASE;
788 } else if (c == 'l') {
789 nextCharacterModifier = Modifier::LOWERCASE;
790 } else if (c == 'U') {
791 toggledModifier = Modifier::UPPERCASE;
792 } else if (c == 'L') {
793 toggledModifier = Modifier::LOWERCASE;
794 } else if (c == 'e' || c == 'E') {
795 nextCharacterModifier = Modifier::NONE;
796 toggledModifier = Modifier::NONE;
797 } else {
798 repl += backslashed(c.unicode());
799 }
800 } else {
801 if (c == '\\')
802 escape = true;
803 else if (c == '&')
804 repl += applyReplacementLetterCases(match.captured(0),
805 toggledModifier,
806 nextCharacterModifier);
807 else
808 repl += applyReplacementLetterCases(c, toggledModifier, nextCharacterModifier);
809 }
810 }
811 text->replace(pos, matched.size(), repl);
812 pos += (repl.isEmpty() && matched.isEmpty()) ? 1 : repl.size();
813
814 if (pos >= text->size() || !global)
815 break;
816 }
817
818 return substituted;
819 }
820
findUnescaped(QChar c,const QString & line,int from)821 static int findUnescaped(QChar c, const QString &line, int from)
822 {
823 for (int i = from; i < line.size(); ++i) {
824 if (line.at(i) == c && (i == 0 || line.at(i - 1) != '\\'))
825 return i;
826 }
827 return -1;
828 }
829
setClipboardData(const QString & content,RangeMode mode,QClipboard::Mode clipboardMode)830 static void setClipboardData(const QString &content, RangeMode mode,
831 QClipboard::Mode clipboardMode)
832 {
833 QClipboard *clipboard = QApplication::clipboard();
834 char vimRangeMode = mode;
835
836 QByteArray bytes1;
837 bytes1.append(vimRangeMode);
838 bytes1.append(content.toUtf8());
839
840 QByteArray bytes2;
841 bytes2.append(vimRangeMode);
842 bytes2.append("utf-8");
843 bytes2.append('\0');
844 bytes2.append(content.toUtf8());
845
846 auto data = new QMimeData;
847 data->setText(content);
848 data->setData(vimMimeText, bytes1);
849 data->setData(vimMimeTextEncoded, bytes2);
850 clipboard->setMimeData(data, clipboardMode);
851 }
852
toLocalEncoding(const QString & text)853 static QByteArray toLocalEncoding(const QString &text)
854 {
855 #if defined(Q_OS_WIN)
856 return QString(text).replace("\n", "\r\n").toLocal8Bit();
857 #else
858 return text.toLocal8Bit();
859 #endif
860 }
861
fromLocalEncoding(const QByteArray & data)862 static QString fromLocalEncoding(const QByteArray &data)
863 {
864 #if defined(Q_OS_WIN)
865 return QString::fromLocal8Bit(data).replace("\n", "\r\n");
866 #else
867 return QString::fromLocal8Bit(data);
868 #endif
869 }
870
getProcessOutput(const QString & command,const QString & input)871 static QString getProcessOutput(const QString &command, const QString &input)
872 {
873 QProcess proc;
874 #if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
875 QStringList arguments = QProcess::splitCommand(command);
876 QString executable = arguments.takeFirst();
877 proc.start(executable, arguments);
878 #else
879 proc.start(command);
880 #endif
881 proc.waitForStarted();
882 proc.write(toLocalEncoding(input));
883 proc.closeWriteChannel();
884
885 // FIXME: Process should be interruptable by user.
886 // Solution is to create a QObject for each process and emit finished state.
887 proc.waitForFinished();
888
889 return fromLocalEncoding(proc.readAllStandardOutput());
890 }
891
vimKeyNames()892 static const QMap<QString, int> &vimKeyNames()
893 {
894 static const QMap<QString, int> k = {
895 // FIXME: Should be value of mapleader.
896 {"LEADER", Key_Backslash},
897
898 {"SPACE", Key_Space},
899 {"TAB", Key_Tab},
900 {"NL", Key_Return},
901 {"NEWLINE", Key_Return},
902 {"LINEFEED", Key_Return},
903 {"LF", Key_Return},
904 {"CR", Key_Return},
905 {"RETURN", Key_Return},
906 {"ENTER", Key_Return},
907 {"BS", Key_Backspace},
908 {"BACKSPACE", Key_Backspace},
909 {"ESC", Key_Escape},
910 {"BAR", Key_Bar},
911 {"BSLASH", Key_Backslash},
912 {"DEL", Key_Delete},
913 {"DELETE", Key_Delete},
914 {"KDEL", Key_Delete},
915 {"UP", Key_Up},
916 {"DOWN", Key_Down},
917 {"LEFT", Key_Left},
918 {"RIGHT", Key_Right},
919
920 {"LT", Key_Less},
921 {"GT", Key_Greater},
922
923 {"F1", Key_F1},
924 {"F2", Key_F2},
925 {"F3", Key_F3},
926 {"F4", Key_F4},
927 {"F5", Key_F5},
928 {"F6", Key_F6},
929 {"F7", Key_F7},
930 {"F8", Key_F8},
931 {"F9", Key_F9},
932 {"F10", Key_F10},
933
934 {"F11", Key_F11},
935 {"F12", Key_F12},
936 {"F13", Key_F13},
937 {"F14", Key_F14},
938 {"F15", Key_F15},
939 {"F16", Key_F16},
940 {"F17", Key_F17},
941 {"F18", Key_F18},
942 {"F19", Key_F19},
943 {"F20", Key_F20},
944
945 {"F21", Key_F21},
946 {"F22", Key_F22},
947 {"F23", Key_F23},
948 {"F24", Key_F24},
949 {"F25", Key_F25},
950 {"F26", Key_F26},
951 {"F27", Key_F27},
952 {"F28", Key_F28},
953 {"F29", Key_F29},
954 {"F30", Key_F30},
955
956 {"F31", Key_F31},
957 {"F32", Key_F32},
958 {"F33", Key_F33},
959 {"F34", Key_F34},
960 {"F35", Key_F35},
961
962 {"INSERT", Key_Insert},
963 {"INS", Key_Insert},
964 {"KINSERT", Key_Insert},
965 {"HOME", Key_Home},
966 {"END", Key_End},
967 {"PAGEUP", Key_PageUp},
968 {"PAGEDOWN", Key_PageDown},
969
970 {"KPLUS", Key_Plus},
971 {"KMINUS", Key_Minus},
972 {"KDIVIDE", Key_Slash},
973 {"KMULTIPLY", Key_Asterisk},
974 {"KENTER", Key_Enter},
975 {"KPOINT", Key_Period},
976
977 {"CAPS", Key_CapsLock},
978 {"NUM", Key_NumLock},
979 {"SCROLL", Key_ScrollLock},
980 {"ALTGR", Key_AltGr}
981 };
982
983 return k;
984 }
985
isOnlyControlModifier(const Qt::KeyboardModifiers & mods)986 static bool isOnlyControlModifier(const Qt::KeyboardModifiers &mods)
987 {
988 return (mods ^ ControlModifier) == Qt::NoModifier;
989 }
990
isAcceptableModifier(const Qt::KeyboardModifiers & mods)991 static bool isAcceptableModifier(const Qt::KeyboardModifiers &mods)
992 {
993 if (mods & ControlModifier) {
994 // Generally, CTRL is not fine, except in combination with ALT.
995 // See QTCREATORBUG-24673
996 return mods & AltModifier;
997 }
998 return true;
999 }
1000
1001
Range(int b,int e,RangeMode m)1002 Range::Range(int b, int e, RangeMode m)
1003 : beginPos(qMin(b, e)), endPos(qMax(b, e)), rangemode(m)
1004 {}
1005
toString() const1006 QString Range::toString() const
1007 {
1008 return QString("%1-%2 (mode: %3)").arg(beginPos).arg(endPos).arg(rangemode);
1009 }
1010
isValid() const1011 bool Range::isValid() const
1012 {
1013 return beginPos >= 0 && endPos >= 0;
1014 }
1015
operator <<(QDebug ts,const Range & range)1016 QDebug operator<<(QDebug ts, const Range &range)
1017 {
1018 return ts << '[' << range.beginPos << ',' << range.endPos << ']';
1019 }
1020
1021
ExCommand(const QString & c,const QString & a,const Range & r)1022 ExCommand::ExCommand(const QString &c, const QString &a, const Range &r)
1023 : cmd(c), args(a), range(r)
1024 {}
1025
matches(const QString & min,const QString & full) const1026 bool ExCommand::matches(const QString &min, const QString &full) const
1027 {
1028 return cmd.startsWith(min) && full.startsWith(cmd);
1029 }
1030
operator <<(QDebug ts,const ExCommand & cmd)1031 QDebug operator<<(QDebug ts, const ExCommand &cmd)
1032 {
1033 return ts << cmd.cmd << ' ' << cmd.args << ' ' << cmd.range;
1034 }
1035
operator <<(QDebug ts,const QList<QTextEdit::ExtraSelection> & sels)1036 QDebug operator<<(QDebug ts, const QList<QTextEdit::ExtraSelection> &sels)
1037 {
1038 foreach (const QTextEdit::ExtraSelection &sel, sels)
1039 ts << "SEL: " << sel.cursor.anchor() << sel.cursor.position();
1040 return ts;
1041 }
1042
quoteUnprintable(const QString & ba)1043 QString quoteUnprintable(const QString &ba)
1044 {
1045 QString res;
1046 for (int i = 0, n = ba.size(); i != n; ++i) {
1047 const QChar c = ba.at(i);
1048 const int cc = c.unicode();
1049 if (c.isPrint())
1050 res += c;
1051 else if (cc == '\n')
1052 res += "<CR>";
1053 else
1054 res += QString("\\x%1").arg(c.unicode(), 2, 16, QLatin1Char('0'));
1055 }
1056 return res;
1057 }
1058
startsWithWhitespace(const QString & str,int col)1059 static bool startsWithWhitespace(const QString &str, int col)
1060 {
1061 if (col > str.size()) {
1062 qWarning("Wrong column");
1063 return false;
1064 }
1065 for (int i = 0; i < col; ++i) {
1066 uint u = str.at(i).unicode();
1067 if (u != ' ' && u != '\t')
1068 return false;
1069 }
1070 return true;
1071 }
1072
msgMarkNotSet(const QString & text)1073 inline QString msgMarkNotSet(const QString &text)
1074 {
1075 return Tr::tr("Mark \"%1\" not set.").arg(text);
1076 }
1077
initSingleShotTimer(QTimer * timer,int interval,FakeVimHandler::Private * receiver,void (FakeVimHandler::Private::* slot)())1078 static void initSingleShotTimer(QTimer *timer, int interval, FakeVimHandler::Private *receiver,
1079 void (FakeVimHandler::Private::*slot)())
1080 {
1081 timer->setSingleShot(true);
1082 timer->setInterval(interval);
1083 QObject::connect(timer, &QTimer::timeout, receiver, slot);
1084 }
1085
1086 class Input
1087 {
1088 public:
1089 // Remove some extra "information" on Mac.
cleanModifier(Qt::KeyboardModifiers m)1090 static Qt::KeyboardModifiers cleanModifier(Qt::KeyboardModifiers m)
1091 {
1092 return m & ~Qt::KeypadModifier;
1093 }
1094
1095 Input() = default;
Input(QChar x)1096 explicit Input(QChar x)
1097 : m_key(x.unicode()), m_xkey(x.unicode()), m_text(x)
1098 {
1099 if (x.isUpper())
1100 m_modifiers = Qt::ShiftModifier;
1101 else if (x.isLower())
1102 m_key = x.toUpper().unicode();
1103 }
1104
Input(int k,Qt::KeyboardModifiers m,const QString & t=QString ())1105 Input(int k, Qt::KeyboardModifiers m, const QString &t = QString())
1106 : m_key(k), m_modifiers(cleanModifier(m)), m_text(t)
1107 {
1108 if (m_text.size() == 1) {
1109 QChar x = m_text.at(0);
1110
1111 // On Mac, QKeyEvent::text() returns non-empty strings for
1112 // cursor keys. This breaks some of the logic later on
1113 // relying on text() being empty for "special" keys.
1114 // FIXME: Check the real conditions.
1115 if (x.unicode() < ' ' && x.unicode() != 27)
1116 m_text.clear();
1117 else if (x.isLetter())
1118 m_key = x.toUpper().unicode();
1119 }
1120
1121 // Set text only if input is ascii key without control modifier.
1122 if (m_text.isEmpty() && k >= 0 && k <= 0x7f && (m & ControlModifier) == 0) {
1123 QChar c = QChar(k);
1124 if (c.isLetter())
1125 m_text = isShift() ? c.toUpper() : c;
1126 else if (!isShift())
1127 m_text = c;
1128 }
1129
1130 // Normalize <S-TAB>.
1131 if (m_key == Qt::Key_Backtab) {
1132 m_key = Qt::Key_Tab;
1133 m_modifiers |= Qt::ShiftModifier;
1134 }
1135
1136 // m_xkey is only a cache.
1137 m_xkey = (m_text.size() == 1 ? m_text.at(0).unicode() : m_key);
1138 }
1139
isValid() const1140 bool isValid() const
1141 {
1142 return m_key != 0 || !m_text.isNull();
1143 }
1144
isDigit() const1145 bool isDigit() const
1146 {
1147 return m_xkey >= '0' && m_xkey <= '9';
1148 }
1149
isKey(int c) const1150 bool isKey(int c) const
1151 {
1152 return !m_modifiers && m_key == c;
1153 }
1154
isBackspace() const1155 bool isBackspace() const
1156 {
1157 return m_key == Key_Backspace || isControl('h');
1158 }
1159
isReturn() const1160 bool isReturn() const
1161 {
1162 return m_key == '\n' || m_key == Key_Return || m_key == Key_Enter;
1163 }
1164
isEscape() const1165 bool isEscape() const
1166 {
1167 return isKey(Key_Escape) || isShift(Key_Escape) || isKey(27) || isShift(27) || isControl('c')
1168 || isControl(Key_BracketLeft);
1169 }
1170
is(int c) const1171 bool is(int c) const
1172 {
1173 return m_xkey == c && isAcceptableModifier(m_modifiers);
1174 }
1175
isControl() const1176 bool isControl() const
1177 {
1178 return isOnlyControlModifier(m_modifiers);
1179 }
1180
isControl(int c) const1181 bool isControl(int c) const
1182 {
1183 return isControl()
1184 && (m_xkey == c || m_xkey + 32 == c || m_xkey + 64 == c || m_xkey + 96 == c);
1185 }
1186
isShift() const1187 bool isShift() const
1188 {
1189 return m_modifiers & Qt::ShiftModifier;
1190 }
1191
isShift(int c) const1192 bool isShift(int c) const
1193 {
1194 return isShift() && m_xkey == c;
1195 }
1196
operator <(const Input & a) const1197 bool operator<(const Input &a) const
1198 {
1199 if (m_key != a.m_key)
1200 return m_key < a.m_key;
1201 // Text for some mapped key cannot be determined (e.g. <C-J>) so if text is not set for
1202 // one of compared keys ignore it.
1203 if (!m_text.isEmpty() && !a.m_text.isEmpty() && m_text != " ")
1204 return m_text < a.m_text;
1205 return m_modifiers < a.m_modifiers;
1206 }
1207
operator ==(const Input & a) const1208 bool operator==(const Input &a) const
1209 {
1210 return !(*this < a || a < *this);
1211 }
1212
operator !=(const Input & a) const1213 bool operator!=(const Input &a) const { return !operator==(a); }
1214
text() const1215 QString text() const { return m_text; }
1216
asChar() const1217 QChar asChar() const
1218 {
1219 return (m_text.size() == 1 ? m_text.at(0) : QChar());
1220 }
1221
toInt(bool * ok,int base) const1222 int toInt(bool *ok, int base) const
1223 {
1224 const int uc = asChar().unicode();
1225 int res;
1226 if ('0' <= uc && uc <= '9')
1227 res = uc -'0';
1228 else if ('a' <= uc && uc <= 'z')
1229 res = 10 + uc - 'a';
1230 else if ('A' <= uc && uc <= 'Z')
1231 res = 10 + uc - 'A';
1232 else
1233 res = base;
1234 *ok = res < base;
1235 return *ok ? res : 0;
1236 }
1237
key() const1238 int key() const { return m_key; }
1239
modifiers() const1240 Qt::KeyboardModifiers modifiers() const { return m_modifiers; }
1241
1242 // Return raw character for macro recording or dot command.
raw() const1243 QChar raw() const
1244 {
1245 if (m_key == Key_Tab)
1246 return '\t';
1247 if (m_key == Key_Return)
1248 return '\n';
1249 if (m_key == Key_Escape)
1250 return QChar(27);
1251 return QChar(m_xkey);
1252 }
1253
toString() const1254 QString toString() const
1255 {
1256 if (!m_text.isEmpty())
1257 return QString(m_text).replace("<", "<LT>");
1258
1259 QString key = vimKeyNames().key(m_key);
1260 bool namedKey = !key.isEmpty();
1261
1262 if (!namedKey) {
1263 if (m_xkey == '<')
1264 key = "<LT>";
1265 else if (m_xkey == '>')
1266 key = "<GT>";
1267 else
1268 key = QChar(m_xkey);
1269 }
1270
1271 bool shift = isShift();
1272 bool ctrl = isControl();
1273 if (shift)
1274 key.prepend("S-");
1275 if (ctrl)
1276 key.prepend("C-");
1277
1278 if (namedKey || shift || ctrl) {
1279 key.prepend('<');
1280 key.append('>');
1281 }
1282
1283 return key;
1284 }
1285
dump(QDebug ts) const1286 QDebug dump(QDebug ts) const
1287 {
1288 return ts << m_key << '-' << m_modifiers << '-'
1289 << quoteUnprintable(m_text);
1290 }
1291
1292 private:
1293 int m_key = 0;
1294 int m_xkey = 0;
1295 Qt::KeyboardModifiers m_modifiers = NoModifier;
1296 QString m_text;
1297 };
1298
1299 // mapping to <Nop> (do nothing)
1300 static const Input Nop(-1, Qt::KeyboardModifiers(-1), QString());
1301
letterCaseModeFromInput(const Input & input)1302 static SubMode letterCaseModeFromInput(const Input &input)
1303 {
1304 if (input.is('~'))
1305 return InvertCaseSubMode;
1306 if (input.is('u'))
1307 return DownCaseSubMode;
1308 if (input.is('U'))
1309 return UpCaseSubMode;
1310
1311 return NoSubMode;
1312 }
1313
indentModeFromInput(const Input & input)1314 static SubMode indentModeFromInput(const Input &input)
1315 {
1316 if (input.is('<'))
1317 return ShiftLeftSubMode;
1318 if (input.is('>'))
1319 return ShiftRightSubMode;
1320 if (input.is('='))
1321 return IndentSubMode;
1322
1323 return NoSubMode;
1324 }
1325
changeDeleteYankModeFromInput(const Input & input)1326 static SubMode changeDeleteYankModeFromInput(const Input &input)
1327 {
1328 if (input.is('c'))
1329 return ChangeSubMode;
1330 if (input.is('d'))
1331 return DeleteSubMode;
1332 if (input.is('y'))
1333 return YankSubMode;
1334
1335 return NoSubMode;
1336 }
1337
dotCommandFromSubMode(SubMode submode)1338 QString dotCommandFromSubMode(SubMode submode)
1339 {
1340 if (submode == ChangeSubMode)
1341 return QLatin1String("c");
1342 if (submode == DeleteSubMode)
1343 return QLatin1String("d");
1344 if (submode == CommentSubMode)
1345 return QLatin1String("gc");
1346 if (submode == DeleteSurroundingSubMode)
1347 return QLatin1String("ds");
1348 if (submode == ChangeSurroundingSubMode)
1349 return QLatin1String("c");
1350 if (submode == AddSurroundingSubMode)
1351 return QLatin1String("y");
1352 if (submode == ExchangeSubMode)
1353 return QLatin1String("cx");
1354 if (submode == ReplaceWithRegisterSubMode)
1355 return QLatin1String("gr");
1356 if (submode == InvertCaseSubMode)
1357 return QLatin1String("g~");
1358 if (submode == DownCaseSubMode)
1359 return QLatin1String("gu");
1360 if (submode == UpCaseSubMode)
1361 return QLatin1String("gU");
1362 if (submode == IndentSubMode)
1363 return QLatin1String("=");
1364 if (submode == ShiftRightSubMode)
1365 return QLatin1String(">");
1366 if (submode == ShiftLeftSubMode)
1367 return QLatin1String("<");
1368
1369 return QString();
1370 }
1371
operator <<(QDebug ts,const Input & input)1372 QDebug operator<<(QDebug ts, const Input &input) { return input.dump(ts); }
1373
1374 class Inputs : public QVector<Input>
1375 {
1376 public:
1377 Inputs() = default;
1378
Inputs(const QString & str,bool noremap=true,bool silent=false)1379 explicit Inputs(const QString &str, bool noremap = true, bool silent = false)
1380 : m_noremap(noremap), m_silent(silent)
1381 {
1382 parseFrom(str);
1383 squeeze();
1384 }
1385
noremap() const1386 bool noremap() const { return m_noremap; }
1387
silent() const1388 bool silent() const { return m_silent; }
1389
1390 private:
1391 void parseFrom(const QString &str);
1392
1393 bool m_noremap = true;
1394 bool m_silent = false;
1395 };
1396
parseVimKeyName(const QString & keyName)1397 static Input parseVimKeyName(const QString &keyName)
1398 {
1399 if (keyName.length() == 1)
1400 return Input(keyName.at(0));
1401
1402 const QStringList keys = keyName.split('-');
1403 const int len = keys.length();
1404
1405 if (len == 1 && keys.at(0).toUpper() == "NOP")
1406 return Nop;
1407
1408 Qt::KeyboardModifiers mods = NoModifier;
1409 for (int i = 0; i < len - 1; ++i) {
1410 const QString &key = keys[i].toUpper();
1411 if (key == "S")
1412 mods |= Qt::ShiftModifier;
1413 else if (key == "C")
1414 mods |= ControlModifier;
1415 else
1416 return Input();
1417 }
1418
1419 if (!keys.isEmpty()) {
1420 const QString key = keys.last();
1421 if (key.length() == 1) {
1422 // simple character
1423 QChar c = key.at(0).toUpper();
1424 return Input(c.unicode(), mods);
1425 }
1426
1427 // find key name
1428 QMap<QString, int>::ConstIterator it = vimKeyNames().constFind(key.toUpper());
1429 if (it != vimKeyNames().end())
1430 return Input(*it, mods);
1431 }
1432
1433 return Input();
1434 }
1435
parseFrom(const QString & str)1436 void Inputs::parseFrom(const QString &str)
1437 {
1438 const int n = str.size();
1439 for (int i = 0; i < n; ++i) {
1440 const QChar c = str.at(i);
1441 if (c == '<') {
1442 int j = str.indexOf('>', i);
1443 Input input;
1444 if (j != -1) {
1445 const QString key = str.mid(i+1, j - i - 1);
1446 if (!key.contains('<'))
1447 input = parseVimKeyName(key);
1448 }
1449 if (input.isValid()) {
1450 append(input);
1451 i = j;
1452 } else {
1453 append(Input(c));
1454 }
1455 } else {
1456 append(Input(c));
1457 }
1458 }
1459 }
1460
1461 class History
1462 {
1463 public:
History()1464 History() : m_items(QString()) {}
1465 void append(const QString &item);
1466 const QString &move(QStringView prefix, int skip);
current() const1467 const QString ¤t() const { return m_items[m_index]; }
items() const1468 const QStringList &items() const { return m_items; }
restart()1469 void restart() { m_index = m_items.size() - 1; }
1470
1471 private:
1472 // Last item is always empty or current search prefix.
1473 QStringList m_items;
1474 int m_index = 0;
1475 };
1476
append(const QString & item)1477 void History::append(const QString &item)
1478 {
1479 if (item.isEmpty())
1480 return;
1481 m_items.pop_back();
1482 m_items.removeAll(item);
1483 m_items << item << QString();
1484 restart();
1485 }
1486
move(QStringView prefix,int skip)1487 const QString &History::move(QStringView prefix, int skip)
1488 {
1489 if (!current().startsWith(prefix))
1490 restart();
1491
1492 if (m_items.last() != prefix)
1493 m_items[m_items.size() - 1] = prefix.toString();
1494
1495 int i = m_index + skip;
1496 if (!prefix.isEmpty())
1497 for (; i >= 0 && i < m_items.size() && !m_items[i].startsWith(prefix); i += skip)
1498 ;
1499 if (i >= 0 && i < m_items.size())
1500 m_index = i;
1501
1502 return current();
1503 }
1504
1505 // Command line buffer with prompt (i.e. :, / or ? characters), text contents and cursor position.
1506 class CommandBuffer
1507 {
1508 public:
setPrompt(const QChar & prompt)1509 void setPrompt(const QChar &prompt) { m_prompt = prompt; }
setContents(const QString & s)1510 void setContents(const QString &s) { m_buffer = s; m_anchor = m_pos = s.size(); }
1511
setContents(const QString & s,int pos,int anchor=-1)1512 void setContents(const QString &s, int pos, int anchor = -1)
1513 {
1514 m_buffer = s; m_pos = m_userPos = pos; m_anchor = anchor >= 0 ? anchor : pos;
1515 }
1516
1517 #if QT_VERSION < QT_VERSION_CHECK(5,10,0)
userContents() const1518 QStringRef userContents() const { return QStringRef(&m_buffer).left(m_userPos); }
1519 #else
userContents() const1520 QStringView userContents() const { return QStringView{m_buffer}.left(m_userPos); }
1521 #endif
prompt() const1522 const QChar &prompt() const { return m_prompt; }
contents() const1523 const QString &contents() const { return m_buffer; }
isEmpty() const1524 bool isEmpty() const { return m_buffer.isEmpty(); }
cursorPos() const1525 int cursorPos() const { return m_pos; }
anchorPos() const1526 int anchorPos() const { return m_anchor; }
hasSelection() const1527 bool hasSelection() const { return m_pos != m_anchor; }
1528
insertChar(QChar c)1529 void insertChar(QChar c) { m_buffer.insert(m_pos++, c); m_anchor = m_userPos = m_pos; }
insertText(const QString & s)1530 void insertText(const QString &s)
1531 {
1532 m_buffer.insert(m_pos, s); m_anchor = m_userPos = m_pos = m_pos + s.size();
1533 }
deleteChar()1534 void deleteChar() { if (m_pos) m_buffer.remove(--m_pos, 1); m_anchor = m_userPos = m_pos; }
1535
moveLeft()1536 void moveLeft() { if (m_pos) m_userPos = --m_pos; }
moveRight()1537 void moveRight() { if (m_pos < m_buffer.size()) m_userPos = ++m_pos; }
moveStart()1538 void moveStart() { m_userPos = m_pos = 0; }
moveEnd()1539 void moveEnd() { m_userPos = m_pos = m_buffer.size(); }
1540
setHistoryAutoSave(bool autoSave)1541 void setHistoryAutoSave(bool autoSave) { m_historyAutoSave = autoSave; }
userContentsValid() const1542 bool userContentsValid() const { return m_userPos >= 0 && m_userPos <= m_buffer.size(); }
historyDown()1543 void historyDown() { if (userContentsValid()) setContents(m_history.move(userContents(), 1)); }
historyUp()1544 void historyUp() { if (userContentsValid()) setContents(m_history.move(userContents(), -1)); }
historyItems() const1545 const QStringList &historyItems() const { return m_history.items(); }
historyPush(const QString & item=QString ())1546 void historyPush(const QString &item = QString())
1547 {
1548 m_history.append(item.isNull() ? contents() : item);
1549 }
1550
clear()1551 void clear()
1552 {
1553 if (m_historyAutoSave)
1554 historyPush();
1555 m_buffer.clear();
1556 m_anchor = m_userPos = m_pos = 0;
1557 }
1558
display() const1559 QString display() const
1560 {
1561 QString msg(m_prompt);
1562 for (int i = 0; i != m_buffer.size(); ++i) {
1563 const QChar c = m_buffer.at(i);
1564 if (c.unicode() < 32) {
1565 msg += '^';
1566 msg += QChar(c.unicode() + 64);
1567 } else {
1568 msg += c;
1569 }
1570 }
1571 return msg;
1572 }
1573
deleteSelected()1574 void deleteSelected()
1575 {
1576 if (m_pos < m_anchor) {
1577 m_buffer.remove(m_pos, m_anchor - m_pos);
1578 m_anchor = m_pos;
1579 } else {
1580 m_buffer.remove(m_anchor, m_pos - m_anchor);
1581 m_pos = m_anchor;
1582 }
1583 }
1584
handleInput(const Input & input)1585 bool handleInput(const Input &input)
1586 {
1587 if (input.isShift(Key_Left)) {
1588 moveLeft();
1589 } else if (input.isShift(Key_Right)) {
1590 moveRight();
1591 } else if (input.isShift(Key_Home)) {
1592 moveStart();
1593 } else if (input.isShift(Key_End)) {
1594 moveEnd();
1595 } else if (input.isKey(Key_Left)) {
1596 moveLeft();
1597 m_anchor = m_pos;
1598 } else if (input.isKey(Key_Right)) {
1599 moveRight();
1600 m_anchor = m_pos;
1601 } else if (input.isKey(Key_Home)) {
1602 moveStart();
1603 m_anchor = m_pos;
1604 } else if (input.isKey(Key_End)) {
1605 moveEnd();
1606 m_anchor = m_pos;
1607 } else if (input.isKey(Key_Up) || input.isKey(Key_PageUp)) {
1608 historyUp();
1609 } else if (input.isKey(Key_Down) || input.isKey(Key_PageDown)) {
1610 historyDown();
1611 } else if (input.isKey(Key_Delete)) {
1612 if (hasSelection()) {
1613 deleteSelected();
1614 } else {
1615 if (m_pos < m_buffer.size())
1616 m_buffer.remove(m_pos, 1);
1617 else
1618 deleteChar();
1619 }
1620 } else if (!input.text().isEmpty()) {
1621 if (hasSelection())
1622 deleteSelected();
1623 insertText(input.text());
1624 } else {
1625 return false;
1626 }
1627 return true;
1628 }
1629
1630 private:
1631 QString m_buffer;
1632 QChar m_prompt;
1633 History m_history;
1634 int m_pos = 0;
1635 int m_anchor = 0;
1636 int m_userPos = 0; // last position of inserted text (for retrieving history items)
1637 bool m_historyAutoSave = true; // store items to history on clear()?
1638 };
1639
1640 // Mappings for a specific mode (trie structure)
1641 class ModeMapping : public QMap<Input, ModeMapping>
1642 {
1643 public:
value() const1644 const Inputs &value() const { return m_value; }
setValue(const Inputs & value)1645 void setValue(const Inputs &value) { m_value = value; }
1646 private:
1647 Inputs m_value;
1648 };
1649
1650 // Mappings for all modes
1651 using Mappings = QHash<char, ModeMapping>;
1652
1653 // Iterator for mappings
1654 class MappingsIterator : public QVector<ModeMapping::Iterator>
1655 {
1656 public:
MappingsIterator(Mappings * mappings,char mode=-1,const Inputs & inputs=Inputs ())1657 MappingsIterator(Mappings *mappings, char mode = -1, const Inputs &inputs = Inputs())
1658 : m_parent(mappings)
1659 {
1660 reset(mode);
1661 walk(inputs);
1662 }
1663
1664 // Reset iterator state. Keep previous mode if 0.
reset(char mode=0)1665 void reset(char mode = 0)
1666 {
1667 clear();
1668 m_lastValid = -1;
1669 m_currentInputs.clear();
1670 if (mode != 0) {
1671 m_mode = mode;
1672 if (mode != -1)
1673 m_modeMapping = m_parent->find(mode);
1674 }
1675 }
1676
isValid() const1677 bool isValid() const { return !empty(); }
1678
1679 // Return true if mapping can be extended.
canExtend() const1680 bool canExtend() const { return isValid() && !last()->empty(); }
1681
1682 // Return true if this mapping can be used.
isComplete() const1683 bool isComplete() const { return m_lastValid != -1; }
1684
1685 // Return size of current map.
mapLength() const1686 int mapLength() const { return m_lastValid + 1; }
1687
walk(const Input & input)1688 bool walk(const Input &input)
1689 {
1690 m_currentInputs.append(input);
1691
1692 if (m_modeMapping == m_parent->end())
1693 return false;
1694
1695 ModeMapping::Iterator it;
1696 if (isValid()) {
1697 it = last()->find(input);
1698 if (it == last()->end())
1699 return false;
1700 } else {
1701 it = m_modeMapping->find(input);
1702 if (it == m_modeMapping->end())
1703 return false;
1704 }
1705
1706 if (!it->value().isEmpty())
1707 m_lastValid = size();
1708 append(it);
1709
1710 return true;
1711 }
1712
walk(const Inputs & inputs)1713 bool walk(const Inputs &inputs)
1714 {
1715 for (const Input &input : inputs) {
1716 if (!walk(input))
1717 return false;
1718 }
1719 return true;
1720 }
1721
1722 // Return current mapped value. Iterator must be valid.
inputs() const1723 const Inputs &inputs() const
1724 {
1725 return at(m_lastValid)->value();
1726 }
1727
remove()1728 void remove()
1729 {
1730 if (isValid()) {
1731 if (canExtend()) {
1732 last()->setValue(Inputs());
1733 } else {
1734 if (size() > 1) {
1735 while (last()->empty()) {
1736 at(size() - 2)->erase(last());
1737 pop_back();
1738 if (size() == 1 || !last()->value().isEmpty())
1739 break;
1740 }
1741 if (last()->empty() && last()->value().isEmpty())
1742 m_modeMapping->erase(last());
1743 } else if (last()->empty() && !last()->value().isEmpty()) {
1744 m_modeMapping->erase(last());
1745 }
1746 }
1747 }
1748 }
1749
setInputs(const Inputs & key,const Inputs & inputs,bool unique=false)1750 void setInputs(const Inputs &key, const Inputs &inputs, bool unique = false)
1751 {
1752 ModeMapping *current = &(*m_parent)[m_mode];
1753 for (const Input &input : key)
1754 current = &(*current)[input];
1755 if (!unique || current->value().isEmpty())
1756 current->setValue(inputs);
1757 }
1758
currentInputs() const1759 const Inputs ¤tInputs() const { return m_currentInputs; }
1760
1761 private:
1762 Mappings *m_parent;
1763 Mappings::Iterator m_modeMapping;
1764 int m_lastValid = -1;
1765 char m_mode = 0;
1766 Inputs m_currentInputs;
1767 };
1768
1769 // state of current mapping
1770 struct MappingState {
1771 MappingState() = default;
MappingStateFakeVim::Internal::MappingState1772 MappingState(bool noremap, bool silent, bool editBlock)
1773 : noremap(noremap), silent(silent), editBlock(editBlock) {}
1774 bool noremap = false;
1775 bool silent = false;
1776 bool editBlock = false;
1777 };
1778
1779 class FakeVimHandler::Private : public QObject
1780 {
1781 public:
1782 Private(FakeVimHandler *parent, QWidget *widget);
1783
1784 EventResult handleEvent(QKeyEvent *ev);
1785 bool wantsOverride(QKeyEvent *ev);
1786 bool parseExCommand(QString *line, ExCommand *cmd);
1787 bool parseLineRange(QString *line, ExCommand *cmd);
1788 int parseLineAddress(QString *cmd);
1789 void parseRangeCount(const QString &line, Range *range) const;
1790 void handleCommand(const QString &cmd); // Sets m_tc + handleExCommand
1791 void handleExCommand(const QString &cmd);
1792
1793 void installEventFilter();
1794 void removeEventFilter();
1795 void passShortcuts(bool enable);
1796 void setupWidget();
1797 void restoreWidget(int tabSize);
1798
1799 friend class FakeVimHandler;
1800
1801 void init();
1802 void focus();
1803 void unfocus();
1804 void fixExternalCursor(bool focus);
1805 void fixExternalCursorPosition(bool focus);
1806
1807 // Call before any FakeVim processing (import cursor position from editor)
1808 void enterFakeVim();
1809 // Call after any FakeVim processing
1810 // (if needUpdate is true, export cursor position to editor and scroll)
1811 void leaveFakeVim(bool needUpdate = true);
1812 void leaveFakeVim(EventResult eventResult);
1813
1814 EventResult handleKey(const Input &input);
1815 EventResult handleDefaultKey(const Input &input);
1816 bool handleCommandBufferPaste(const Input &input);
1817 EventResult handleCurrentMapAsDefault();
1818 void prependInputs(const QVector<Input> &inputs); // Handle inputs.
1819 void prependMapping(const Inputs &inputs); // Handle inputs as mapping.
1820 bool expandCompleteMapping(); // Return false if current mapping is not complete.
1821 bool extendMapping(const Input &input); // Return false if no suitable mappig found.
1822 void endMapping();
1823 bool canHandleMapping();
1824 void clearPendingInput();
1825 void waitForMapping();
1826 EventResult stopWaitForMapping(bool hasInput);
1827 EventResult handleInsertOrReplaceMode(const Input &);
1828 void handleInsertMode(const Input &);
1829 void handleReplaceMode(const Input &);
1830 void finishInsertMode();
1831
1832 EventResult handleCommandMode(const Input &);
1833
1834 // return true only if input in current mode and sub-mode was correctly handled
1835 bool handleEscape();
1836 bool handleNoSubMode(const Input &);
1837 bool handleChangeDeleteYankSubModes(const Input &);
1838 void handleChangeDeleteYankSubModes();
1839 bool handleReplaceSubMode(const Input &);
1840 bool handleCommentSubMode(const Input &);
1841 bool handleReplaceWithRegisterSubMode(const Input &);
1842 bool handleExchangeSubMode(const Input &);
1843 bool handleDeleteChangeSurroundingSubMode(const Input &);
1844 bool handleAddSurroundingSubMode(const Input &);
1845 bool handleFilterSubMode(const Input &);
1846 bool handleRegisterSubMode(const Input &);
1847 bool handleShiftSubMode(const Input &);
1848 bool handleChangeCaseSubMode(const Input &);
1849 bool handleWindowSubMode(const Input &);
1850 bool handleZSubMode(const Input &);
1851 bool handleCapitalZSubMode(const Input &);
1852 bool handleMacroRecordSubMode(const Input &);
1853 bool handleMacroExecuteSubMode(const Input &);
1854
1855 bool handleCount(const Input &); // Handle count for commands (return false if input isn't count).
1856 bool handleMovement(const Input &);
1857
1858 EventResult handleExMode(const Input &);
1859 EventResult handleSearchSubSubMode(const Input &);
1860 bool handleCommandSubSubMode(const Input &);
1861 void fixSelection(); // Fix selection according to current range, move and command modes.
1862 bool finishSearch();
1863 void finishMovement(const QString &dotCommandMovement = QString());
1864
1865 // Returns to insert/replace mode after <C-O> command in insert mode,
1866 // otherwise returns to command mode.
1867 void leaveCurrentMode();
1868
1869 // Clear data for current (possibly incomplete) command in current mode.
1870 // I.e. clears count, register, g flag, sub-modes etc.
1871 void clearCurrentMode();
1872
1873 QTextCursor search(const SearchData &sd, int startPos, int count, bool showMessages);
1874 void search(const SearchData &sd, bool showMessages = true);
1875 bool searchNext(bool forward = true);
1876 void searchBalanced(bool forward, QChar needle, QChar other);
1877 void highlightMatches(const QString &needle);
1878 void stopIncrementalFind();
1879 void updateFind(bool isComplete);
1880
1881 void resetCount();
1882 bool isInputCount(const Input &) const; // Return true if input can be used as count for commands.
mvCount() const1883 int mvCount() const { return qMax(1, g.mvcount); }
opCount() const1884 int opCount() const { return qMax(1, g.opcount); }
count() const1885 int count() const { return mvCount() * opCount(); }
block() const1886 QTextBlock block() const { return m_cursor.block(); }
leftDist() const1887 int leftDist() const { return position() - block().position(); }
rightDist() const1888 int rightDist() const { return block().length() - leftDist() - (isVisualCharMode() ? 0 : 1); }
atBlockStart() const1889 bool atBlockStart() const { return m_cursor.atBlockStart(); }
atBlockEnd() const1890 bool atBlockEnd() const { return m_cursor.atBlockEnd(); }
atEndOfLine() const1891 bool atEndOfLine() const { return atBlockEnd() && block().length() > 1; }
atDocumentEnd() const1892 bool atDocumentEnd() const { return position() >= lastPositionInDocument(true); }
atDocumentStart() const1893 bool atDocumentStart() const { return m_cursor.atStart(); }
1894
1895 bool atEmptyLine(int pos) const;
1896 bool atEmptyLine(const QTextCursor &tc) const;
1897 bool atEmptyLine() const;
1898 bool atBoundary(bool end, bool simple, bool onlyWords = false,
1899 const QTextCursor &tc = QTextCursor()) const;
1900 bool atWordBoundary(bool end, bool simple, const QTextCursor &tc = QTextCursor()) const;
1901 bool atWordStart(bool simple, const QTextCursor &tc = QTextCursor()) const;
1902 bool atWordEnd(bool simple, const QTextCursor &tc = QTextCursor()) const;
1903 bool isFirstNonBlankOnLine(int pos);
1904
1905 int lastPositionInDocument(bool ignoreMode = false) const; // Returns last valid position in doc.
1906 int firstPositionInLine(int line, bool onlyVisibleLines = true) const; // 1 based line, 0 based pos
1907 int lastPositionInLine(int line, bool onlyVisibleLines = true) const; // 1 based line, 0 based pos
1908 int lineForPosition(int pos) const; // 1 based line, 0 based pos
1909 QString lineContents(int line) const; // 1 based line
1910 QString textAt(int from, int to) const;
1911 void setLineContents(int line, const QString &contents); // 1 based line
1912 int blockBoundary(const QString &left, const QString &right,
1913 bool end, int count) const; // end or start position of current code block
1914 int lineNumber(const QTextBlock &block) const;
1915
1916 int columnAt(int pos) const;
1917 int blockNumberAt(int pos) const;
1918 QTextBlock blockAt(int pos) const;
1919 QTextBlock nextLine(const QTextBlock &block) const; // following line (respects wrapped parts)
1920 QTextBlock previousLine(const QTextBlock &block) const; // previous line (respects wrapped parts)
1921
1922 int linesOnScreen() const;
1923 int linesInDocument() const;
1924
1925 // The following use all zero-based counting.
1926 int cursorLineOnScreen() const;
1927 int cursorLine() const;
1928 int cursorBlockNumber() const; // "." address
1929 int physicalCursorColumn() const; // as stored in the data
1930 int logicalCursorColumn() const; // as visible on screen
1931 int physicalToLogicalColumn(int physical, const QString &text) const;
1932 int logicalToPhysicalColumn(int logical, const QString &text) const;
1933 int windowScrollOffset() const; // return scrolloffset but max half the current window height
1934 Column cursorColumn() const; // as visible on screen
1935 void updateFirstVisibleLine();
1936 int firstVisibleLine() const;
1937 int lastVisibleLine() const;
1938 int lineOnTop(int count = 1) const; // [count]-th line from top reachable without scrolling
1939 int lineOnBottom(int count = 1) const; // [count]-th line from bottom reachable without scrolling
1940 void scrollToLine(int line);
1941 void scrollUp(int count);
scrollDown(int count)1942 void scrollDown(int count) { scrollUp(-count); }
1943 void updateScrollOffset();
1944 void alignViewportToCursor(Qt::AlignmentFlag align, int line = -1,
1945 bool moveToNonBlank = false);
1946
1947 int lineToBlockNumber(int line) const;
1948
1949 void setCursorPosition(const CursorPosition &p);
1950 void setCursorPosition(QTextCursor *tc, const CursorPosition &p);
1951
1952 // Helper functions for indenting/
1953 bool isElectricCharacter(QChar c) const;
1954 void indentSelectedText(QChar lastTyped = QChar());
1955 void indentText(const Range &range, QChar lastTyped = QChar());
1956 void shiftRegionLeft(int repeat = 1);
1957 void shiftRegionRight(int repeat = 1);
1958
1959 void moveToFirstNonBlankOnLine();
1960 void moveToFirstNonBlankOnLine(QTextCursor *tc);
1961 void moveToFirstNonBlankOnLineVisually();
1962 void moveToNonBlankOnLine(QTextCursor *tc);
1963 void moveToTargetColumn();
1964 void setTargetColumn();
1965 void moveToMatchingParanthesis();
1966 void moveToBoundary(bool simple, bool forward = true);
1967 void moveToNextBoundary(bool end, int count, bool simple, bool forward);
1968 void moveToNextBoundaryStart(int count, bool simple, bool forward = true);
1969 void moveToNextBoundaryEnd(int count, bool simple, bool forward = true);
1970 void moveToBoundaryStart(int count, bool simple, bool forward = true);
1971 void moveToBoundaryEnd(int count, bool simple, bool forward = true);
1972 void moveToNextWord(bool end, int count, bool simple, bool forward, bool emptyLines);
1973 void moveToNextWordStart(int count, bool simple, bool forward = true, bool emptyLines = true);
1974 void moveToNextWordEnd(int count, bool simple, bool forward = true, bool emptyLines = true);
1975 void moveToWordStart(int count, bool simple, bool forward = true, bool emptyLines = true);
1976 void moveToWordEnd(int count, bool simple, bool forward = true, bool emptyLines = true);
1977
1978 // Convenience wrappers to reduce line noise.
1979 void moveToStartOfLine();
1980 void moveToStartOfLineVisually();
1981 void moveToEndOfLine();
1982 void moveToEndOfLineVisually();
1983 void moveToEndOfLineVisually(QTextCursor *tc);
1984 void moveBehindEndOfLine();
moveUp(int n=1)1985 void moveUp(int n = 1) { moveDown(-n); }
1986 void moveDown(int n = 1);
moveUpVisually(int n=1)1987 void moveUpVisually(int n = 1) { moveDownVisually(-n); }
1988 void moveDownVisually(int n = 1);
moveVertically(int n=1)1989 void moveVertically(int n = 1) {
1990 if (g.gflag) {
1991 g.movetype = MoveExclusive;
1992 moveDownVisually(n);
1993 } else {
1994 g.movetype = MoveLineWise;
1995 moveDown(n);
1996 }
1997 }
1998 void movePageDown(int count = 1);
movePageUp(int count=1)1999 void movePageUp(int count = 1) { movePageDown(-count); }
dump(const char * msg) const2000 void dump(const char *msg) const {
2001 qDebug() << msg << "POS: " << anchor() << position()
2002 << "VISUAL: " << g.visualMode;
2003 }
moveRight(int n=1)2004 void moveRight(int n = 1) {
2005 if (isVisualCharMode()) {
2006 const QTextBlock currentBlock = block();
2007 const int max = currentBlock.position() + currentBlock.length() - 1;
2008 const int pos = position() + n;
2009 setPosition(qMin(pos, max));
2010 } else {
2011 m_cursor.movePosition(Right, KeepAnchor, n);
2012 }
2013 if (atEndOfLine())
2014 q->fold(1, false);
2015 setTargetColumn();
2016 }
moveLeft(int n=1)2017 void moveLeft(int n = 1) {
2018 m_cursor.movePosition(Left, KeepAnchor, n);
2019 setTargetColumn();
2020 }
moveToNextCharacter()2021 void moveToNextCharacter() {
2022 moveRight();
2023 if (atEndOfLine())
2024 moveRight();
2025 }
moveToPreviousCharacter()2026 void moveToPreviousCharacter() {
2027 moveLeft();
2028 if (atBlockStart())
2029 moveLeft();
2030 }
setAnchor()2031 void setAnchor() {
2032 m_cursor.setPosition(position(), MoveAnchor);
2033 }
setAnchor(int position)2034 void setAnchor(int position) {
2035 m_cursor.setPosition(position, KeepAnchor);
2036 }
setPosition(int position)2037 void setPosition(int position) {
2038 m_cursor.setPosition(position, KeepAnchor);
2039 }
setAnchorAndPosition(int anchor,int position)2040 void setAnchorAndPosition(int anchor, int position) {
2041 m_cursor.setPosition(anchor, MoveAnchor);
2042 m_cursor.setPosition(position, KeepAnchor);
2043 }
2044
2045 // Set cursor in text editor widget.
2046 void commitCursor();
2047
2048 // Restore cursor from editor widget.
2049 // Update selection, record jump and target column if cursor position
2050 // changes externally (e.g. by code completion).
2051 void pullCursor();
2052
2053 QTextCursor editorCursor() const;
2054
2055 // Values to save when starting FakeVim processing.
2056 int m_firstVisibleLine;
2057 QTextCursor m_cursor;
2058 bool m_cursorNeedsUpdate;
2059
moveToPreviousParagraph(int count=1)2060 bool moveToPreviousParagraph(int count = 1) { return moveToNextParagraph(-count); }
2061 bool moveToNextParagraph(int count = 1);
2062 void moveToParagraphStartOrEnd(int direction = 1);
2063
2064 bool handleFfTt(const QString &key, bool repeats = false);
2065
2066 void enterVisualInsertMode(QChar command);
2067 void enterReplaceMode();
2068 void enterInsertMode();
2069 void enterInsertOrReplaceMode(Mode mode);
2070 void enterCommandMode(Mode returnToMode = CommandMode);
2071 void enterExMode(const QString &contents = QString());
2072 void showMessage(MessageLevel level, const QString &msg);
clearMessage()2073 void clearMessage() { showMessage(MessageInfo, QString()); }
2074 void notImplementedYet();
2075 void updateMiniBuffer();
2076 void updateSelection();
2077 void updateHighlights();
2078 void updateCursorShape();
2079 void setThinCursor(bool enable = true);
2080 bool hasThinCursor() const;
2081 QWidget *editor() const;
document() const2082 QTextDocument *document() const { return EDITOR(document()); }
characterAt(int pos) const2083 QChar characterAt(int pos) const { return document()->characterAt(pos); }
characterAtCursor() const2084 QChar characterAtCursor() const { return characterAt(position()); }
2085
2086 void joinPreviousEditBlock();
2087 void beginEditBlock(bool largeEditBlock = false);
beginLargeEditBlock()2088 void beginLargeEditBlock() { beginEditBlock(true); }
2089 void endEditBlock();
breakEditBlock()2090 void breakEditBlock() { m_buffer->breakEditBlock = true; }
2091
canModifyBufferData() const2092 bool canModifyBufferData() const { return m_buffer->currentHandler.data() == this; }
2093
2094 void onContentsChanged(int position, int charsRemoved, int charsAdded);
2095 void onCursorPositionChanged();
2096 void onUndoCommandAdded();
2097
2098 void onInputTimeout();
2099 void onFixCursorTimeout();
2100
isCommandLineMode() const2101 bool isCommandLineMode() const { return g.mode == ExMode || g.subsubmode == SearchSubSubMode; }
isInsertMode() const2102 bool isInsertMode() const { return g.mode == InsertMode || g.mode == ReplaceMode; }
2103 // Waiting for movement operator.
isOperatorPending() const2104 bool isOperatorPending() const {
2105 return g.submode == ChangeSubMode
2106 || g.submode == DeleteSubMode
2107 || g.submode == ExchangeSubMode
2108 || g.submode == CommentSubMode
2109 || g.submode == ReplaceWithRegisterSubMode
2110 || g.submode == AddSurroundingSubMode
2111 || g.submode == FilterSubMode
2112 || g.submode == IndentSubMode
2113 || g.submode == ShiftLeftSubMode
2114 || g.submode == ShiftRightSubMode
2115 || g.submode == InvertCaseSubMode
2116 || g.submode == DownCaseSubMode
2117 || g.submode == UpCaseSubMode
2118 || g.submode == YankSubMode; }
2119
isVisualMode() const2120 bool isVisualMode() const { return g.visualMode != NoVisualMode; }
isNoVisualMode() const2121 bool isNoVisualMode() const { return g.visualMode == NoVisualMode; }
isVisualCharMode() const2122 bool isVisualCharMode() const { return g.visualMode == VisualCharMode; }
isVisualLineMode() const2123 bool isVisualLineMode() const { return g.visualMode == VisualLineMode; }
isVisualBlockMode() const2124 bool isVisualBlockMode() const { return g.visualMode == VisualBlockMode; }
2125 char currentModeCode() const;
2126 void updateEditor();
2127
2128 void selectTextObject(bool simple, bool inner);
2129 void selectWordTextObject(bool inner);
2130 void selectWORDTextObject(bool inner);
2131 void selectSentenceTextObject(bool inner);
2132 void selectParagraphTextObject(bool inner);
2133 bool changeNumberTextObject(int count);
2134 // return true only if cursor is in a block delimited with correct characters
2135 bool selectBlockTextObject(bool inner, QChar left, QChar right);
2136 bool selectQuotedStringTextObject(bool inner, const QString "e);
2137 bool selectArgumentTextObject(bool inner);
2138
2139 void commitInsertState();
2140 void invalidateInsertState();
2141 bool isInsertStateValid() const;
2142 void clearLastInsertion();
2143 void ensureCursorVisible();
2144 void insertInInsertMode(const QString &text);
2145
2146 // Macro recording
2147 bool startRecording(const Input &input);
2148 void record(const Input &input);
2149 void stopRecording();
2150 bool executeRegister(int reg);
2151
2152 // Handle current command as synonym
2153 void handleAs(const QString &command);
2154
2155 public:
2156 QTextEdit *m_textedit;
2157 QPlainTextEdit *m_plaintextedit;
2158 bool m_wasReadOnly; // saves read-only state of document
2159
2160 bool m_inFakeVim; // true if currently processing a key press or a command
2161
2162 FakeVimHandler *q;
2163 int m_register;
2164 BlockInsertMode m_visualBlockInsert;
2165
2166 bool m_anchorPastEnd;
2167 bool m_positionPastEnd; // '$' & 'l' in visual mode can move past eol
2168
2169 QString m_currentFileName;
2170
2171 int m_findStartPosition;
2172
anchor() const2173 int anchor() const { return m_cursor.anchor(); }
position() const2174 int position() const { return m_cursor.position(); }
2175
2176 // Transform text selected by cursor in current visual mode.
2177 using Transformation = std::function<QString(const QString &)>;
2178 void transformText(const Range &range, QTextCursor &tc, const std::function<void()> &transform) const;
2179 void transformText(const Range &range, const Transformation &transform);
2180
2181 void insertText(QTextCursor &tc, const QString &text);
2182 void insertText(const Register ®);
2183 void removeText(const Range &range);
2184
2185 void invertCase(const Range &range);
2186
2187 void toggleComment(const Range &range);
2188
2189 void exchangeRange(const Range &range);
2190
2191 void replaceWithRegister(const Range &range);
2192
2193 void surroundCurrentRange(const Input &input, const QString &prefix = {});
2194
2195 void upCase(const Range &range);
2196
2197 void downCase(const Range &range);
2198
2199 void replaceText(const Range &range, const QString &str);
2200
2201 QString selectText(const Range &range) const;
2202 void setCurrentRange(const Range &range);
currentRange() const2203 Range currentRange() const { return Range(position(), anchor(), g.rangemode); }
2204
2205 void yankText(const Range &range, int toregister);
2206
2207 void pasteText(bool afterCursor);
2208
2209 void cutSelectedText(int reg = 0);
2210
2211 void joinLines(int count, bool preserveSpace = false);
2212
2213 void insertNewLine();
2214
2215 bool handleInsertInEditor(const Input &input);
2216 bool passEventToEditor(QEvent &event, QTextCursor &tc); // Pass event to editor widget without filtering. Returns true if event was processed.
2217
2218 // undo handling
revision() const2219 int revision() const { return document()->availableUndoSteps(); }
2220 void undoRedo(bool undo);
2221 void undo();
2222 void redo();
2223 void pushUndoState(bool overwrite = true);
2224
2225 // extra data for '.'
2226 void replay(const QString &text, int repeat = 1);
setDotCommand(const QString & cmd)2227 void setDotCommand(const QString &cmd) { g.dotCommand = cmd; }
setDotCommand(const QString & cmd,int n)2228 void setDotCommand(const QString &cmd, int n) { g.dotCommand = cmd.arg(n); }
2229 QString visualDotCommand() const;
2230
2231 // visual modes
2232 void toggleVisualMode(VisualMode visualMode);
2233 void leaveVisualMode();
2234 void saveLastVisualMode();
2235
2236 // marks
2237 Mark mark(QChar code) const;
2238 void setMark(QChar code, CursorPosition position);
2239 // jump to valid mark return true if mark is valid and local
2240 bool jumpToMark(QChar mark, bool backTickMode);
2241 // update marks on undo/redo
2242 void updateMarks(const Marks &newMarks);
markLessPosition() const2243 CursorPosition markLessPosition() const { return mark('<').position(document()); }
markGreaterPosition() const2244 CursorPosition markGreaterPosition() const { return mark('>').position(document()); }
2245
2246 int m_targetColumn; // -1 if past end of line
2247 int m_visualTargetColumn; // 'l' can move past eol in visual mode only
2248 int m_targetColumnWrapped; // column in current part of wrapped line
2249
2250 // auto-indent
2251 QString tabExpand(int len) const;
2252 Column indentation(const QString &line) const;
2253 void insertAutomaticIndentation(bool goingDown, bool forceAutoIndent = false);
2254 // number of autoindented characters
2255 void handleStartOfLine();
2256
2257 // register handling
2258 QString registerContents(int reg) const;
2259 void setRegister(int reg, const QString &contents, RangeMode mode);
2260 RangeMode registerRangeMode(int reg) const;
2261 void getRegisterType(int *reg, bool *isClipboard, bool *isSelection, bool *append = nullptr) const;
2262
2263 void recordJump(int position = -1);
2264 void jump(int distance);
2265
2266 QList<QTextEdit::ExtraSelection> m_extraSelections;
2267 QTextCursor m_searchCursor;
2268 int m_searchStartPosition;
2269 int m_searchFromScreenLine;
2270 QString m_highlighted; // currently highlighted text
2271
2272 bool handleExCommandHelper(ExCommand &cmd); // Returns success.
2273 bool handleExPluginCommand(const ExCommand &cmd); // Handled by plugin?
2274 bool handleExBangCommand(const ExCommand &cmd);
2275 bool handleExYankDeleteCommand(const ExCommand &cmd);
2276 bool handleExChangeCommand(const ExCommand &cmd);
2277 bool handleExMoveCommand(const ExCommand &cmd);
2278 bool handleExJoinCommand(const ExCommand &cmd);
2279 bool handleExGotoCommand(const ExCommand &cmd);
2280 bool handleExHistoryCommand(const ExCommand &cmd);
2281 bool handleExRegisterCommand(const ExCommand &cmd);
2282 bool handleExMapCommand(const ExCommand &cmd);
2283 bool handleExNohlsearchCommand(const ExCommand &cmd);
2284 bool handleExNormalCommand(const ExCommand &cmd);
2285 bool handleExReadCommand(const ExCommand &cmd);
2286 bool handleExUndoRedoCommand(const ExCommand &cmd);
2287 bool handleExSetCommand(const ExCommand &cmd);
2288 bool handleExSortCommand(const ExCommand &cmd);
2289 bool handleExShiftCommand(const ExCommand &cmd);
2290 bool handleExSourceCommand(const ExCommand &cmd);
2291 bool handleExSubstituteCommand(const ExCommand &cmd);
2292 bool handleExTabNextCommand(const ExCommand &cmd);
2293 bool handleExTabPreviousCommand(const ExCommand &cmd);
2294 bool handleExWriteCommand(const ExCommand &cmd);
2295 bool handleExEchoCommand(const ExCommand &cmd);
2296
2297 void setTabSize(int tabSize);
2298 void setupCharClass();
2299 int charClass(QChar c, bool simple) const;
2300 signed char m_charClass[256];
2301
2302 int m_ctrlVAccumulator;
2303 int m_ctrlVLength;
2304 int m_ctrlVBase;
2305
2306 QTimer m_fixCursorTimer;
2307 QTimer m_inputTimer;
2308
2309 void miniBufferTextEdited(const QString &text, int cursorPos, int anchorPos);
2310
2311 // Data shared among editors with same document.
2312 struct BufferData
2313 {
2314 QStack<State> undo;
2315 QStack<State> redo;
2316 State undoState;
2317 int lastRevision = 0;
2318
2319 int editBlockLevel = 0; // current level of edit blocks
2320 bool breakEditBlock = false; // if true, joinPreviousEditBlock() starts new edit block
2321
2322 QStack<CursorPosition> jumpListUndo;
2323 QStack<CursorPosition> jumpListRedo;
2324
2325 VisualMode lastVisualMode = NoVisualMode;
2326 bool lastVisualModeInverted = false;
2327
2328 Marks marks;
2329
2330 // Insert state to get last inserted text.
2331 struct InsertState {
2332 int pos1;
2333 int pos2;
2334 int backspaces;
2335 int deletes;
2336 QSet<int> spaces;
2337 bool insertingSpaces;
2338 QString textBeforeCursor;
2339 bool newLineBefore;
2340 bool newLineAfter;
2341 } insertState;
2342
2343 QString lastInsertion;
2344
2345 // If there are multiple editors with same document,
2346 // only the handler with last focused editor can change buffer data.
2347 QPointer<FakeVimHandler::Private> currentHandler;
2348 };
2349
2350 using BufferDataPtr = QSharedPointer<BufferData>;
2351 void pullOrCreateBufferData();
2352 BufferDataPtr m_buffer;
2353
2354 // Data shared among all editors.
2355 static struct GlobalData
2356 {
GlobalDataFakeVim::Internal::FakeVimHandler::Private::GlobalData2357 GlobalData()
2358 : mappings()
2359 , currentMap(&mappings)
2360 {
2361 commandBuffer.setPrompt(':');
2362 }
2363
2364 // Current state.
2365 bool passing = false; // let the core see the next event
2366 Mode mode = CommandMode;
2367 SubMode submode = NoSubMode;
2368 SubSubMode subsubmode = NoSubSubMode;
2369 Input subsubdata;
2370 VisualMode visualMode = NoVisualMode;
2371 Input minibufferData;
2372
2373 // [count] for current command, 0 if no [count] available
2374 int mvcount = 0;
2375 int opcount = 0;
2376
2377 MoveType movetype = MoveInclusive;
2378 RangeMode rangemode = RangeCharMode;
2379 bool gflag = false; // whether current command started with 'g'
2380
2381 // Extra data for ';'.
2382 Input semicolonType; // 'f', 'F', 't', 'T'
2383 QString semicolonKey;
2384
2385 // Repetition.
2386 QString dotCommand;
2387
2388 QHash<int, Register> registers;
2389
2390 // All mappings.
2391 Mappings mappings;
2392
2393 // Input.
2394 QList<Input> pendingInput;
2395 MappingsIterator currentMap;
2396 QStack<MappingState> mapStates;
2397 int mapDepth = 0;
2398
2399 // Command line buffers.
2400 CommandBuffer commandBuffer;
2401 CommandBuffer searchBuffer;
2402
2403 // Current mini buffer message.
2404 QString currentMessage;
2405 MessageLevel currentMessageLevel = MessageInfo;
2406 QString currentCommand;
2407
2408 // Search state.
2409 QString lastSearch; // last search expression as entered by user
2410 QString lastNeedle; // last search expression translated with vimPatternToQtPattern()
2411 bool lastSearchForward = false; // last search command was '/' or '*'
2412 bool highlightsCleared = false; // ':nohlsearch' command is active until next search
2413 bool findPending = false; // currently searching using external tool (until editor is focused again)
2414
2415 // Last substitution command.
2416 QString lastSubstituteFlags;
2417 QString lastSubstitutePattern;
2418 QString lastSubstituteReplacement;
2419
2420 // Global marks.
2421 Marks marks;
2422
2423 // Return to insert/replace mode after single command (<C-O>).
2424 Mode returnToMode = CommandMode;
2425
2426 // Currently recorded macro
2427 bool isRecording = false;
2428 QString recorded;
2429 int currentRegister = 0;
2430 int lastExecutedRegister = 0;
2431
2432 // If empty, cx{motion} will store the range defined by {motion} here.
2433 // If non-empty, cx{motion} replaces the {motion} with selectText(*exchangeData)
2434 Utils::optional<Range> exchangeRange;
2435
2436 bool surroundUpperCaseS; // True for yS and cS, false otherwise
2437 QString surroundFunction; // Used for storing the function name provided to ys{motion}f
2438 } g;
2439
2440 FakeVimSettings &s = *fakeVimSettings();
2441 };
2442
2443 FakeVimHandler::Private::GlobalData FakeVimHandler::Private::g;
2444
Private(FakeVimHandler * parent,QWidget * widget)2445 FakeVimHandler::Private::Private(FakeVimHandler *parent, QWidget *widget)
2446 {
2447 q = parent;
2448 m_textedit = qobject_cast<QTextEdit *>(widget);
2449 m_plaintextedit = qobject_cast<QPlainTextEdit *>(widget);
2450
2451 init();
2452
2453 if (editor()) {
2454 connect(EDITOR(document()), &QTextDocument::contentsChange,
2455 this, &Private::onContentsChanged);
2456 connect(EDITOR(document()), &QTextDocument::undoCommandAdded,
2457 this, &Private::onUndoCommandAdded);
2458 m_buffer->lastRevision = revision();
2459 }
2460 }
2461
init()2462 void FakeVimHandler::Private::init()
2463 {
2464 m_cursor = QTextCursor(document());
2465 m_cursorNeedsUpdate = true;
2466 m_inFakeVim = false;
2467 m_findStartPosition = -1;
2468 m_visualBlockInsert = NoneBlockInsertMode;
2469 m_positionPastEnd = false;
2470 m_anchorPastEnd = false;
2471 m_register = '"';
2472 m_targetColumn = 0;
2473 m_visualTargetColumn = 0;
2474 m_targetColumnWrapped = 0;
2475 m_searchStartPosition = 0;
2476 m_searchFromScreenLine = 0;
2477 m_firstVisibleLine = 0;
2478 m_ctrlVAccumulator = 0;
2479 m_ctrlVLength = 0;
2480 m_ctrlVBase = 0;
2481
2482 initSingleShotTimer(&m_fixCursorTimer, 0, this, &FakeVimHandler::Private::onFixCursorTimeout);
2483 initSingleShotTimer(&m_inputTimer, 1000, this, &FakeVimHandler::Private::onInputTimeout);
2484
2485 pullOrCreateBufferData();
2486 setupCharClass();
2487 }
2488
focus()2489 void FakeVimHandler::Private::focus()
2490 {
2491 m_buffer->currentHandler = this;
2492
2493 enterFakeVim();
2494
2495 stopIncrementalFind();
2496 if (isCommandLineMode()) {
2497 if (g.subsubmode == SearchSubSubMode) {
2498 setPosition(m_searchStartPosition);
2499 scrollToLine(m_searchFromScreenLine);
2500 } else {
2501 leaveVisualMode();
2502 setPosition(qMin(position(), anchor()));
2503 }
2504 leaveCurrentMode();
2505 setTargetColumn();
2506 setAnchor();
2507 commitCursor();
2508 } else {
2509 clearCurrentMode();
2510 }
2511 fixExternalCursor(true);
2512 updateHighlights();
2513
2514 leaveFakeVim(false);
2515 }
2516
unfocus()2517 void FakeVimHandler::Private::unfocus()
2518 {
2519 fixExternalCursor(false);
2520 }
2521
fixExternalCursor(bool focus)2522 void FakeVimHandler::Private::fixExternalCursor(bool focus)
2523 {
2524 m_fixCursorTimer.stop();
2525
2526 if (isVisualCharMode() && !focus && !hasThinCursor()) {
2527 // Select the character under thick cursor for external operations with text selection.
2528 fixExternalCursorPosition(false);
2529 } else if (isVisualCharMode() && focus && hasThinCursor()) {
2530 // Fix cursor position if changing its shape.
2531 // The fix is postponed so context menu action can be finished.
2532 m_fixCursorTimer.start();
2533 } else {
2534 updateCursorShape();
2535 }
2536 }
2537
fixExternalCursorPosition(bool focus)2538 void FakeVimHandler::Private::fixExternalCursorPosition(bool focus)
2539 {
2540 QTextCursor tc = editorCursor();
2541 if (tc.anchor() < tc.position()) {
2542 tc.movePosition(focus ? Left : Right, KeepAnchor);
2543 EDITOR(setTextCursor(tc));
2544 }
2545
2546 setThinCursor(!focus);
2547 }
2548
enterFakeVim()2549 void FakeVimHandler::Private::enterFakeVim()
2550 {
2551 if (m_inFakeVim) {
2552 qWarning("enterFakeVim() shouldn't be called recursively!");
2553 return;
2554 }
2555
2556 if (!m_buffer->currentHandler)
2557 m_buffer->currentHandler = this;
2558
2559 pullOrCreateBufferData();
2560
2561 m_inFakeVim = true;
2562
2563 removeEventFilter();
2564
2565 pullCursor();
2566
2567 updateFirstVisibleLine();
2568 }
2569
leaveFakeVim(bool needUpdate)2570 void FakeVimHandler::Private::leaveFakeVim(bool needUpdate)
2571 {
2572 if (!m_inFakeVim) {
2573 qWarning("enterFakeVim() not called before leaveFakeVim()!");
2574 return;
2575 }
2576
2577 // The command might have destroyed the editor.
2578 if (m_textedit || m_plaintextedit) {
2579 if (s.showMarks.value())
2580 updateSelection();
2581
2582 updateMiniBuffer();
2583
2584 if (needUpdate) {
2585 // Move cursor line to middle of screen if it's not visible.
2586 const int line = cursorLine();
2587 if (line < firstVisibleLine() || line > firstVisibleLine() + linesOnScreen())
2588 scrollToLine(qMax(0, line - linesOnScreen() / 2));
2589 else
2590 scrollToLine(firstVisibleLine());
2591 updateScrollOffset();
2592
2593 commitCursor();
2594 }
2595
2596 installEventFilter();
2597 }
2598
2599 m_inFakeVim = false;
2600 }
2601
leaveFakeVim(EventResult eventResult)2602 void FakeVimHandler::Private::leaveFakeVim(EventResult eventResult)
2603 {
2604 leaveFakeVim(eventResult == EventHandled || eventResult == EventCancelled);
2605 }
2606
wantsOverride(QKeyEvent * ev)2607 bool FakeVimHandler::Private::wantsOverride(QKeyEvent *ev)
2608 {
2609 const int key = ev->key();
2610 const Qt::KeyboardModifiers mods = ev->modifiers();
2611 KEY_DEBUG("SHORTCUT OVERRIDE" << key << " PASSING: " << g.passing);
2612
2613 if (key == Key_Escape) {
2614 if (g.subsubmode == SearchSubSubMode)
2615 return true;
2616 // Not sure this feels good. People often hit Esc several times.
2617 if (isNoVisualMode()
2618 && g.mode == CommandMode
2619 && g.submode == NoSubMode
2620 && g.currentCommand.isEmpty()
2621 && g.returnToMode == CommandMode)
2622 return false;
2623 return true;
2624 }
2625
2626 // We are interested in overriding most Ctrl key combinations.
2627 if (isOnlyControlModifier(mods)
2628 && !s.passControlKey.value()
2629 && ((key >= Key_A && key <= Key_Z && key != Key_K)
2630 || key == Key_BracketLeft || key == Key_BracketRight)) {
2631 // Ctrl-K is special as it is the Core's default notion of Locator
2632 if (g.passing) {
2633 KEY_DEBUG(" PASSING CTRL KEY");
2634 // We get called twice on the same key
2635 //g.passing = false;
2636 return false;
2637 }
2638 KEY_DEBUG(" NOT PASSING CTRL KEY");
2639 return true;
2640 }
2641
2642 // Let other shortcuts trigger.
2643 return false;
2644 }
2645
handleEvent(QKeyEvent * ev)2646 EventResult FakeVimHandler::Private::handleEvent(QKeyEvent *ev)
2647 {
2648 const int key = ev->key();
2649 const Qt::KeyboardModifiers mods = ev->modifiers();
2650
2651 if (key == Key_Shift || key == Key_Alt || key == Key_Control
2652 || key == Key_AltGr || key == Key_Meta)
2653 {
2654 KEY_DEBUG("PLAIN MODIFIER");
2655 return EventUnhandled;
2656 }
2657
2658 if (g.passing) {
2659 passShortcuts(false);
2660 KEY_DEBUG("PASSING PLAIN KEY..." << ev->key() << ev->text());
2661 //if (input.is(',')) { // use ',,' to leave, too.
2662 // qDebug() << "FINISHED...";
2663 // return EventHandled;
2664 //}
2665 KEY_DEBUG(" PASS TO CORE");
2666 return EventPassedToCore;
2667 }
2668
2669 #ifndef FAKEVIM_STANDALONE
2670 bool inSnippetMode = false;
2671 QMetaObject::invokeMethod(editor(),
2672 "inSnippetMode", Q_ARG(bool *, &inSnippetMode));
2673
2674 if (inSnippetMode)
2675 return EventPassedToCore;
2676 #endif
2677
2678 // Fake "End of line"
2679 //m_tc = m_cursor;
2680
2681 //bool hasBlock = false;
2682 //q->requestHasBlockSelection(&hasBlock);
2683 //qDebug() << "IMPORT BLOCK 2:" << hasBlock;
2684
2685 //if (0 && hasBlock) {
2686 // (pos > anc) ? --pos : --anc;
2687
2688 //if ((mods & RealControlModifier) != 0) {
2689 // if (key >= Key_A && key <= Key_Z)
2690 // key = shift(key); // make it lower case
2691 // key = control(key);
2692 //} else if (key >= Key_A && key <= Key_Z && (mods & Qt::ShiftModifier) == 0) {
2693 // key = shift(key);
2694 //}
2695
2696 //QTC_ASSERT(g.mode == InsertMode || g.mode == ReplaceMode
2697 // || !atBlockEnd() || block().length() <= 1,
2698 // qDebug() << "Cursor at EOL before key handler");
2699
2700 const Input input(key, mods, ev->text());
2701 if (!input.isValid())
2702 return EventUnhandled;
2703
2704 enterFakeVim();
2705 EventResult result = handleKey(input);
2706 leaveFakeVim(result);
2707 return result;
2708 }
2709
installEventFilter()2710 void FakeVimHandler::Private::installEventFilter()
2711 {
2712 EDITOR(installEventFilter(q));
2713 }
2714
removeEventFilter()2715 void FakeVimHandler::Private::removeEventFilter()
2716 {
2717 EDITOR(removeEventFilter(q));
2718 }
2719
setupWidget()2720 void FakeVimHandler::Private::setupWidget()
2721 {
2722 m_cursorNeedsUpdate = true;
2723 if (m_textedit) {
2724 connect(m_textedit, &QTextEdit::cursorPositionChanged,
2725 this, &FakeVimHandler::Private::onCursorPositionChanged, Qt::UniqueConnection);
2726 } else {
2727 connect(m_plaintextedit, &QPlainTextEdit::cursorPositionChanged,
2728 this, &FakeVimHandler::Private::onCursorPositionChanged, Qt::UniqueConnection);
2729 }
2730
2731 enterFakeVim();
2732
2733 leaveCurrentMode();
2734 m_wasReadOnly = EDITOR(isReadOnly());
2735
2736 updateEditor();
2737
2738 leaveFakeVim();
2739 }
2740
commitInsertState()2741 void FakeVimHandler::Private::commitInsertState()
2742 {
2743 if (!isInsertStateValid())
2744 return;
2745
2746 QString &lastInsertion = m_buffer->lastInsertion;
2747 BufferData::InsertState &insertState = m_buffer->insertState;
2748
2749 // Get raw inserted text.
2750 lastInsertion = textAt(insertState.pos1, insertState.pos2);
2751
2752 // Escape special characters and spaces inserted by user (not by auto-indentation).
2753 for (int i = lastInsertion.size() - 1; i >= 0; --i) {
2754 const int pos = insertState.pos1 + i;
2755 const QChar c = characterAt(pos);
2756 if (c == '<')
2757 lastInsertion.replace(i, 1, "<LT>");
2758 else if ((c == ' ' || c == '\t') && insertState.spaces.contains(pos))
2759 lastInsertion.replace(i, 1, QLatin1String(c == ' ' ? "<SPACE>" : "<TAB>"));
2760 }
2761
2762 // Remove unnecessary backspaces.
2763 while (insertState.backspaces > 0 && !lastInsertion.isEmpty() && lastInsertion[0].isSpace())
2764 --insertState.backspaces;
2765
2766 // backspaces in front of inserted text
2767 lastInsertion.prepend(QString("<BS>").repeated(insertState.backspaces));
2768 // deletes after inserted text
2769 lastInsertion.prepend(QString("<DELETE>").repeated(insertState.deletes));
2770
2771 // Remove indentation.
2772 lastInsertion.replace(QRegularExpression("(^|\n)[\\t ]+"), "\\1");
2773 }
2774
invalidateInsertState()2775 void FakeVimHandler::Private::invalidateInsertState()
2776 {
2777 BufferData::InsertState &insertState = m_buffer->insertState;
2778 insertState.pos1 = -1;
2779 insertState.pos2 = position();
2780 insertState.backspaces = 0;
2781 insertState.deletes = 0;
2782 insertState.spaces.clear();
2783 insertState.insertingSpaces = false;
2784 insertState.textBeforeCursor = textAt(block().position(), position());
2785 insertState.newLineBefore = false;
2786 insertState.newLineAfter = false;
2787 }
2788
isInsertStateValid() const2789 bool FakeVimHandler::Private::isInsertStateValid() const
2790 {
2791 return m_buffer->insertState.pos1 != -1;
2792 }
2793
clearLastInsertion()2794 void FakeVimHandler::Private::clearLastInsertion()
2795 {
2796 invalidateInsertState();
2797 m_buffer->lastInsertion.clear();
2798 m_buffer->insertState.pos1 = m_buffer->insertState.pos2;
2799 }
2800
ensureCursorVisible()2801 void FakeVimHandler::Private::ensureCursorVisible()
2802 {
2803 int pos = position();
2804 int anc = isVisualMode() ? anchor() : position();
2805
2806 // fix selection so it is outside folded block
2807 int start = qMin(pos, anc);
2808 int end = qMax(pos, anc) + 1;
2809 QTextBlock block = blockAt(start);
2810 QTextBlock block2 = blockAt(end);
2811 if (!block.isVisible() || !block2.isVisible()) {
2812 // FIXME: Moving cursor left/right or unfolding block immediately after block is folded
2813 // should restore cursor position inside block.
2814 // Changing cursor position after folding is not Vim behavior so at least record the jump.
2815 if (block.isValid() && !block.isVisible())
2816 recordJump();
2817
2818 pos = start;
2819 while (block.isValid() && !block.isVisible())
2820 block = block.previous();
2821 if (block.isValid())
2822 pos = block.position() + qMin(m_targetColumn, block.length() - 2);
2823
2824 if (isVisualMode()) {
2825 anc = end;
2826 while (block2.isValid() && !block2.isVisible()) {
2827 anc = block2.position() + block2.length() - 2;
2828 block2 = block2.next();
2829 }
2830 }
2831
2832 setAnchorAndPosition(anc, pos);
2833 }
2834 }
2835
updateEditor()2836 void FakeVimHandler::Private::updateEditor()
2837 {
2838 setTabSize(s.tabStop.value());
2839 setupCharClass();
2840 }
2841
setTabSize(int tabSize)2842 void FakeVimHandler::Private::setTabSize(int tabSize)
2843 {
2844 #if QT_VERSION >= QT_VERSION_CHECK(5,11,0)
2845 const int charWidth = QFontMetrics(EDITOR(font())).horizontalAdvance(' ');
2846 #else
2847 const int charWidth = QFontMetrics(EDITOR(font())).width(' ');
2848 #endif
2849 const int width = charWidth * tabSize;
2850 #if QT_VERSION >= QT_VERSION_CHECK(5,10,0)
2851 EDITOR(setTabStopDistance(width));
2852 #else
2853 EDITOR(setTabStopWidth(width));
2854 #endif
2855 }
2856
restoreWidget(int tabSize)2857 void FakeVimHandler::Private::restoreWidget(int tabSize)
2858 {
2859 //EDITOR(removeEventFilter(q));
2860 //EDITOR(setReadOnly(m_wasReadOnly));
2861 setTabSize(tabSize);
2862 g.visualMode = NoVisualMode;
2863 // Force "ordinary" cursor.
2864 setThinCursor();
2865 updateSelection();
2866 updateHighlights();
2867 if (m_textedit) {
2868 disconnect(m_textedit, &QTextEdit::cursorPositionChanged,
2869 this, &FakeVimHandler::Private::onCursorPositionChanged);
2870 } else {
2871 disconnect(m_plaintextedit, &QPlainTextEdit::cursorPositionChanged,
2872 this, &FakeVimHandler::Private::onCursorPositionChanged);
2873 }
2874 }
2875
handleKey(const Input & input)2876 EventResult FakeVimHandler::Private::handleKey(const Input &input)
2877 {
2878 KEY_DEBUG("HANDLE INPUT: " << input);
2879
2880 bool hasInput = input.isValid();
2881
2882 // Waiting on input to complete mapping?
2883 EventResult r = stopWaitForMapping(hasInput);
2884
2885 if (hasInput) {
2886 record(input);
2887 g.pendingInput.append(input);
2888 }
2889
2890 // Process pending input.
2891 // Note: Pending input is global state and can be extended by:
2892 // 1. handling a user input (though handleKey() is not called recursively),
2893 // 2. expanding a user mapping or
2894 // 3. executing a register.
2895 while (!g.pendingInput.isEmpty() && r == EventHandled) {
2896 const Input in = g.pendingInput.takeFirst();
2897
2898 // invalid input is used to pop mapping state
2899 if (!in.isValid()) {
2900 endMapping();
2901 } else {
2902 // Handle user mapping.
2903 if (canHandleMapping()) {
2904 if (extendMapping(in)) {
2905 if (!hasInput || !g.currentMap.canExtend())
2906 expandCompleteMapping();
2907 } else if (!expandCompleteMapping()) {
2908 r = handleCurrentMapAsDefault();
2909 }
2910 } else {
2911 r = handleDefaultKey(in);
2912 }
2913 }
2914 }
2915
2916 if (g.currentMap.canExtend()) {
2917 waitForMapping();
2918 return EventHandled;
2919 }
2920
2921 if (r != EventHandled)
2922 clearPendingInput();
2923
2924 return r;
2925 }
2926
handleCommandBufferPaste(const Input & input)2927 bool FakeVimHandler::Private::handleCommandBufferPaste(const Input &input)
2928 {
2929 if (input.isControl('r')
2930 && (g.subsubmode == SearchSubSubMode || g.mode == ExMode)) {
2931 g.minibufferData = input;
2932 return true;
2933 }
2934 if (g.minibufferData.isControl('r')) {
2935 g.minibufferData = Input();
2936 if (input.isEscape())
2937 return true;
2938 CommandBuffer &buffer = (g.subsubmode == SearchSubSubMode)
2939 ? g.searchBuffer : g.commandBuffer;
2940 if (input.isControl('w')) {
2941 QTextCursor tc = m_cursor;
2942 tc.select(QTextCursor::WordUnderCursor);
2943 QString word = tc.selectedText();
2944 buffer.insertText(word);
2945 } else {
2946 QString r = registerContents(input.asChar().unicode());
2947 buffer.insertText(r);
2948 }
2949 updateMiniBuffer();
2950 return true;
2951 }
2952 return false;
2953 }
2954
handleDefaultKey(const Input & input)2955 EventResult FakeVimHandler::Private::handleDefaultKey(const Input &input)
2956 {
2957 if (g.passing) {
2958 passShortcuts(false);
2959 QKeyEvent event(QEvent::KeyPress, input.key(), input.modifiers(), input.text());
2960 bool accepted = QApplication::sendEvent(editor()->window(), &event);
2961 if (accepted || (!m_textedit && !m_plaintextedit))
2962 return EventHandled;
2963 }
2964
2965 if (input == Nop)
2966 return EventHandled;
2967 else if (g.subsubmode == SearchSubSubMode)
2968 return handleSearchSubSubMode(input);
2969 else if (g.mode == CommandMode)
2970 return handleCommandMode(input);
2971 else if (g.mode == InsertMode || g.mode == ReplaceMode)
2972 return handleInsertOrReplaceMode(input);
2973 else if (g.mode == ExMode)
2974 return handleExMode(input);
2975 return EventUnhandled;
2976 }
2977
handleCurrentMapAsDefault()2978 EventResult FakeVimHandler::Private::handleCurrentMapAsDefault()
2979 {
2980 // If mapping has failed take the first input from it and try default command.
2981 const Inputs &inputs = g.currentMap.currentInputs();
2982 if (inputs.isEmpty())
2983 return EventHandled;
2984
2985 Input in = inputs.front();
2986 if (inputs.size() > 1)
2987 prependInputs(inputs.mid(1));
2988 g.currentMap.reset();
2989
2990 return handleDefaultKey(in);
2991 }
2992
prependInputs(const QVector<Input> & inputs)2993 void FakeVimHandler::Private::prependInputs(const QVector<Input> &inputs)
2994 {
2995 for (int i = inputs.size() - 1; i >= 0; --i)
2996 g.pendingInput.prepend(inputs[i]);
2997 }
2998
prependMapping(const Inputs & inputs)2999 void FakeVimHandler::Private::prependMapping(const Inputs &inputs)
3000 {
3001 // FIXME: Implement Vim option maxmapdepth (default value is 1000).
3002 if (g.mapDepth >= 1000) {
3003 const int i = qMax(0, g.pendingInput.lastIndexOf(Input()));
3004 const QList<Input> inputs = g.pendingInput.mid(i);
3005 clearPendingInput();
3006 g.pendingInput.append(inputs);
3007 showMessage(MessageError, Tr::tr("Recursive mapping"));
3008 return;
3009 }
3010
3011 ++g.mapDepth;
3012 g.pendingInput.prepend(Input());
3013 prependInputs(inputs);
3014 g.commandBuffer.setHistoryAutoSave(false);
3015
3016 // start new edit block (undo/redo) only if necessary
3017 bool editBlock = m_buffer->editBlockLevel == 0 && !(isInsertMode() && isInsertStateValid());
3018 if (editBlock)
3019 beginLargeEditBlock();
3020 g.mapStates << MappingState(inputs.noremap(), inputs.silent(), editBlock);
3021 }
3022
expandCompleteMapping()3023 bool FakeVimHandler::Private::expandCompleteMapping()
3024 {
3025 if (!g.currentMap.isComplete())
3026 return false;
3027
3028 const Inputs &inputs = g.currentMap.inputs();
3029 int usedInputs = g.currentMap.mapLength();
3030 prependInputs(g.currentMap.currentInputs().mid(usedInputs));
3031 prependMapping(inputs);
3032 g.currentMap.reset();
3033
3034 return true;
3035 }
3036
extendMapping(const Input & input)3037 bool FakeVimHandler::Private::extendMapping(const Input &input)
3038 {
3039 if (!g.currentMap.isValid())
3040 g.currentMap.reset(currentModeCode());
3041 return g.currentMap.walk(input);
3042 }
3043
endMapping()3044 void FakeVimHandler::Private::endMapping()
3045 {
3046 if (!g.currentMap.canExtend())
3047 --g.mapDepth;
3048 if (g.mapStates.isEmpty())
3049 return;
3050 if (g.mapStates.last().editBlock)
3051 endEditBlock();
3052 g.mapStates.pop_back();
3053 if (g.mapStates.isEmpty())
3054 g.commandBuffer.setHistoryAutoSave(true);
3055 }
3056
canHandleMapping()3057 bool FakeVimHandler::Private::canHandleMapping()
3058 {
3059 // Don't handle user mapping in sub-modes that cannot be followed by movement and in "noremap".
3060 return g.subsubmode == NoSubSubMode
3061 && g.submode != RegisterSubMode
3062 && g.submode != WindowSubMode
3063 && g.submode != ZSubMode
3064 && g.submode != CapitalZSubMode
3065 && g.submode != ReplaceSubMode
3066 && g.submode != MacroRecordSubMode
3067 && g.submode != MacroExecuteSubMode
3068 && (g.mapStates.isEmpty() || !g.mapStates.last().noremap);
3069 }
3070
clearPendingInput()3071 void FakeVimHandler::Private::clearPendingInput()
3072 {
3073 // Clear pending input on interrupt or bad mapping.
3074 g.pendingInput.clear();
3075 g.mapStates.clear();
3076 g.mapDepth = 0;
3077
3078 // Clear all started edit blocks.
3079 while (m_buffer->editBlockLevel > 0)
3080 endEditBlock();
3081 }
3082
waitForMapping()3083 void FakeVimHandler::Private::waitForMapping()
3084 {
3085 g.currentCommand.clear();
3086 foreach (const Input &input, g.currentMap.currentInputs())
3087 g.currentCommand.append(input.toString());
3088
3089 // wait for user to press any key or trigger complete mapping after interval
3090 m_inputTimer.start();
3091 }
3092
stopWaitForMapping(bool hasInput)3093 EventResult FakeVimHandler::Private::stopWaitForMapping(bool hasInput)
3094 {
3095 if (!hasInput || m_inputTimer.isActive()) {
3096 m_inputTimer.stop();
3097 g.currentCommand.clear();
3098 if (!hasInput && !expandCompleteMapping()) {
3099 // Cannot complete mapping so handle the first input from it as default command.
3100 return handleCurrentMapAsDefault();
3101 }
3102 }
3103
3104 return EventHandled;
3105 }
3106
stopIncrementalFind()3107 void FakeVimHandler::Private::stopIncrementalFind()
3108 {
3109 if (g.findPending) {
3110 g.findPending = false;
3111 setAnchorAndPosition(m_findStartPosition, m_cursor.selectionStart());
3112 finishMovement();
3113 setAnchor();
3114 }
3115 }
3116
updateFind(bool isComplete)3117 void FakeVimHandler::Private::updateFind(bool isComplete)
3118 {
3119 if (!isComplete && !s.incSearch.value())
3120 return;
3121
3122 g.currentMessage.clear();
3123
3124 const QString &needle = g.searchBuffer.contents();
3125 if (isComplete) {
3126 setPosition(m_searchStartPosition);
3127 if (!needle.isEmpty())
3128 recordJump();
3129 }
3130
3131 SearchData sd;
3132 sd.needle = needle;
3133 sd.forward = g.lastSearchForward;
3134 sd.highlightMatches = isComplete;
3135 search(sd, isComplete);
3136 }
3137
resetCount()3138 void FakeVimHandler::Private::resetCount()
3139 {
3140 g.mvcount = 0;
3141 g.opcount = 0;
3142 }
3143
isInputCount(const Input & input) const3144 bool FakeVimHandler::Private::isInputCount(const Input &input) const
3145 {
3146 return input.isDigit() && (!input.is('0') || g.mvcount > 0);
3147 }
3148
atEmptyLine(int pos) const3149 bool FakeVimHandler::Private::atEmptyLine(int pos) const
3150 {
3151 return blockAt(pos).length() == 1;
3152 }
3153
atEmptyLine(const QTextCursor & tc) const3154 bool FakeVimHandler::Private::atEmptyLine(const QTextCursor &tc) const
3155 {
3156 return atEmptyLine(tc.position());
3157 }
3158
atEmptyLine() const3159 bool FakeVimHandler::Private::atEmptyLine() const
3160 {
3161 return atEmptyLine(position());
3162 }
3163
atBoundary(bool end,bool simple,bool onlyWords,const QTextCursor & tc) const3164 bool FakeVimHandler::Private::atBoundary(bool end, bool simple, bool onlyWords,
3165 const QTextCursor &tc) const
3166 {
3167 if (tc.isNull())
3168 return atBoundary(end, simple, onlyWords, m_cursor);
3169 if (atEmptyLine(tc))
3170 return true;
3171 int pos = tc.position();
3172 QChar c1 = characterAt(pos);
3173 QChar c2 = characterAt(pos + (end ? 1 : -1));
3174 int thisClass = charClass(c1, simple);
3175 return (!onlyWords || thisClass != 0)
3176 && (c2.isNull() || c2 == ParagraphSeparator || thisClass != charClass(c2, simple));
3177 }
3178
atWordBoundary(bool end,bool simple,const QTextCursor & tc) const3179 bool FakeVimHandler::Private::atWordBoundary(bool end, bool simple, const QTextCursor &tc) const
3180 {
3181 return atBoundary(end, simple, true, tc);
3182 }
3183
atWordStart(bool simple,const QTextCursor & tc) const3184 bool FakeVimHandler::Private::atWordStart(bool simple, const QTextCursor &tc) const
3185 {
3186 return atWordBoundary(false, simple, tc);
3187 }
3188
atWordEnd(bool simple,const QTextCursor & tc) const3189 bool FakeVimHandler::Private::atWordEnd(bool simple, const QTextCursor &tc) const
3190 {
3191 return atWordBoundary(true, simple, tc);
3192 }
3193
isFirstNonBlankOnLine(int pos)3194 bool FakeVimHandler::Private::isFirstNonBlankOnLine(int pos)
3195 {
3196 for (int i = blockAt(pos).position(); i < pos; ++i) {
3197 if (!document()->characterAt(i).isSpace())
3198 return false;
3199 }
3200 return true;
3201 }
3202
pushUndoState(bool overwrite)3203 void FakeVimHandler::Private::pushUndoState(bool overwrite)
3204 {
3205 if (m_buffer->editBlockLevel != 0 && m_buffer->undoState.isValid())
3206 return; // No need to save undo state for inner edit blocks.
3207
3208 if (m_buffer->undoState.isValid() && !overwrite)
3209 return;
3210
3211 UNDO_DEBUG("PUSH UNDO");
3212 int pos = position();
3213 if (!isInsertMode()) {
3214 if (isVisualMode() || g.submode == DeleteSubMode
3215 || (g.submode == ChangeSubMode && g.movetype != MoveLineWise)) {
3216 pos = qMin(pos, anchor());
3217 if (isVisualLineMode())
3218 pos = firstPositionInLine(lineForPosition(pos));
3219 else if (isVisualBlockMode())
3220 pos = blockAt(pos).position() + qMin(columnAt(anchor()), columnAt(position()));
3221 } else if (g.movetype == MoveLineWise && s.startOfLine.value()) {
3222 QTextCursor tc = m_cursor;
3223 if (g.submode == ShiftLeftSubMode || g.submode == ShiftRightSubMode
3224 || g.submode == IndentSubMode) {
3225 pos = qMin(pos, anchor());
3226 }
3227 tc.setPosition(pos);
3228 moveToFirstNonBlankOnLine(&tc);
3229 pos = qMin(pos, tc.position());
3230 }
3231 }
3232
3233 CursorPosition lastChangePosition(document(), pos);
3234 setMark('.', lastChangePosition);
3235
3236 m_buffer->redo.clear();
3237 m_buffer->undoState = State(
3238 revision(), lastChangePosition, m_buffer->marks,
3239 m_buffer->lastVisualMode, m_buffer->lastVisualModeInverted);
3240 }
3241
moveDown(int n)3242 void FakeVimHandler::Private::moveDown(int n)
3243 {
3244 if (n == 0)
3245 return;
3246
3247 QTextBlock block = m_cursor.block();
3248 const int col = position() - block.position();
3249
3250 int lines = qAbs(n);
3251 int position = 0;
3252 while (block.isValid()) {
3253 position = block.position() + qMax(0, qMin(block.length() - 2, col));
3254 if (block.isVisible()) {
3255 --lines;
3256 if (lines < 0)
3257 break;
3258 }
3259 block = n > 0 ? nextLine(block) : previousLine(block);
3260 }
3261
3262 setPosition(position);
3263 moveToTargetColumn();
3264 updateScrollOffset();
3265 }
3266
moveDownVisually(int n)3267 void FakeVimHandler::Private::moveDownVisually(int n)
3268 {
3269 const QTextCursor::MoveOperation moveOperation = (n > 0) ? Down : Up;
3270 int count = qAbs(n);
3271 int oldPos = m_cursor.position();
3272
3273 while (count > 0) {
3274 m_cursor.movePosition(moveOperation, KeepAnchor, 1);
3275 if (oldPos == m_cursor.position())
3276 break;
3277 oldPos = m_cursor.position();
3278 QTextBlock block = m_cursor.block();
3279 if (block.isVisible())
3280 --count;
3281 }
3282
3283 QTextCursor tc = m_cursor;
3284 tc.movePosition(StartOfLine);
3285 const int minPos = tc.position();
3286 moveToEndOfLineVisually(&tc);
3287 const int maxPos = tc.position();
3288
3289 if (m_targetColumn == -1) {
3290 setPosition(maxPos);
3291 } else {
3292 setPosition(qMin(maxPos, minPos + m_targetColumnWrapped));
3293 const int targetColumn = m_targetColumnWrapped;
3294 setTargetColumn();
3295 m_targetColumnWrapped = targetColumn;
3296 }
3297
3298 if (!isInsertMode() && atEndOfLine())
3299 m_cursor.movePosition(Left, KeepAnchor);
3300
3301 updateScrollOffset();
3302 }
3303
movePageDown(int count)3304 void FakeVimHandler::Private::movePageDown(int count)
3305 {
3306 const int scrollOffset = windowScrollOffset();
3307 const int screenLines = linesOnScreen();
3308 const int offset = count > 0 ? scrollOffset - 2 : screenLines - scrollOffset + 2;
3309 const int value = count * screenLines - cursorLineOnScreen() + offset;
3310 moveDown(value);
3311
3312 if (count > 0)
3313 scrollToLine(cursorLine());
3314 else
3315 scrollToLine(qMax(0, cursorLine() - screenLines + 1));
3316 }
3317
commitCursor()3318 void FakeVimHandler::Private::commitCursor()
3319 {
3320 QTextCursor tc = m_cursor;
3321
3322 if (isVisualMode()) {
3323 int pos = tc.position();
3324 int anc = tc.anchor();
3325
3326 if (isVisualBlockMode()) {
3327 const int col1 = columnAt(anc);
3328 const int col2 = columnAt(pos);
3329 if (col1 > col2)
3330 ++anc;
3331 else if (!tc.atBlockEnd())
3332 ++pos;
3333 // FIXME: After '$' command (i.e. m_visualTargetColumn == -1), end of selected lines
3334 // should be selected.
3335 } else if (isVisualLineMode()) {
3336 const int posLine = lineForPosition(pos);
3337 const int ancLine = lineForPosition(anc);
3338 if (anc < pos) {
3339 pos = lastPositionInLine(posLine);
3340 anc = firstPositionInLine(ancLine);
3341 } else {
3342 pos = firstPositionInLine(posLine);
3343 anc = lastPositionInLine(ancLine) + 1;
3344 }
3345 // putting cursor on folded line will unfold the line, so move the cursor a bit
3346 if (!blockAt(pos).isVisible())
3347 ++pos;
3348 } else if (isVisualCharMode()) {
3349 if (anc > pos)
3350 ++anc;
3351 else if (!editor()->hasFocus() || isCommandLineMode())
3352 m_fixCursorTimer.start();
3353 }
3354
3355 tc.setPosition(anc);
3356 tc.setPosition(pos, KeepAnchor);
3357 } else if (g.subsubmode == SearchSubSubMode && !m_searchCursor.isNull()) {
3358 tc = m_searchCursor;
3359 } else {
3360 tc.clearSelection();
3361 }
3362
3363 updateCursorShape();
3364
3365 if (isVisualBlockMode()) {
3366 q->requestSetBlockSelection(tc);
3367 } else {
3368 q->requestDisableBlockSelection();
3369 if (editor())
3370 EDITOR(setTextCursor(tc));
3371 }
3372 }
3373
pullCursor()3374 void FakeVimHandler::Private::pullCursor()
3375 {
3376 if (!m_cursorNeedsUpdate)
3377 return;
3378
3379 m_cursorNeedsUpdate = false;
3380
3381 QTextCursor oldCursor = m_cursor;
3382
3383 bool visualBlockMode = false;
3384 q->requestHasBlockSelection(&visualBlockMode);
3385
3386 if (visualBlockMode)
3387 q->requestBlockSelection(&m_cursor);
3388 else if (editor())
3389 m_cursor = editorCursor();
3390
3391 // Cursor should be always valid.
3392 if (m_cursor.isNull())
3393 m_cursor = QTextCursor(document());
3394
3395 if (visualBlockMode)
3396 g.visualMode = VisualBlockMode;
3397 else if (m_cursor.hasSelection())
3398 g.visualMode = VisualCharMode;
3399 else
3400 g.visualMode = NoVisualMode;
3401
3402 // Keep visually the text selection same.
3403 // With thick text cursor, the character under cursor is treated as selected.
3404 if (isVisualCharMode() && hasThinCursor())
3405 moveLeft();
3406
3407 // Cursor position can be after the end of line only in some modes.
3408 if (atEndOfLine() && !isVisualMode() && !isInsertMode())
3409 moveLeft();
3410
3411 // Record external jump to different line.
3412 if (lineForPosition(position()) != lineForPosition(oldCursor.position()))
3413 recordJump(oldCursor.position());
3414
3415 setTargetColumn();
3416 }
3417
editorCursor() const3418 QTextCursor FakeVimHandler::Private::editorCursor() const
3419 {
3420 QTextCursor tc = EDITOR(textCursor());
3421 tc.setVisualNavigation(false);
3422 return tc;
3423 }
3424
moveToNextParagraph(int count)3425 bool FakeVimHandler::Private::moveToNextParagraph(int count)
3426 {
3427 const bool forward = count > 0;
3428 int repeat = forward ? count : -count;
3429 QTextBlock block = this->block();
3430
3431 if (block.isValid() && block.length() == 1)
3432 ++repeat;
3433
3434 for (; block.isValid(); block = forward ? block.next() : block.previous()) {
3435 if (block.length() == 1) {
3436 if (--repeat == 0)
3437 break;
3438 while (block.isValid() && block.length() == 1)
3439 block = forward ? block.next() : block.previous();
3440 if (!block.isValid())
3441 break;
3442 }
3443 }
3444
3445 if (!block.isValid())
3446 --repeat;
3447
3448 if (repeat > 0)
3449 return false;
3450
3451 if (block.isValid())
3452 setPosition(block.position());
3453 else
3454 setPosition(forward ? lastPositionInDocument() : 0);
3455
3456 return true;
3457 }
3458
moveToParagraphStartOrEnd(int direction)3459 void FakeVimHandler::Private::moveToParagraphStartOrEnd(int direction)
3460 {
3461 bool emptyLine = atEmptyLine();
3462 int oldPos = -1;
3463
3464 while (atEmptyLine() == emptyLine && oldPos != position()) {
3465 oldPos = position();
3466 moveDown(direction);
3467 }
3468
3469 if (oldPos != position())
3470 moveUp(direction);
3471 }
3472
moveToEndOfLine()3473 void FakeVimHandler::Private::moveToEndOfLine()
3474 {
3475 // Additionally select (in visual mode) or apply current command on hidden lines following
3476 // the current line.
3477 bool onlyVisibleLines = isVisualMode() || g.submode != NoSubMode;
3478 const int id = onlyVisibleLines ? lineNumber(block()) : block().blockNumber() + 1;
3479 setPosition(lastPositionInLine(id, onlyVisibleLines));
3480 setTargetColumn();
3481 }
3482
moveToEndOfLineVisually()3483 void FakeVimHandler::Private::moveToEndOfLineVisually()
3484 {
3485 moveToEndOfLineVisually(&m_cursor);
3486 setTargetColumn();
3487 }
3488
moveToEndOfLineVisually(QTextCursor * tc)3489 void FakeVimHandler::Private::moveToEndOfLineVisually(QTextCursor *tc)
3490 {
3491 // Moving to end of line ends up on following line if the line is wrapped.
3492 tc->movePosition(StartOfLine);
3493 const int minPos = tc->position();
3494 tc->movePosition(EndOfLine);
3495 int maxPos = tc->position();
3496 tc->movePosition(StartOfLine);
3497 if (minPos != tc->position())
3498 --maxPos;
3499 tc->setPosition(maxPos);
3500 }
3501
moveBehindEndOfLine()3502 void FakeVimHandler::Private::moveBehindEndOfLine()
3503 {
3504 q->fold(1, false);
3505 int pos = qMin(block().position() + block().length() - 1,
3506 lastPositionInDocument() + 1);
3507 setPosition(pos);
3508 setTargetColumn();
3509 }
3510
moveToStartOfLine()3511 void FakeVimHandler::Private::moveToStartOfLine()
3512 {
3513 setPosition(block().position());
3514 setTargetColumn();
3515 }
3516
moveToStartOfLineVisually()3517 void FakeVimHandler::Private::moveToStartOfLineVisually()
3518 {
3519 m_cursor.movePosition(StartOfLine, KeepAnchor);
3520 setTargetColumn();
3521 }
3522
fixSelection()3523 void FakeVimHandler::Private::fixSelection()
3524 {
3525 if (g.rangemode == RangeBlockMode)
3526 return;
3527
3528 if (g.movetype == MoveInclusive) {
3529 // If position or anchor is after end of non-empty line, include line break in selection.
3530 if (characterAtCursor() == ParagraphSeparator) {
3531 if (!atEmptyLine() && !atDocumentEnd()) {
3532 setPosition(position() + 1);
3533 return;
3534 }
3535 } else if (characterAt(anchor()) == ParagraphSeparator) {
3536 QTextCursor tc = m_cursor;
3537 tc.setPosition(anchor());
3538 if (!atEmptyLine(tc)) {
3539 setAnchorAndPosition(anchor() + 1, position());
3540 return;
3541 }
3542 }
3543 }
3544
3545 if (g.movetype == MoveExclusive && g.subsubmode == NoSubSubMode) {
3546 if (anchor() < position() && atBlockStart()) {
3547 // Exclusive motion ending at the beginning of line
3548 // becomes inclusive and end is moved to end of previous line.
3549 g.movetype = MoveInclusive;
3550 moveToStartOfLine();
3551 moveLeft();
3552
3553 // Exclusive motion ending at the beginning of line and
3554 // starting at or before first non-blank on a line becomes linewise.
3555 if (anchor() < block().position() && isFirstNonBlankOnLine(anchor()))
3556 g.movetype = MoveLineWise;
3557 }
3558 }
3559
3560 if (g.movetype == MoveLineWise)
3561 g.rangemode = (g.submode == ChangeSubMode)
3562 ? RangeLineModeExclusive
3563 : RangeLineMode;
3564
3565 if (g.movetype == MoveInclusive) {
3566 if (anchor() <= position()) {
3567 if (!atBlockEnd())
3568 setPosition(position() + 1); // correction
3569
3570 // Omit first character in selection if it's line break on non-empty line.
3571 int start = anchor();
3572 int end = position();
3573 if (afterEndOfLine(document(), start) && start > 0) {
3574 start = qMin(start + 1, end);
3575 if (g.submode == DeleteSubMode && !atDocumentEnd())
3576 setAnchorAndPosition(start, end + 1);
3577 else
3578 setAnchorAndPosition(start, end);
3579 }
3580
3581 // If more than one line is selected and all are selected completely
3582 // movement becomes linewise.
3583 if (start < block().position() && isFirstNonBlankOnLine(start) && atBlockEnd()) {
3584 if (g.submode != ChangeSubMode) {
3585 moveRight();
3586 if (atEmptyLine())
3587 moveRight();
3588 }
3589 g.movetype = MoveLineWise;
3590 }
3591 } else if (!m_anchorPastEnd) {
3592 setAnchorAndPosition(anchor() + 1, position());
3593 }
3594 }
3595
3596 if (m_positionPastEnd) {
3597 moveBehindEndOfLine();
3598 moveRight();
3599 setAnchorAndPosition(anchor(), position());
3600 }
3601
3602 if (m_anchorPastEnd) {
3603 const int pos = position();
3604 setPosition(anchor());
3605 moveBehindEndOfLine();
3606 moveRight();
3607 setAnchorAndPosition(position(), pos);
3608 }
3609 }
3610
finishSearch()3611 bool FakeVimHandler::Private::finishSearch()
3612 {
3613 if (g.lastSearch.isEmpty()
3614 || (!g.currentMessage.isEmpty() && g.currentMessageLevel == MessageError)) {
3615 return false;
3616 }
3617 if (g.submode != NoSubMode)
3618 setAnchorAndPosition(m_searchStartPosition, position());
3619 return true;
3620 }
3621
finishMovement(const QString & dotCommandMovement)3622 void FakeVimHandler::Private::finishMovement(const QString &dotCommandMovement)
3623 {
3624 //dump("FINISH MOVEMENT");
3625 if (g.submode == FilterSubMode) {
3626 int beginLine = lineForPosition(anchor());
3627 int endLine = lineForPosition(position());
3628 setPosition(qMin(anchor(), position()));
3629 enterExMode(QString(".,+%1!").arg(qAbs(endLine - beginLine)));
3630 return;
3631 }
3632
3633 if (g.submode == ChangeSubMode
3634 || g.submode == DeleteSubMode
3635 || g.submode == CommentSubMode
3636 || g.submode == ExchangeSubMode
3637 || g.submode == ReplaceWithRegisterSubMode
3638 || g.submode == AddSurroundingSubMode
3639 || g.submode == YankSubMode
3640 || g.submode == InvertCaseSubMode
3641 || g.submode == DownCaseSubMode
3642 || g.submode == UpCaseSubMode
3643 || g.submode == IndentSubMode
3644 || g.submode == ShiftLeftSubMode
3645 || g.submode == ShiftRightSubMode)
3646 {
3647 fixSelection();
3648
3649 if (g.submode == ChangeSubMode
3650 || g.submode == DeleteSubMode
3651 || g.submode == YankSubMode)
3652 {
3653 yankText(currentRange(), m_register);
3654 }
3655 }
3656
3657 if (g.submode == ChangeSubMode) {
3658 pushUndoState(false);
3659 beginEditBlock();
3660 removeText(currentRange());
3661 if (g.movetype == MoveLineWise)
3662 insertAutomaticIndentation(true);
3663 endEditBlock();
3664 setTargetColumn();
3665 } else if (g.submode == CommentSubMode) {
3666 pushUndoState(false);
3667 beginEditBlock();
3668 toggleComment(currentRange());
3669 endEditBlock();
3670 } else if (g.submode == AddSurroundingSubMode) {
3671 g.subsubmode = SurroundSubSubMode;
3672 g.dotCommand = dotCommandMovement;
3673
3674 // We now only know the region that should be surrounded, but not the actual
3675 // character that should surround it. We thus do NOT want to finish the
3676 // movement yet here, so we return early.
3677 // The next character entered will be used by the SurroundSubSubMode.
3678 return;
3679 } else if (g.submode == ExchangeSubMode) {
3680 exchangeRange(currentRange());
3681 } else if (g.submode == ReplaceWithRegisterSubMode && s.emulateReplaceWithRegister.value()) {
3682 pushUndoState(false);
3683 beginEditBlock();
3684 replaceWithRegister(currentRange());
3685 endEditBlock();
3686 } else if (g.submode == DeleteSubMode) {
3687 pushUndoState(false);
3688 beginEditBlock();
3689 const int pos = position();
3690 // Always delete something (e.g. 'dw' on an empty line deletes the line).
3691 if (pos == anchor() && g.movetype == MoveInclusive)
3692 removeText(Range(pos, pos + 1));
3693 else
3694 removeText(currentRange());
3695 if (g.movetype == MoveLineWise)
3696 handleStartOfLine();
3697 endEditBlock();
3698 } else if (g.submode == YankSubMode) {
3699 bool isVisualModeYank = isVisualMode();
3700 leaveVisualMode();
3701 const QTextCursor tc = m_cursor;
3702 if (g.rangemode == RangeBlockMode) {
3703 const int pos1 = tc.block().position();
3704 const int pos2 = blockAt(tc.anchor()).position();
3705 const int col = qMin(tc.position() - pos1, tc.anchor() - pos2);
3706 setPosition(qMin(pos1, pos2) + col);
3707 } else {
3708 setPosition(qMin(position(), anchor()));
3709 if (g.rangemode == RangeLineMode) {
3710 if (isVisualModeYank)
3711 moveToStartOfLine();
3712 else
3713 moveToTargetColumn();
3714 }
3715 }
3716 setTargetColumn();
3717 } else if (g.submode == InvertCaseSubMode
3718 || g.submode == UpCaseSubMode
3719 || g.submode == DownCaseSubMode) {
3720 beginEditBlock();
3721 if (g.submode == InvertCaseSubMode)
3722 invertCase(currentRange());
3723 else if (g.submode == DownCaseSubMode)
3724 downCase(currentRange());
3725 else if (g.submode == UpCaseSubMode)
3726 upCase(currentRange());
3727 if (g.movetype == MoveLineWise)
3728 handleStartOfLine();
3729 endEditBlock();
3730 } else if (g.submode == IndentSubMode
3731 || g.submode == ShiftRightSubMode
3732 || g.submode == ShiftLeftSubMode) {
3733 recordJump();
3734 pushUndoState(false);
3735 if (g.submode == IndentSubMode)
3736 indentSelectedText();
3737 else if (g.submode == ShiftRightSubMode)
3738 shiftRegionRight(1);
3739 else if (g.submode == ShiftLeftSubMode)
3740 shiftRegionLeft(1);
3741 }
3742
3743 if (!dotCommandMovement.isEmpty()) {
3744 QString dotCommand = dotCommandFromSubMode(g.submode);
3745 if (!dotCommand.isEmpty()) {
3746 if (g.submode == ReplaceWithRegisterSubMode)
3747 dotCommand = QString("\"%1%2").arg(QChar(m_register)).arg(dotCommand);
3748
3749 setDotCommand(dotCommand + dotCommandMovement);
3750 }
3751 }
3752
3753 // Change command continues in insert mode.
3754 if (g.submode == ChangeSubMode) {
3755 clearCurrentMode();
3756 enterInsertMode();
3757 } else {
3758 leaveCurrentMode();
3759 }
3760 }
3761
leaveCurrentMode()3762 void FakeVimHandler::Private::leaveCurrentMode()
3763 {
3764 if (isVisualMode())
3765 enterCommandMode(g.returnToMode);
3766 else if (g.returnToMode == CommandMode)
3767 enterCommandMode();
3768 else if (g.returnToMode == InsertMode)
3769 enterInsertMode();
3770 else
3771 enterReplaceMode();
3772
3773 if (isNoVisualMode())
3774 setAnchor();
3775 }
3776
clearCurrentMode()3777 void FakeVimHandler::Private::clearCurrentMode()
3778 {
3779 g.submode = NoSubMode;
3780 g.subsubmode = NoSubSubMode;
3781 g.movetype = MoveInclusive;
3782 g.gflag = false;
3783 g.surroundUpperCaseS = false;
3784 g.surroundFunction.clear();
3785 m_register = '"';
3786 g.rangemode = RangeCharMode;
3787 g.currentCommand.clear();
3788 resetCount();
3789 }
3790
updateSelection()3791 void FakeVimHandler::Private::updateSelection()
3792 {
3793 QList<QTextEdit::ExtraSelection> selections = m_extraSelections;
3794 if (s.showMarks.value()) {
3795 for (auto it = m_buffer->marks.cbegin(), end = m_buffer->marks.cend(); it != end; ++it) {
3796 QTextEdit::ExtraSelection sel;
3797 sel.cursor = m_cursor;
3798 setCursorPosition(&sel.cursor, it.value().position(document()));
3799 sel.cursor.setPosition(sel.cursor.position(), MoveAnchor);
3800 sel.cursor.movePosition(Right, KeepAnchor);
3801 sel.format = m_cursor.blockCharFormat();
3802 sel.format.setForeground(Qt::blue);
3803 sel.format.setBackground(Qt::green);
3804 selections.append(sel);
3805 }
3806 }
3807 //qDebug() << "SELECTION: " << selections;
3808 q->selectionChanged(selections);
3809 }
3810
updateHighlights()3811 void FakeVimHandler::Private::updateHighlights()
3812 {
3813 if (s.useCoreSearch.value() || !s.hlSearch.value() || g.highlightsCleared) {
3814 if (m_highlighted.isEmpty())
3815 return;
3816 m_highlighted.clear();
3817 } else if (m_highlighted != g.lastNeedle) {
3818 m_highlighted = g.lastNeedle;
3819 } else {
3820 return;
3821 }
3822
3823 q->highlightMatches(m_highlighted);
3824 }
3825
updateMiniBuffer()3826 void FakeVimHandler::Private::updateMiniBuffer()
3827 {
3828 if (!m_textedit && !m_plaintextedit)
3829 return;
3830
3831 QString msg;
3832 int cursorPos = -1;
3833 int anchorPos = -1;
3834 MessageLevel messageLevel = MessageMode;
3835
3836 if (!g.mapStates.isEmpty() && g.mapStates.last().silent && g.currentMessageLevel < MessageInfo)
3837 g.currentMessage.clear();
3838
3839 if (g.passing) {
3840 msg = "PASSING";
3841 } else if (g.subsubmode == SearchSubSubMode) {
3842 msg = g.searchBuffer.display();
3843 if (g.mapStates.isEmpty()) {
3844 cursorPos = g.searchBuffer.cursorPos() + 1;
3845 anchorPos = g.searchBuffer.anchorPos() + 1;
3846 }
3847 } else if (g.mode == ExMode) {
3848 msg = g.commandBuffer.display();
3849 if (g.mapStates.isEmpty()) {
3850 cursorPos = g.commandBuffer.cursorPos() + 1;
3851 anchorPos = g.commandBuffer.anchorPos() + 1;
3852 }
3853 } else if (!g.currentMessage.isEmpty()) {
3854 msg = g.currentMessage;
3855 g.currentMessage.clear();
3856 messageLevel = g.currentMessageLevel;
3857 } else if (!g.mapStates.isEmpty() && !g.mapStates.last().silent) {
3858 // Do not reset previous message when after running a mapped command.
3859 return;
3860 } else if (g.mode == CommandMode && !g.currentCommand.isEmpty() && s.showCmd.value()) {
3861 msg = g.currentCommand;
3862 messageLevel = MessageShowCmd;
3863 } else if (g.mode == CommandMode && isVisualMode()) {
3864 if (isVisualCharMode())
3865 msg = "-- VISUAL --";
3866 else if (isVisualLineMode())
3867 msg = "-- VISUAL LINE --";
3868 else if (isVisualBlockMode())
3869 msg = "VISUAL BLOCK";
3870 } else if (g.mode == InsertMode) {
3871 msg = "-- INSERT --";
3872 if (g.submode == CtrlRSubMode)
3873 msg += " ^R";
3874 else if (g.submode == CtrlVSubMode)
3875 msg += " ^V";
3876 } else if (g.mode == ReplaceMode) {
3877 msg = "-- REPLACE --";
3878 } else {
3879 if (g.returnToMode == CommandMode)
3880 msg = "-- COMMAND --";
3881 else if (g.returnToMode == InsertMode)
3882 msg = "-- (insert) --";
3883 else
3884 msg = "-- (replace) --";
3885 }
3886
3887 if (g.isRecording && msg.startsWith("--"))
3888 msg.append(' ').append("Recording");
3889
3890 q->commandBufferChanged(msg, cursorPos, anchorPos, messageLevel);
3891
3892 int linesInDoc = linesInDocument();
3893 int l = cursorLine();
3894 QString status;
3895 const QString pos = QString("%1,%2")
3896 .arg(l + 1).arg(physicalCursorColumn() + 1);
3897 // FIXME: physical "-" logical
3898 if (linesInDoc != 0)
3899 status = Tr::tr("%1%2%").arg(pos, -10).arg(l * 100 / linesInDoc, 4);
3900 else
3901 status = Tr::tr("%1All").arg(pos, -10);
3902 q->statusDataChanged(status);
3903 }
3904
showMessage(MessageLevel level,const QString & msg)3905 void FakeVimHandler::Private::showMessage(MessageLevel level, const QString &msg)
3906 {
3907 //qDebug() << "MSG: " << msg;
3908 g.currentMessage = msg;
3909 g.currentMessageLevel = level;
3910 }
3911
notImplementedYet()3912 void FakeVimHandler::Private::notImplementedYet()
3913 {
3914 qDebug() << "Not implemented in FakeVim";
3915 showMessage(MessageError, Tr::tr("Not implemented in FakeVim."));
3916 }
3917
passShortcuts(bool enable)3918 void FakeVimHandler::Private::passShortcuts(bool enable)
3919 {
3920 g.passing = enable;
3921 updateMiniBuffer();
3922 if (enable)
3923 QCoreApplication::instance()->installEventFilter(q);
3924 else
3925 QCoreApplication::instance()->removeEventFilter(q);
3926 }
3927
handleCommandSubSubMode(const Input & input)3928 bool FakeVimHandler::Private::handleCommandSubSubMode(const Input &input)
3929 {
3930 bool handled = true;
3931
3932 if (g.subsubmode == FtSubSubMode) {
3933 g.semicolonType = g.subsubdata;
3934 g.semicolonKey = input.text();
3935 handled = handleFfTt(g.semicolonKey);
3936 g.subsubmode = NoSubSubMode;
3937 if (handled) {
3938 finishMovement(QString("%1%2%3")
3939 .arg(count())
3940 .arg(g.semicolonType.text())
3941 .arg(g.semicolonKey));
3942 }
3943 } else if (g.subsubmode == TextObjectSubSubMode) {
3944 // vim-surround treats aw and aW the same as iw and iW, respectively
3945 if ((input.is('w') || input.is('W'))
3946 && g.submode == AddSurroundingSubMode && g.subsubdata.is('a'))
3947 g.subsubdata = Input('i');
3948
3949 if (input.is('w'))
3950 selectWordTextObject(g.subsubdata.is('i'));
3951 else if (input.is('W'))
3952 selectWORDTextObject(g.subsubdata.is('i'));
3953 else if (input.is('s'))
3954 selectSentenceTextObject(g.subsubdata.is('i'));
3955 else if (input.is('p'))
3956 selectParagraphTextObject(g.subsubdata.is('i'));
3957 else if (input.is('[') || input.is(']'))
3958 handled = selectBlockTextObject(g.subsubdata.is('i'), '[', ']');
3959 else if (input.is('(') || input.is(')') || input.is('b'))
3960 handled = selectBlockTextObject(g.subsubdata.is('i'), '(', ')');
3961 else if (input.is('<') || input.is('>'))
3962 handled = selectBlockTextObject(g.subsubdata.is('i'), '<', '>');
3963 else if (input.is('{') || input.is('}') || input.is('B'))
3964 handled = selectBlockTextObject(g.subsubdata.is('i'), '{', '}');
3965 else if (input.is('"') || input.is('\'') || input.is('`'))
3966 handled = selectQuotedStringTextObject(g.subsubdata.is('i'), input.asChar());
3967 else if (input.is('a') && s.emulateArgTextObj.value())
3968 handled = selectArgumentTextObject(g.subsubdata.is('i'));
3969 else
3970 handled = false;
3971 g.subsubmode = NoSubSubMode;
3972 if (handled) {
3973 finishMovement(QString("%1%2%3")
3974 .arg(count())
3975 .arg(g.subsubdata.text())
3976 .arg(input.text()));
3977 }
3978 } else if (g.subsubmode == MarkSubSubMode) {
3979 setMark(input.asChar(), CursorPosition(m_cursor));
3980 g.subsubmode = NoSubSubMode;
3981 } else if (g.subsubmode == BackTickSubSubMode
3982 || g.subsubmode == TickSubSubMode) {
3983 handled = jumpToMark(input.asChar(), g.subsubmode == BackTickSubSubMode);
3984 if (handled)
3985 finishMovement();
3986 g.subsubmode = NoSubSubMode;
3987 } else if (g.subsubmode == ZSubSubMode) {
3988 handled = false;
3989 if (input.is('j') || input.is('k')) {
3990 int pos = position();
3991 q->foldGoTo(input.is('j') ? count() : -count(), false);
3992 if (pos != position()) {
3993 handled = true;
3994 finishMovement(QString("%1z%2")
3995 .arg(count())
3996 .arg(input.text()));
3997 }
3998 }
3999 } else if (g.subsubmode == OpenSquareSubSubMode || g.subsubmode == CloseSquareSubSubMode) {
4000 int pos = position();
4001 if (input.is('{') && g.subsubmode == OpenSquareSubSubMode)
4002 searchBalanced(false, '{', '}');
4003 else if (input.is('}') && g.subsubmode == CloseSquareSubSubMode)
4004 searchBalanced(true, '}', '{');
4005 else if (input.is('(') && g.subsubmode == OpenSquareSubSubMode)
4006 searchBalanced(false, '(', ')');
4007 else if (input.is(')') && g.subsubmode == CloseSquareSubSubMode)
4008 searchBalanced(true, ')', '(');
4009 else if (input.is('[') && g.subsubmode == OpenSquareSubSubMode)
4010 bracketSearchBackward(&m_cursor, "^\\{", count());
4011 else if (input.is('[') && g.subsubmode == CloseSquareSubSubMode)
4012 bracketSearchForward(&m_cursor, "^\\}", count(), false);
4013 else if (input.is(']') && g.subsubmode == OpenSquareSubSubMode)
4014 bracketSearchBackward(&m_cursor, "^\\}", count());
4015 else if (input.is(']') && g.subsubmode == CloseSquareSubSubMode)
4016 bracketSearchForward(&m_cursor, "^\\{", count(), g.submode != NoSubMode);
4017 else if (input.is('z'))
4018 q->foldGoTo(g.subsubmode == OpenSquareSubSubMode ? -count() : count(), true);
4019 handled = pos != position();
4020 if (handled) {
4021 if (lineForPosition(pos) != lineForPosition(position()))
4022 recordJump(pos);
4023 finishMovement(QString("%1%2%3")
4024 .arg(count())
4025 .arg(g.subsubmode == OpenSquareSubSubMode ? '[' : ']')
4026 .arg(input.text()));
4027 }
4028 } else if (g.subsubmode == SurroundWithFunctionSubSubMode) {
4029 if (input.isReturn()) {
4030 pushUndoState(false);
4031 beginEditBlock();
4032
4033 const QString dotCommand = "ys" + g.dotCommand + "f" + g.surroundFunction + "<CR>";
4034
4035 surroundCurrentRange(Input(')'), g.surroundFunction);
4036
4037 g.dotCommand = dotCommand;
4038
4039 endEditBlock();
4040 leaveCurrentMode();
4041 } else {
4042 g.surroundFunction += input.asChar();
4043 }
4044 return true;
4045 } else if (g.subsubmode == SurroundSubSubMode) {
4046 if (input.is('f') && g.submode == AddSurroundingSubMode) {
4047 g.subsubmode = SurroundWithFunctionSubSubMode;
4048 g.commandBuffer.setContents("");
4049 return true;
4050 }
4051
4052 pushUndoState(false);
4053 beginEditBlock();
4054
4055 surroundCurrentRange(input);
4056
4057 endEditBlock();
4058 leaveCurrentMode();
4059 } else {
4060 handled = false;
4061 }
4062 return handled;
4063 }
4064
handleCount(const Input & input)4065 bool FakeVimHandler::Private::handleCount(const Input &input)
4066 {
4067 if (!isInputCount(input))
4068 return false;
4069 g.mvcount = g.mvcount * 10 + input.text().toInt();
4070 return true;
4071 }
4072
handleMovement(const Input & input)4073 bool FakeVimHandler::Private::handleMovement(const Input &input)
4074 {
4075 bool handled = true;
4076 int count = this->count();
4077
4078 if (handleCount(input)) {
4079 return true;
4080 } else if (input.is('0')) {
4081 g.movetype = MoveExclusive;
4082 if (g.gflag)
4083 moveToStartOfLineVisually();
4084 else
4085 moveToStartOfLine();
4086 count = 1;
4087 } else if (input.is('a') || input.is('i')) {
4088 g.subsubmode = TextObjectSubSubMode;
4089 g.subsubdata = input;
4090 } else if (input.is('^') || input.is('_')) {
4091 if (g.gflag)
4092 moveToFirstNonBlankOnLineVisually();
4093 else
4094 moveToFirstNonBlankOnLine();
4095 g.movetype = MoveExclusive;
4096 } else if (0 && input.is(',')) {
4097 // FIXME: fakevim uses ',' by itself, so it is incompatible
4098 g.subsubmode = FtSubSubMode;
4099 // HACK: toggle 'f' <-> 'F', 't' <-> 'T'
4100 //g.subsubdata = g.semicolonType ^ 32;
4101 handleFfTt(g.semicolonKey, true);
4102 g.subsubmode = NoSubSubMode;
4103 } else if (input.is(';')) {
4104 g.subsubmode = FtSubSubMode;
4105 g.subsubdata = g.semicolonType;
4106 handleFfTt(g.semicolonKey, true);
4107 g.subsubmode = NoSubSubMode;
4108 } else if (input.is('/') || input.is('?')) {
4109 g.lastSearchForward = input.is('/');
4110 if (s.useCoreSearch.value()) {
4111 // re-use the core dialog.
4112 g.findPending = true;
4113 m_findStartPosition = position();
4114 g.movetype = MoveExclusive;
4115 setAnchor(); // clear selection: otherwise, search is restricted to selection
4116 q->findRequested(!g.lastSearchForward);
4117 } else {
4118 // FIXME: make core find dialog sufficiently flexible to
4119 // produce the "default vi" behaviour too. For now, roll our own.
4120 g.currentMessage.clear();
4121 g.movetype = MoveExclusive;
4122 g.subsubmode = SearchSubSubMode;
4123 g.searchBuffer.setPrompt(g.lastSearchForward ? '/' : '?');
4124 m_searchStartPosition = position();
4125 m_searchFromScreenLine = firstVisibleLine();
4126 m_searchCursor = QTextCursor();
4127 g.searchBuffer.clear();
4128 }
4129 } else if (input.is('`')) {
4130 g.subsubmode = BackTickSubSubMode;
4131 } else if (input.is('#') || input.is('*')) {
4132 // FIXME: That's not proper vim behaviour
4133 QString needle;
4134 QTextCursor tc = m_cursor;
4135 tc.select(QTextCursor::WordUnderCursor);
4136 needle = QRegularExpression::escape(tc.selection().toPlainText());
4137 if (!g.gflag) {
4138 needle.prepend("\\<");
4139 needle.append("\\>");
4140 }
4141 setAnchorAndPosition(tc.position(), tc.anchor());
4142 g.searchBuffer.historyPush(needle);
4143 g.lastSearch = needle;
4144 g.lastSearchForward = input.is('*');
4145 handled = searchNext();
4146 } else if (input.is('\'')) {
4147 g.subsubmode = TickSubSubMode;
4148 if (g.submode != NoSubMode)
4149 g.movetype = MoveLineWise;
4150 } else if (input.is('|')) {
4151 moveToStartOfLine();
4152 const int column = count - 1;
4153 moveRight(qMin(column, rightDist() - 1));
4154 m_targetColumn = column;
4155 m_visualTargetColumn = column;
4156 } else if (input.is('{') || input.is('}')) {
4157 const int oldPosition = position();
4158 handled = input.is('}')
4159 ? moveToNextParagraph(count)
4160 : moveToPreviousParagraph(count);
4161 if (handled) {
4162 recordJump(oldPosition);
4163 setTargetColumn();
4164 g.movetype = MoveExclusive;
4165 }
4166 } else if (input.isReturn()) {
4167 moveToStartOfLine();
4168 moveDown();
4169 moveToFirstNonBlankOnLine();
4170 } else if (input.is('-')) {
4171 moveToStartOfLine();
4172 moveUp(count);
4173 moveToFirstNonBlankOnLine();
4174 } else if (input.is('+')) {
4175 moveToStartOfLine();
4176 moveDown(count);
4177 moveToFirstNonBlankOnLine();
4178 } else if (input.isKey(Key_Home)) {
4179 moveToStartOfLine();
4180 } else if (input.is('$') || input.isKey(Key_End)) {
4181 if (g.gflag) {
4182 if (count > 1)
4183 moveDownVisually(count - 1);
4184 moveToEndOfLineVisually();
4185 } else {
4186 if (count > 1)
4187 moveDown(count - 1);
4188 moveToEndOfLine();
4189 }
4190 g.movetype = atEmptyLine() ? MoveExclusive : MoveInclusive;
4191 if (g.submode == NoSubMode)
4192 m_targetColumn = -1;
4193 if (isVisualMode())
4194 m_visualTargetColumn = -1;
4195 } else if (input.is('%')) {
4196 recordJump();
4197 if (g.mvcount == 0) {
4198 moveToMatchingParanthesis();
4199 g.movetype = MoveInclusive;
4200 } else {
4201 // set cursor position in percentage - formula taken from Vim help
4202 setPosition(firstPositionInLine((count * linesInDocument() + 99) / 100));
4203 moveToTargetColumn();
4204 handleStartOfLine();
4205 g.movetype = MoveLineWise;
4206 }
4207 } else if (input.is('b') || input.isShift(Key_Left)) {
4208 moveToNextWordStart(count, false, false);
4209 } else if (input.is('B') || input.isControl(Key_Left)) {
4210 moveToNextWordStart(count, true, false);
4211 } else if (input.is('e') && g.gflag) {
4212 moveToNextWordEnd(count, false, false);
4213 } else if (input.is('e')) {
4214 moveToNextWordEnd(count, false, true, false);
4215 } else if (input.is('E') && g.gflag) {
4216 moveToNextWordEnd(count, true, false);
4217 } else if (input.is('E')) {
4218 moveToNextWordEnd(count, true, true, false);
4219 } else if (input.isControl('e')) {
4220 // FIXME: this should use the "scroll" option, and "count"
4221 if (cursorLineOnScreen() == 0)
4222 moveDown(1);
4223 scrollDown(1);
4224 } else if (input.is('f')) {
4225 g.subsubmode = FtSubSubMode;
4226 g.movetype = MoveInclusive;
4227 g.subsubdata = input;
4228 } else if (input.is('F')) {
4229 g.subsubmode = FtSubSubMode;
4230 g.movetype = MoveExclusive;
4231 g.subsubdata = input;
4232 } else if (!g.gflag && input.is('g')) {
4233 g.gflag = true;
4234 return true;
4235 } else if (input.is('g') || input.is('G')) {
4236 QString dotCommand = QString("%1G").arg(count);
4237 recordJump();
4238 if (input.is('G') && g.mvcount == 0)
4239 dotCommand = "G";
4240 int n = (input.is('g')) ? 1 : linesInDocument();
4241 n = g.mvcount == 0 ? n : count;
4242 if (g.submode == NoSubMode || g.submode == ZSubMode
4243 || g.submode == CapitalZSubMode || g.submode == RegisterSubMode) {
4244 setPosition(firstPositionInLine(n, false));
4245 handleStartOfLine();
4246 } else {
4247 g.movetype = MoveLineWise;
4248 g.rangemode = RangeLineMode;
4249 setAnchor();
4250 setPosition(firstPositionInLine(n, false));
4251 }
4252 setTargetColumn();
4253 updateScrollOffset();
4254 } else if (input.is('h') || input.isKey(Key_Left) || input.isBackspace()) {
4255 g.movetype = MoveExclusive;
4256 int n = qMin(count, leftDist());
4257 moveLeft(n);
4258 } else if (input.is('H')) {
4259 const CursorPosition pos(lineToBlockNumber(lineOnTop(count)), 0);
4260 setCursorPosition(&m_cursor, pos);
4261 handleStartOfLine();
4262 } else if (input.is('j') || input.isKey(Key_Down)
4263 || input.isControl('j') || input.isControl('n')) {
4264 moveVertically(count);
4265 } else if (input.is('k') || input.isKey(Key_Up) || input.isControl('p')) {
4266 moveVertically(-count);
4267 } else if (input.is('l') || input.isKey(Key_Right) || input.is(' ')) {
4268 g.movetype = MoveExclusive;
4269 moveRight(qMax(0, qMin(count, rightDist() - (g.submode == NoSubMode))));
4270 } else if (input.is('L')) {
4271 const CursorPosition pos(lineToBlockNumber(lineOnBottom(count)), 0);
4272 setCursorPosition(&m_cursor, pos);
4273 handleStartOfLine();
4274 } else if (g.gflag && input.is('m')) {
4275 const QPoint pos(EDITOR(viewport()->width()) / 2, EDITOR(cursorRect(m_cursor)).y());
4276 QTextCursor tc = EDITOR(cursorForPosition(pos));
4277 if (!tc.isNull()) {
4278 m_cursor = tc;
4279 setTargetColumn();
4280 }
4281 } else if (input.is('M')) {
4282 m_cursor = EDITOR(cursorForPosition(QPoint(0, EDITOR(height()) / 2)));
4283 handleStartOfLine();
4284 } else if (input.is('n') || input.is('N')) {
4285 if (s.useCoreSearch.value()) {
4286 bool forward = (input.is('n')) ? g.lastSearchForward : !g.lastSearchForward;
4287 int pos = position();
4288 q->findNextRequested(!forward);
4289 if (forward && pos == m_cursor.selectionStart()) {
4290 // if cursor is already positioned at the start of a find result, this is returned
4291 q->findNextRequested(false);
4292 }
4293 setPosition(m_cursor.selectionStart());
4294 } else {
4295 handled = searchNext(input.is('n'));
4296 }
4297 } else if (input.is('t')) {
4298 g.movetype = MoveInclusive;
4299 g.subsubmode = FtSubSubMode;
4300 g.subsubdata = input;
4301 } else if (input.is('T')) {
4302 g.movetype = MoveExclusive;
4303 g.subsubmode = FtSubSubMode;
4304 g.subsubdata = input;
4305 } else if (input.is('w') || input.is('W')
4306 || input.isShift(Key_Right) || input.isControl(Key_Right)) { // tested
4307 // Special case: "cw" and "cW" work the same as "ce" and "cE" if the
4308 // cursor is on a non-blank - except if the cursor is on the last
4309 // character of a word: only the current word will be changed
4310 bool simple = input.is('W') || input.isControl(Key_Right);
4311 if (g.submode == ChangeSubMode && !characterAtCursor().isSpace()) {
4312 moveToWordEnd(count, simple, true);
4313 } else {
4314 moveToNextWordStart(count, simple, true);
4315 // Command 'dw' deletes to the next word on the same line or to end of line.
4316 if (g.submode == DeleteSubMode && count == 1) {
4317 const QTextBlock currentBlock = blockAt(anchor());
4318 setPosition(qMin(position(), currentBlock.position() + currentBlock.length()));
4319 }
4320 }
4321 } else if (input.is('z')) {
4322 g.movetype = MoveLineWise;
4323 g.subsubmode = ZSubSubMode;
4324 } else if (input.is('[')) {
4325 g.subsubmode = OpenSquareSubSubMode;
4326 } else if (input.is(']')) {
4327 g.subsubmode = CloseSquareSubSubMode;
4328 } else if (input.isKey(Key_PageDown) || input.isControl('f')) {
4329 movePageDown(count);
4330 handleStartOfLine();
4331 } else if (input.isKey(Key_PageUp) || input.isControl('b')) {
4332 movePageUp(count);
4333 handleStartOfLine();
4334 } else {
4335 handled = false;
4336 }
4337
4338 if (handled && g.subsubmode == NoSubSubMode) {
4339 if (g.submode == NoSubMode) {
4340 leaveCurrentMode();
4341 } else {
4342 // finish movement for sub modes
4343 const QString dotMovement =
4344 (count > 1 ? QString::number(count) : QString())
4345 + QLatin1String(g.gflag ? "g" : "")
4346 + input.toString();
4347 finishMovement(dotMovement);
4348 setTargetColumn();
4349 }
4350 }
4351
4352 return handled;
4353 }
4354
handleCommandMode(const Input & input)4355 EventResult FakeVimHandler::Private::handleCommandMode(const Input &input)
4356 {
4357 bool handled = false;
4358
4359 bool clearGflag = g.gflag;
4360 bool clearRegister = g.submode != RegisterSubMode;
4361 bool clearCount = g.submode != RegisterSubMode && !isInputCount(input);
4362
4363 // Process input for a sub-mode.
4364 if (input.isEscape()) {
4365 handled = handleEscape();
4366 } else if (m_wasReadOnly) {
4367 return EventUnhandled;
4368 } else if (g.subsubmode != NoSubSubMode) {
4369 handled = handleCommandSubSubMode(input);
4370 } else if (g.submode == NoSubMode) {
4371 handled = handleNoSubMode(input);
4372 } else if (g.submode == ExchangeSubMode) {
4373 handled = handleExchangeSubMode(input);
4374 } else if (g.submode == ChangeSubMode && input.is('x') && s.emulateExchange.value()) {
4375 // Exchange submode is "cx", so we need to switch over from ChangeSubMode here
4376 g.submode = ExchangeSubMode;
4377 handled = true;
4378 } else if (g.submode == DeleteSurroundingSubMode
4379 || g.submode == ChangeSurroundingSubMode) {
4380 handled = handleDeleteChangeSurroundingSubMode(input);
4381 } else if (g.submode == AddSurroundingSubMode) {
4382 handled = handleAddSurroundingSubMode(input);
4383 } else if (g.submode == ChangeSubMode && (input.is('s') || input.is('S'))
4384 && s.emulateSurround.value()) {
4385 g.submode = ChangeSurroundingSubMode;
4386 g.surroundUpperCaseS = input.is('S');
4387 handled = true;
4388 } else if (g.submode == DeleteSubMode && input.is('s') && s.emulateSurround.value()) {
4389 g.submode = DeleteSurroundingSubMode;
4390 handled = true;
4391 } else if (g.submode == YankSubMode && (input.is('s') || input.is('S'))
4392 && s.emulateSurround.value()) {
4393 g.submode = AddSurroundingSubMode;
4394 g.movetype = MoveInclusive;
4395 g.surroundUpperCaseS = input.is('S');
4396 handled = true;
4397 } else if (g.submode == ChangeSubMode
4398 || g.submode == DeleteSubMode
4399 || g.submode == YankSubMode) {
4400 handled = handleChangeDeleteYankSubModes(input);
4401 } else if (g.submode == CommentSubMode && s.emulateVimCommentary.value()) {
4402 handled = handleCommentSubMode(input);
4403 } else if (g.submode == ReplaceWithRegisterSubMode
4404 && s.emulateReplaceWithRegister.value()) {
4405 handled = handleReplaceWithRegisterSubMode(input);
4406 } else if (g.submode == ReplaceSubMode) {
4407 handled = handleReplaceSubMode(input);
4408 } else if (g.submode == FilterSubMode) {
4409 handled = handleFilterSubMode(input);
4410 } else if (g.submode == RegisterSubMode) {
4411 handled = handleRegisterSubMode(input);
4412 } else if (g.submode == WindowSubMode) {
4413 handled = handleWindowSubMode(input);
4414 } else if (g.submode == ZSubMode) {
4415 handled = handleZSubMode(input);
4416 } else if (g.submode == CapitalZSubMode) {
4417 handled = handleCapitalZSubMode(input);
4418 } else if (g.submode == MacroRecordSubMode) {
4419 handled = handleMacroRecordSubMode(input);
4420 } else if (g.submode == MacroExecuteSubMode) {
4421 handled = handleMacroExecuteSubMode(input);
4422 } else if (g.submode == ShiftLeftSubMode
4423 || g.submode == ShiftRightSubMode
4424 || g.submode == IndentSubMode) {
4425 handled = handleShiftSubMode(input);
4426 } else if (g.submode == InvertCaseSubMode
4427 || g.submode == DownCaseSubMode
4428 || g.submode == UpCaseSubMode) {
4429 handled = handleChangeCaseSubMode(input);
4430 }
4431
4432 if (!handled && isOperatorPending())
4433 handled = handleMovement(input);
4434
4435 // Clear state and display incomplete command if necessary.
4436 if (handled) {
4437 bool noMode =
4438 (g.mode == CommandMode && g.submode == NoSubMode && g.subsubmode == NoSubSubMode);
4439 clearCount = clearCount && noMode && !g.gflag;
4440 if (clearCount && clearRegister) {
4441 leaveCurrentMode();
4442 } else {
4443 // Use gflag only for next input.
4444 if (clearGflag)
4445 g.gflag = false;
4446 // Clear [count] and [register] if its no longer needed.
4447 if (clearCount)
4448 resetCount();
4449 // Show or clear current command on minibuffer (showcmd).
4450 if (input.isEscape() || g.mode != CommandMode || clearCount)
4451 g.currentCommand.clear();
4452 else
4453 g.currentCommand.append(input.toString());
4454 }
4455
4456 saveLastVisualMode();
4457 } else {
4458 leaveCurrentMode();
4459 //qDebug() << "IGNORED IN COMMAND MODE: " << key << text
4460 // << " VISUAL: " << g.visualMode;
4461
4462 // if a key which produces text was pressed, don't mark it as unhandled
4463 // - otherwise the text would be inserted while being in command mode
4464 if (input.text().isEmpty())
4465 handled = false;
4466 }
4467
4468 m_positionPastEnd = (m_visualTargetColumn == -1) && isVisualMode() && !atEmptyLine();
4469
4470 return handled ? EventHandled : EventCancelled;
4471 }
4472
handleEscape()4473 bool FakeVimHandler::Private::handleEscape()
4474 {
4475 if (isVisualMode())
4476 leaveVisualMode();
4477 leaveCurrentMode();
4478 return true;
4479 }
4480
handleNoSubMode(const Input & input)4481 bool FakeVimHandler::Private::handleNoSubMode(const Input &input)
4482 {
4483 bool handled = true;
4484
4485 const int oldRevision = revision();
4486 QString dotCommand = visualDotCommand()
4487 + QLatin1String(g.gflag ? "g" : "")
4488 + QString::number(count())
4489 + input.toString();
4490
4491 if (input.is('&')) {
4492 handleExCommand(QLatin1String(g.gflag ? "%s//~/&" : "s"));
4493 } else if (input.is(':')) {
4494 enterExMode();
4495 } else if (input.is('!') && isNoVisualMode()) {
4496 g.submode = FilterSubMode;
4497 } else if (input.is('!') && isVisualMode()) {
4498 enterExMode(QString("!"));
4499 } else if (input.is('"')) {
4500 g.submode = RegisterSubMode;
4501 } else if (input.is(',')) {
4502 passShortcuts(true);
4503 } else if (input.is('.')) {
4504 //qDebug() << "REPEATING" << quoteUnprintable(g.dotCommand) << count()
4505 // << input;
4506 dotCommand.clear();
4507 QString savedCommand = g.dotCommand;
4508 g.dotCommand.clear();
4509 beginLargeEditBlock();
4510 replay(savedCommand);
4511 endEditBlock();
4512 leaveCurrentMode();
4513 g.dotCommand = savedCommand;
4514 } else if (input.is('<') || input.is('>') || input.is('=')) {
4515 g.submode = indentModeFromInput(input);
4516 if (isVisualMode()) {
4517 leaveVisualMode();
4518 const int repeat = count();
4519 if (g.submode == ShiftLeftSubMode)
4520 shiftRegionLeft(repeat);
4521 else if (g.submode == ShiftRightSubMode)
4522 shiftRegionRight(repeat);
4523 else
4524 indentSelectedText();
4525 g.submode = NoSubMode;
4526 } else {
4527 setAnchor();
4528 }
4529 } else if ((!isVisualMode() && input.is('a')) || (isVisualMode() && input.is('A'))) {
4530 if (isVisualMode()) {
4531 if (!isVisualBlockMode())
4532 dotCommand = QString::number(count()) + "a";
4533 enterVisualInsertMode('A');
4534 } else {
4535 moveRight(qMin(rightDist(), 1));
4536 breakEditBlock();
4537 enterInsertMode();
4538 }
4539 } else if (input.is('A')) {
4540 breakEditBlock();
4541 moveBehindEndOfLine();
4542 setAnchor();
4543 enterInsertMode();
4544 setTargetColumn();
4545 } else if (input.isControl('a')) {
4546 changeNumberTextObject(count());
4547 } else if (g.gflag && input.is('c') && s.emulateVimCommentary.value()) {
4548 if (isVisualMode()) {
4549 pushUndoState();
4550
4551 QTextCursor start(m_cursor);
4552 QTextCursor end(start);
4553 end.setPosition(end.anchor());
4554
4555 const int count = qAbs(start.blockNumber() - end.blockNumber());
4556 if (count == 0) {
4557 dotCommand = "gcc";
4558 } else {
4559 dotCommand = QString("gc%1j").arg(count);
4560 }
4561
4562 leaveVisualMode();
4563 toggleComment(currentRange());
4564
4565 g.submode = NoSubMode;
4566 } else {
4567 g.movetype = MoveLineWise;
4568 g.submode = CommentSubMode;
4569 pushUndoState();
4570 setAnchor();
4571 }
4572 } else if (g.gflag && input.is('r') && s.emulateReplaceWithRegister.value()) {
4573 g.submode = ReplaceWithRegisterSubMode;
4574 if (isVisualMode()) {
4575 dotCommand = visualDotCommand() + QString::number(count()) + "gr";
4576 pasteText(true);
4577 } else {
4578 setAnchor();
4579 }
4580 } else if ((input.is('c') || input.is('d') || input.is('y')) && isNoVisualMode()) {
4581 setAnchor();
4582 g.opcount = g.mvcount;
4583 g.mvcount = 0;
4584 g.rangemode = RangeCharMode;
4585 g.movetype = MoveExclusive;
4586 g.submode = changeDeleteYankModeFromInput(input);
4587 } else if ((input.is('c') || input.is('C') || input.is('s') || input.is('R'))
4588 && (isVisualCharMode() || isVisualLineMode())) {
4589 leaveVisualMode();
4590 g.submode = ChangeSubMode;
4591 finishMovement();
4592 } else if ((input.is('c') || input.is('s')) && isVisualBlockMode()) {
4593 resetCount();
4594 enterVisualInsertMode(input.asChar());
4595 } else if (input.is('C')) {
4596 handleAs("%1c$");
4597 } else if (input.isControl('c')) {
4598 if (isNoVisualMode()) {
4599 #if defined(Q_OS_MACOS)
4600 showMessage(MessageInfo, Tr::tr("Type Meta-Shift-Y, Meta-Shift-Y to quit FakeVim mode."));
4601 #else
4602 showMessage(MessageInfo, Tr::tr("Type Alt-Y, Alt-Y to quit FakeVim mode."));
4603 #endif
4604 } else {
4605 leaveVisualMode();
4606 }
4607 } else if ((input.is('d') || input.is('x') || input.isKey(Key_Delete))
4608 && isVisualMode()) {
4609 cutSelectedText();
4610 } else if (input.is('D') && isNoVisualMode()) {
4611 handleAs("%1d$");
4612 } else if ((input.is('D') || input.is('X')) && isVisualMode()) {
4613 if (isVisualCharMode())
4614 toggleVisualMode(VisualLineMode);
4615 if (isVisualBlockMode() && input.is('D'))
4616 m_visualTargetColumn = -1;
4617 cutSelectedText();
4618 } else if (input.isControl('d')) {
4619 const int scrollOffset = windowScrollOffset();
4620 int sline = cursorLine() < scrollOffset ? scrollOffset : cursorLineOnScreen();
4621 // FIXME: this should use the "scroll" option, and "count"
4622 moveDown(linesOnScreen() / 2);
4623 handleStartOfLine();
4624 scrollToLine(cursorLine() - sline);
4625 } else if (!g.gflag && input.is('g')) {
4626 g.gflag = true;
4627 } else if (!isVisualMode() && (input.is('i') || input.isKey(Key_Insert))) {
4628 breakEditBlock();
4629 enterInsertMode();
4630 if (atEndOfLine())
4631 moveLeft();
4632 } else if (input.is('I')) {
4633 if (isVisualMode()) {
4634 if (!isVisualBlockMode())
4635 dotCommand = QString::number(count()) + "i";
4636 enterVisualInsertMode('I');
4637 } else {
4638 if (g.gflag)
4639 moveToStartOfLine();
4640 else
4641 moveToFirstNonBlankOnLine();
4642 breakEditBlock();
4643 enterInsertMode();
4644 }
4645 } else if (input.isControl('i')) {
4646 jump(count());
4647 } else if (input.is('J')) {
4648 pushUndoState();
4649 moveBehindEndOfLine();
4650 beginEditBlock();
4651 if (g.submode == NoSubMode)
4652 joinLines(count(), g.gflag);
4653 endEditBlock();
4654 } else if (input.isControl('l')) {
4655 // screen redraw. should not be needed
4656 } else if (!g.gflag && input.is('m')) {
4657 g.subsubmode = MarkSubSubMode;
4658 } else if (isVisualMode() && (input.is('o') || input.is('O'))) {
4659 int pos = position();
4660 setAnchorAndPosition(pos, anchor());
4661 std::swap(m_positionPastEnd, m_anchorPastEnd);
4662 setTargetColumn();
4663 if (m_positionPastEnd)
4664 m_visualTargetColumn = -1;
4665 } else if (input.is('o') || input.is('O')) {
4666 bool insertAfter = input.is('o');
4667 pushUndoState();
4668
4669 // Prepend line only if on the first line and command is 'O'.
4670 bool appendLine = true;
4671 if (!insertAfter) {
4672 if (block().blockNumber() == 0)
4673 appendLine = false;
4674 else
4675 moveUp();
4676 }
4677 const int line = lineNumber(block());
4678
4679 beginEditBlock();
4680 enterInsertMode();
4681 setPosition(appendLine ? lastPositionInLine(line) : firstPositionInLine(line));
4682 clearLastInsertion();
4683 setAnchor();
4684 insertNewLine();
4685 if (appendLine) {
4686 m_buffer->insertState.newLineBefore = true;
4687 } else {
4688 moveUp();
4689 m_buffer->insertState.pos1 = position();
4690 m_buffer->insertState.newLineAfter = true;
4691 }
4692 setTargetColumn();
4693 endEditBlock();
4694
4695 // Close accidentally opened block.
4696 if (block().blockNumber() > 0) {
4697 moveUp();
4698 if (line != lineNumber(block()))
4699 q->fold(1, true);
4700 moveDown();
4701 }
4702 } else if (input.isControl('o')) {
4703 jump(-count());
4704 } else if (input.is('p') || input.is('P') || input.isShift(Qt::Key_Insert)) {
4705 dotCommand = QString("\"%1%2%3").arg(QChar(m_register)).arg(count()).arg(input.asChar());
4706
4707 pasteText(!input.is('P'));
4708 setTargetColumn();
4709 finishMovement();
4710 } else if (input.is('q')) {
4711 if (g.isRecording) {
4712 // Stop recording.
4713 stopRecording();
4714 } else {
4715 // Recording shouldn't work in mapping or while executing register.
4716 handled = g.mapStates.empty();
4717 if (handled)
4718 g.submode = MacroRecordSubMode;
4719 }
4720 } else if (input.is('r')) {
4721 g.submode = ReplaceSubMode;
4722 } else if (!isVisualMode() && input.is('R')) {
4723 pushUndoState();
4724 breakEditBlock();
4725 enterReplaceMode();
4726 } else if (input.isControl('r')) {
4727 dotCommand.clear();
4728 int repeat = count();
4729 while (--repeat >= 0)
4730 redo();
4731 } else if (input.is('S') && isVisualMode() && s.emulateSurround.value()) {
4732 g.submode = AddSurroundingSubMode;
4733 g.subsubmode = SurroundSubSubMode;
4734 } else if (input.is('s')) {
4735 handleAs("c%1l");
4736 } else if (input.is('S')) {
4737 handleAs("%1cc");
4738 } else if (g.gflag && input.is('t')) {
4739 handleExCommand("tabnext");
4740 } else if (g.gflag && input.is('T')) {
4741 handleExCommand("tabprevious");
4742 } else if (input.isControl('t')) {
4743 handleExCommand("pop");
4744 } else if (!g.gflag && input.is('u') && !isVisualMode()) {
4745 dotCommand.clear();
4746 int repeat = count();
4747 while (--repeat >= 0)
4748 undo();
4749 } else if (input.isControl('u')) {
4750 int sline = cursorLineOnScreen();
4751 // FIXME: this should use the "scroll" option, and "count"
4752 moveUp(linesOnScreen() / 2);
4753 handleStartOfLine();
4754 scrollToLine(cursorLine() - sline);
4755 } else if (g.gflag && input.is('v')) {
4756 if (isNoVisualMode()) {
4757 CursorPosition from = markLessPosition();
4758 CursorPosition to = markGreaterPosition();
4759 if (m_buffer->lastVisualModeInverted)
4760 std::swap(from, to);
4761 toggleVisualMode(m_buffer->lastVisualMode);
4762 setCursorPosition(from);
4763 setAnchor();
4764 setCursorPosition(to);
4765 setTargetColumn();
4766 }
4767 } else if (input.is('v')) {
4768 toggleVisualMode(VisualCharMode);
4769 } else if (input.is('V')) {
4770 toggleVisualMode(VisualLineMode);
4771 } else if (input.isControl('v')) {
4772 toggleVisualMode(VisualBlockMode);
4773 } else if (input.isControl('w')) {
4774 g.submode = WindowSubMode;
4775 } else if (input.is('x') && isNoVisualMode()) {
4776 handleAs("%1dl");
4777 } else if (input.isControl('x')) {
4778 changeNumberTextObject(-count());
4779 } else if (input.is('X')) {
4780 handleAs("%1dh");
4781 } else if (input.is('Y') && isNoVisualMode()) {
4782 handleAs("%1yy");
4783 } else if (input.isControl('y')) {
4784 // FIXME: this should use the "scroll" option, and "count"
4785 if (cursorLineOnScreen() == linesOnScreen() - 1)
4786 moveUp(1);
4787 scrollUp(1);
4788 } else if (input.is('y') && isVisualCharMode()) {
4789 g.rangemode = RangeCharMode;
4790 g.movetype = MoveInclusive;
4791 g.submode = YankSubMode;
4792 finishMovement();
4793 } else if ((input.is('y') && isVisualLineMode())
4794 || (input.is('Y') && isVisualLineMode())
4795 || (input.is('Y') && isVisualCharMode())) {
4796 g.rangemode = RangeLineMode;
4797 g.movetype = MoveLineWise;
4798 g.submode = YankSubMode;
4799 finishMovement();
4800 } else if ((input.is('y') || input.is('Y')) && isVisualBlockMode()) {
4801 g.rangemode = RangeBlockMode;
4802 g.movetype = MoveInclusive;
4803 g.submode = YankSubMode;
4804 finishMovement();
4805 } else if (input.is('z')) {
4806 g.submode = ZSubMode;
4807 } else if (input.is('Z')) {
4808 g.submode = CapitalZSubMode;
4809 } else if ((input.is('~') || input.is('u') || input.is('U'))) {
4810 g.movetype = MoveExclusive;
4811 g.submode = letterCaseModeFromInput(input);
4812 pushUndoState();
4813 if (isVisualMode()) {
4814 leaveVisualMode();
4815 finishMovement();
4816 } else if (g.gflag || (g.submode == InvertCaseSubMode && s.tildeOp.value())) {
4817 if (atEndOfLine())
4818 moveLeft();
4819 setAnchor();
4820 } else {
4821 const QString movementCommand = QString("%1l%1l").arg(count());
4822 handleAs("g" + input.toString() + movementCommand);
4823 }
4824 } else if (input.is('@')) {
4825 g.submode = MacroExecuteSubMode;
4826 } else if (input.isKey(Key_Delete)) {
4827 setAnchor();
4828 moveRight(qMin(1, rightDist()));
4829 removeText(currentRange());
4830 if (atEndOfLine())
4831 moveLeft();
4832 } else if (input.isControl(Key_BracketRight)) {
4833 handleExCommand("tag");
4834 } else if (handleMovement(input)) {
4835 // movement handled
4836 dotCommand.clear();
4837 } else {
4838 handled = false;
4839 }
4840
4841 // Set dot command if the current input changed document or entered insert mode.
4842 if (handled && !dotCommand.isEmpty() && (oldRevision != revision() || isInsertMode()))
4843 setDotCommand(dotCommand);
4844
4845 return handled;
4846 }
4847
handleChangeDeleteYankSubModes(const Input & input)4848 bool FakeVimHandler::Private::handleChangeDeleteYankSubModes(const Input &input)
4849 {
4850 if (g.submode != changeDeleteYankModeFromInput(input))
4851 return false;
4852
4853 handleChangeDeleteYankSubModes();
4854
4855 return true;
4856 }
4857
handleChangeDeleteYankSubModes()4858 void FakeVimHandler::Private::handleChangeDeleteYankSubModes()
4859 {
4860 g.movetype = MoveLineWise;
4861
4862 const QString dotCommand = dotCommandFromSubMode(g.submode);
4863
4864 if (!dotCommand.isEmpty())
4865 pushUndoState();
4866
4867 const int anc = firstPositionInLine(cursorLine() + 1);
4868 moveDown(count() - 1);
4869 const int pos = lastPositionInLine(cursorLine() + 1);
4870 setAnchorAndPosition(anc, pos);
4871
4872 if (!dotCommand.isEmpty())
4873 setDotCommand(QString("%2%1%1").arg(dotCommand), count());
4874
4875 finishMovement();
4876
4877 g.submode = NoSubMode;
4878 }
4879
handleReplaceSubMode(const Input & input)4880 bool FakeVimHandler::Private::handleReplaceSubMode(const Input &input)
4881 {
4882 bool handled = true;
4883
4884 const QChar c = input.asChar();
4885 setDotCommand(visualDotCommand() + 'r' + c);
4886 if (isVisualMode()) {
4887 pushUndoState();
4888 leaveVisualMode();
4889 Range range = currentRange();
4890 if (g.rangemode == RangeCharMode)
4891 ++range.endPos;
4892 // Replace each character but preserve lines.
4893 transformText(range, [&c](const QString &text) {
4894 return QString(text).replace(QRegularExpression("[^\\n]"), c);
4895 });
4896 } else if (count() <= rightDist()) {
4897 pushUndoState();
4898 setAnchor();
4899 moveRight(count());
4900 Range range = currentRange();
4901 if (input.isReturn()) {
4902 beginEditBlock();
4903 replaceText(range, QString());
4904 insertText(QString("\n"));
4905 endEditBlock();
4906 } else {
4907 replaceText(range, QString(count(), c));
4908 moveRight(count() - 1);
4909 }
4910 setTargetColumn();
4911 setDotCommand("%1r" + input.text(), count());
4912 } else {
4913 handled = false;
4914 }
4915 g.submode = NoSubMode;
4916 finishMovement();
4917
4918 return handled;
4919 }
4920
handleCommentSubMode(const Input & input)4921 bool FakeVimHandler::Private::handleCommentSubMode(const Input &input)
4922 {
4923 if (!input.is('c'))
4924 return false;
4925
4926 g.movetype = MoveLineWise;
4927
4928 const int anc = firstPositionInLine(cursorLine() + 1);
4929 moveDown(count() - 1);
4930 const int pos = lastPositionInLine(cursorLine() + 1);
4931 setAnchorAndPosition(anc, pos);
4932
4933 setDotCommand(QString("%1gcc").arg(count()));
4934
4935 finishMovement();
4936
4937 g.submode = NoSubMode;
4938
4939 return true;
4940 }
4941
handleReplaceWithRegisterSubMode(const Input & input)4942 bool FakeVimHandler::Private::handleReplaceWithRegisterSubMode(const Input &input)
4943 {
4944 if (!input.is('r'))
4945 return false;
4946
4947 pushUndoState(false);
4948 beginEditBlock();
4949
4950 const QString movement = (count() == 1)
4951 ? QString() : (QString::number(count() - 1) + "j");
4952
4953 g.dotCommand = "V" + movement + "gr";
4954 replay(g.dotCommand);
4955
4956 endEditBlock();
4957
4958 return true;
4959 }
4960
handleExchangeSubMode(const Input & input)4961 bool FakeVimHandler::Private::handleExchangeSubMode(const Input &input)
4962 {
4963 if (input.is('c')) { // cxc
4964 g.exchangeRange.reset();
4965 g.submode = NoSubMode;
4966 return true;
4967 }
4968
4969 if (input.is('x')) { // cxx
4970 setAnchorAndPosition(firstPositionInLine(cursorLine() + 1),
4971 lastPositionInLine(cursorLine() + 1) + 1);
4972
4973 setDotCommand("cxx");
4974
4975 finishMovement();
4976
4977 g.submode = NoSubMode;
4978
4979 return true;
4980 }
4981
4982 return false;
4983 }
4984
handleDeleteChangeSurroundingSubMode(const Input & input)4985 bool FakeVimHandler::Private::handleDeleteChangeSurroundingSubMode(const Input &input)
4986 {
4987 if (g.submode != ChangeSurroundingSubMode && g.submode != DeleteSurroundingSubMode)
4988 return false;
4989
4990 bool handled = false;
4991
4992 if (input.is('(') || input.is(')') || input.is('b')) {
4993 handled = selectBlockTextObject(false, '(', ')');
4994 } else if (input.is('{') || input.is('}') || input.is('B')) {
4995 handled = selectBlockTextObject(false, '{', '}');
4996 } else if (input.is('[') || input.is(']')) {
4997 handled = selectBlockTextObject(false, '[', ']');
4998 } else if (input.is('<') || input.is('>') || input.is('t')) {
4999 handled = selectBlockTextObject(false, '<', '>');
5000 } else if (input.is('"') || input.is('\'') || input.is('`')) {
5001 handled = selectQuotedStringTextObject(false, input.asChar());
5002 }
5003
5004 if (handled) {
5005 if (g.submode == DeleteSurroundingSubMode) {
5006 pushUndoState(false);
5007 beginEditBlock();
5008
5009 // Surround is always one character, so just delete the first and last one
5010 transformText(currentRange(), [](const QString &text) {
5011 return text.mid(1, text.size() - 2);
5012 });
5013
5014 endEditBlock();
5015 clearCurrentMode();
5016
5017 g.dotCommand = "ds" + input.asChar();
5018 } else if (g.submode == ChangeSurroundingSubMode) {
5019 g.subsubmode = SurroundSubSubMode;
5020 }
5021 }
5022
5023 return handled;
5024 }
5025
handleAddSurroundingSubMode(const Input & input)5026 bool FakeVimHandler::Private::handleAddSurroundingSubMode(const Input &input)
5027 {
5028 if (!input.is('s'))
5029 return false;
5030
5031 g.subsubmode = SurroundSubSubMode;
5032
5033 int anc = firstPositionInLine(cursorLine() + 1);
5034 const int pos = lastPositionInLine(cursorLine() + 1);
5035
5036 // Ignore leading spaces
5037 while ((characterAt(anc) == ' ' || characterAt(anc) == '\t') && anc != pos) {
5038 anc++;
5039 }
5040
5041 setAnchorAndPosition(anc, pos);
5042
5043 finishMovement("s");
5044
5045 return true;
5046 }
5047
handleFilterSubMode(const Input &)5048 bool FakeVimHandler::Private::handleFilterSubMode(const Input &)
5049 {
5050 return false;
5051 }
5052
handleRegisterSubMode(const Input & input)5053 bool FakeVimHandler::Private::handleRegisterSubMode(const Input &input)
5054 {
5055 bool handled = false;
5056
5057 QChar reg = input.asChar();
5058 if (QString("*+.%#:-\"_").contains(reg) || reg.isLetterOrNumber()) {
5059 m_register = reg.unicode();
5060 handled = true;
5061 }
5062 g.submode = NoSubMode;
5063
5064 return handled;
5065 }
5066
handleShiftSubMode(const Input & input)5067 bool FakeVimHandler::Private::handleShiftSubMode(const Input &input)
5068 {
5069 if (g.submode != indentModeFromInput(input))
5070 return false;
5071
5072 g.movetype = MoveLineWise;
5073 pushUndoState();
5074 moveDown(count() - 1);
5075 setDotCommand(QString("%2%1%1").arg(input.asChar()), count());
5076 finishMovement();
5077 g.submode = NoSubMode;
5078
5079 return true;
5080 }
5081
handleChangeCaseSubMode(const Input & input)5082 bool FakeVimHandler::Private::handleChangeCaseSubMode(const Input &input)
5083 {
5084 if (g.submode != letterCaseModeFromInput(input))
5085 return false;
5086
5087 if (!isFirstNonBlankOnLine(position())) {
5088 moveToStartOfLine();
5089 moveToFirstNonBlankOnLine();
5090 }
5091 setTargetColumn();
5092 pushUndoState();
5093 setAnchor();
5094 setPosition(lastPositionInLine(cursorLine() + count()) + 1);
5095 finishMovement(QString("%1%2").arg(count()).arg(input.raw()));
5096 g.submode = NoSubMode;
5097
5098 return true;
5099 }
5100
handleWindowSubMode(const Input & input)5101 bool FakeVimHandler::Private::handleWindowSubMode(const Input &input)
5102 {
5103 if (handleCount(input))
5104 return true;
5105
5106 leaveVisualMode();
5107 leaveCurrentMode();
5108 q->windowCommandRequested(input.toString(), count());
5109
5110 return true;
5111 }
5112
handleZSubMode(const Input & input)5113 bool FakeVimHandler::Private::handleZSubMode(const Input &input)
5114 {
5115 bool handled = true;
5116 bool foldMaybeClosed = false;
5117 if (input.isReturn() || input.is('t')
5118 || input.is('-') || input.is('b')
5119 || input.is('.') || input.is('z')) {
5120 // Cursor line to top/center/bottom of window.
5121 Qt::AlignmentFlag align;
5122 if (input.isReturn() || input.is('t'))
5123 align = Qt::AlignTop;
5124 else if (input.is('.') || input.is('z'))
5125 align = Qt::AlignVCenter;
5126 else
5127 align = Qt::AlignBottom;
5128 const bool moveToNonBlank = (input.is('.') || input.isReturn() || input.is('-'));
5129 const int line = g.mvcount == 0 ? -1 : firstPositionInLine(count());
5130 alignViewportToCursor(align, line, moveToNonBlank);
5131 } else if (input.is('o') || input.is('c')) {
5132 // Open/close current fold.
5133 foldMaybeClosed = input.is('c');
5134 q->fold(count(), foldMaybeClosed);
5135 } else if (input.is('O') || input.is('C')) {
5136 // Recursively open/close current fold.
5137 foldMaybeClosed = input.is('C');
5138 q->fold(-1, foldMaybeClosed);
5139 } else if (input.is('a') || input.is('A')) {
5140 // Toggle current fold.
5141 foldMaybeClosed = true;
5142 q->foldToggle(input.is('a') ? count() : -1);
5143 } else if (input.is('R') || input.is('M')) {
5144 // Open/close all folds in document.
5145 foldMaybeClosed = input.is('M');
5146 q->foldAll(foldMaybeClosed);
5147 } else if (input.is('j') || input.is('k')) {
5148 q->foldGoTo(input.is('j') ? count() : -count(), false);
5149 } else {
5150 handled = false;
5151 }
5152 if (foldMaybeClosed)
5153 ensureCursorVisible();
5154 g.submode = NoSubMode;
5155 return handled;
5156 }
5157
handleCapitalZSubMode(const Input & input)5158 bool FakeVimHandler::Private::handleCapitalZSubMode(const Input &input)
5159 {
5160 // Recognize ZZ and ZQ as aliases for ":x" and ":q!".
5161 bool handled = true;
5162 if (input.is('Z'))
5163 handleExCommand("x");
5164 else if (input.is('Q'))
5165 handleExCommand("q!");
5166 else
5167 handled = false;
5168 g.submode = NoSubMode;
5169 return handled;
5170 }
5171
handleMacroRecordSubMode(const Input & input)5172 bool FakeVimHandler::Private::handleMacroRecordSubMode(const Input &input)
5173 {
5174 g.submode = NoSubMode;
5175 return startRecording(input);
5176 }
5177
handleMacroExecuteSubMode(const Input & input)5178 bool FakeVimHandler::Private::handleMacroExecuteSubMode(const Input &input)
5179 {
5180 g.submode = NoSubMode;
5181
5182 bool result = true;
5183 int repeat = count();
5184 while (result && --repeat >= 0)
5185 result = executeRegister(input.asChar().unicode());
5186
5187 return result;
5188 }
5189
handleInsertOrReplaceMode(const Input & input)5190 EventResult FakeVimHandler::Private::handleInsertOrReplaceMode(const Input &input)
5191 {
5192 if (position() < m_buffer->insertState.pos1 || position() > m_buffer->insertState.pos2) {
5193 commitInsertState();
5194 invalidateInsertState();
5195 }
5196
5197 if (g.mode == InsertMode)
5198 handleInsertMode(input);
5199 else
5200 handleReplaceMode(input);
5201
5202 if (!m_textedit && !m_plaintextedit)
5203 return EventHandled;
5204
5205 if (!isInsertMode() || m_buffer->breakEditBlock
5206 || position() < m_buffer->insertState.pos1 || position() > m_buffer->insertState.pos2) {
5207 commitInsertState();
5208 invalidateInsertState();
5209 breakEditBlock();
5210 m_visualBlockInsert = NoneBlockInsertMode;
5211 }
5212
5213 // We don't want fancy stuff in insert mode.
5214 return EventHandled;
5215 }
5216
handleReplaceMode(const Input & input)5217 void FakeVimHandler::Private::handleReplaceMode(const Input &input)
5218 {
5219 if (input.isEscape()) {
5220 commitInsertState();
5221 moveLeft(qMin(1, leftDist()));
5222 enterCommandMode();
5223 g.dotCommand.append(m_buffer->lastInsertion + "<ESC>");
5224 } else if (input.isKey(Key_Left)) {
5225 moveLeft();
5226 } else if (input.isKey(Key_Right)) {
5227 moveRight();
5228 } else if (input.isKey(Key_Up)) {
5229 moveUp();
5230 } else if (input.isKey(Key_Down)) {
5231 moveDown();
5232 } else if (input.isKey(Key_Insert)) {
5233 g.mode = InsertMode;
5234 } else if (input.isControl('o')) {
5235 enterCommandMode(ReplaceMode);
5236 } else {
5237 joinPreviousEditBlock();
5238 if (!atEndOfLine()) {
5239 setAnchor();
5240 moveRight();
5241 removeText(currentRange());
5242 }
5243 const QString text = input.text();
5244 setAnchor();
5245 insertText(text);
5246 setTargetColumn();
5247 endEditBlock();
5248 }
5249 }
5250
finishInsertMode()5251 void FakeVimHandler::Private::finishInsertMode()
5252 {
5253 bool newLineAfter = m_buffer->insertState.newLineAfter;
5254 bool newLineBefore = m_buffer->insertState.newLineBefore;
5255
5256 // Repeat insertion [count] times.
5257 // One instance was already physically inserted while typing.
5258 if (!m_buffer->breakEditBlock && isInsertStateValid()) {
5259 commitInsertState();
5260
5261 QString text = m_buffer->lastInsertion;
5262 const QString dotCommand = g.dotCommand;
5263 const int repeat = count() - 1;
5264 m_buffer->lastInsertion.clear();
5265 joinPreviousEditBlock();
5266
5267 if (newLineAfter) {
5268 text.chop(1);
5269 text.prepend("<END>\n");
5270 } else if (newLineBefore) {
5271 text.prepend("<END>");
5272 }
5273
5274 replay(text, repeat);
5275
5276 if (m_visualBlockInsert != NoneBlockInsertMode && !text.contains('\n')) {
5277 const CursorPosition lastAnchor = markLessPosition();
5278 const CursorPosition lastPosition = markGreaterPosition();
5279 const bool change = m_visualBlockInsert == ChangeBlockInsertMode;
5280 const int insertColumn = (m_visualBlockInsert == InsertBlockInsertMode || change)
5281 ? qMin(lastPosition.column, lastAnchor.column)
5282 : qMax(lastPosition.column, lastAnchor.column) + 1;
5283
5284 CursorPosition pos(lastAnchor.line, insertColumn);
5285
5286 if (change)
5287 pos.column = columnAt(m_buffer->insertState.pos1);
5288
5289 // Cursor position after block insert is on the first selected line,
5290 // last selected column for 's' command, otherwise first selected column.
5291 const int endColumn = change ? qMax(0, m_cursor.positionInBlock() - 1)
5292 : qMin(lastPosition.column, lastAnchor.column);
5293
5294 while (pos.line < lastPosition.line) {
5295 ++pos.line;
5296 setCursorPosition(&m_cursor, pos);
5297 if (m_visualBlockInsert == AppendToEndOfLineBlockInsertMode) {
5298 moveToEndOfLine();
5299 } else if (m_visualBlockInsert == AppendBlockInsertMode) {
5300 // Prepend spaces if necessary.
5301 int spaces = pos.column - m_cursor.positionInBlock();
5302 if (spaces > 0) {
5303 setAnchor();
5304 m_cursor.insertText(QString(" ").repeated(spaces));
5305 }
5306 } else if (m_cursor.positionInBlock() != pos.column) {
5307 continue;
5308 }
5309 replay(text, repeat + 1);
5310 }
5311
5312 setCursorPosition(CursorPosition(lastAnchor.line, endColumn));
5313 } else {
5314 moveLeft(qMin(1, leftDist()));
5315 }
5316
5317 endEditBlock();
5318 breakEditBlock();
5319
5320 m_buffer->lastInsertion = text;
5321 g.dotCommand = dotCommand;
5322 } else {
5323 moveLeft(qMin(1, leftDist()));
5324 }
5325
5326 if (newLineBefore || newLineAfter)
5327 m_buffer->lastInsertion.remove(0, m_buffer->lastInsertion.indexOf('\n') + 1);
5328 g.dotCommand.append(m_buffer->lastInsertion + "<ESC>");
5329
5330 setTargetColumn();
5331 enterCommandMode();
5332 }
5333
handleInsertMode(const Input & input)5334 void FakeVimHandler::Private::handleInsertMode(const Input &input)
5335 {
5336 if (input.isEscape()) {
5337 if (g.submode == CtrlRSubMode || g.submode == CtrlVSubMode) {
5338 g.submode = NoSubMode;
5339 g.subsubmode = NoSubSubMode;
5340 updateMiniBuffer();
5341 } else {
5342 finishInsertMode();
5343 }
5344 } else if (g.submode == CtrlRSubMode) {
5345 m_cursor.insertText(registerContents(input.asChar().unicode()));
5346 g.submode = NoSubMode;
5347 } else if (g.submode == CtrlVSubMode) {
5348 if (g.subsubmode == NoSubSubMode) {
5349 g.subsubmode = CtrlVUnicodeSubSubMode;
5350 m_ctrlVAccumulator = 0;
5351 if (input.is('x') || input.is('X')) {
5352 // ^VXnn or ^Vxnn with 00 <= nn <= FF
5353 // BMP Unicode codepoints ^Vunnnn with 0000 <= nnnn <= FFFF
5354 // any Unicode codepoint ^VUnnnnnnnn with 00000000 <= nnnnnnnn <= 7FFFFFFF
5355 // ^Vnnn with 000 <= nnn <= 255
5356 // ^VOnnn or ^Vonnn with 000 <= nnn <= 377
5357 m_ctrlVLength = 2;
5358 m_ctrlVBase = 16;
5359 } else if (input.is('O') || input.is('o')) {
5360 m_ctrlVLength = 3;
5361 m_ctrlVBase = 8;
5362 } else if (input.is('u')) {
5363 m_ctrlVLength = 4;
5364 m_ctrlVBase = 16;
5365 } else if (input.is('U')) {
5366 m_ctrlVLength = 8;
5367 m_ctrlVBase = 16;
5368 } else if (input.isDigit()) {
5369 bool ok;
5370 m_ctrlVAccumulator = input.toInt(&ok, 10);
5371 m_ctrlVLength = 2;
5372 m_ctrlVBase = 10;
5373 } else {
5374 insertInInsertMode(input.raw());
5375 g.submode = NoSubMode;
5376 g.subsubmode = NoSubSubMode;
5377 }
5378 } else {
5379 bool ok;
5380 int current = input.toInt(&ok, m_ctrlVBase);
5381 if (ok)
5382 m_ctrlVAccumulator = m_ctrlVAccumulator * m_ctrlVBase + current;
5383 --m_ctrlVLength;
5384 if (m_ctrlVLength == 0 || !ok) {
5385 QString s;
5386 if (QChar::requiresSurrogates(m_ctrlVAccumulator)) {
5387 s.append(QChar(QChar::highSurrogate(m_ctrlVAccumulator)));
5388 s.append(QChar(QChar::lowSurrogate(m_ctrlVAccumulator)));
5389 } else {
5390 s.append(QChar(m_ctrlVAccumulator));
5391 }
5392 insertInInsertMode(s);
5393 g.submode = NoSubMode;
5394 g.subsubmode = NoSubSubMode;
5395
5396 // Try again without Ctrl-V interpretation.
5397 if (!ok)
5398 handleInsertMode(input);
5399 }
5400 }
5401 } else if (input.isControl('o')) {
5402 enterCommandMode(InsertMode);
5403 } else if (input.isControl('v')) {
5404 g.submode = CtrlVSubMode;
5405 g.subsubmode = NoSubSubMode;
5406 updateMiniBuffer();
5407 } else if (input.isControl('r')) {
5408 g.submode = CtrlRSubMode;
5409 g.subsubmode = NoSubSubMode;
5410 updateMiniBuffer();
5411 } else if (input.isControl('w')) {
5412 const int blockNumber = m_cursor.blockNumber();
5413 const int endPos = position();
5414 moveToNextWordStart(1, false, false);
5415 if (blockNumber != m_cursor.blockNumber())
5416 moveToEndOfLine();
5417 const int beginPos = position();
5418 Range range(beginPos, endPos, RangeCharMode);
5419 removeText(range);
5420 } else if (input.isControl('u')) {
5421 const int blockNumber = m_cursor.blockNumber();
5422 const int endPos = position();
5423 moveToStartOfLine();
5424 if (blockNumber != m_cursor.blockNumber())
5425 moveToEndOfLine();
5426 const int beginPos = position();
5427 Range range(beginPos, endPos, RangeCharMode);
5428 removeText(range);
5429 } else if (input.isKey(Key_Insert)) {
5430 g.mode = ReplaceMode;
5431 } else if (input.isKey(Key_Left)) {
5432 moveLeft();
5433 } else if (input.isShift(Key_Left) || input.isControl(Key_Left)) {
5434 moveToNextWordStart(1, false, false);
5435 } else if (input.isKey(Key_Down)) {
5436 g.submode = NoSubMode;
5437 moveDown();
5438 } else if (input.isKey(Key_Up)) {
5439 g.submode = NoSubMode;
5440 moveUp();
5441 } else if (input.isKey(Key_Right)) {
5442 moveRight();
5443 } else if (input.isShift(Key_Right) || input.isControl(Key_Right)) {
5444 moveToNextWordStart(1, false, true);
5445 } else if (input.isKey(Key_Home)) {
5446 moveToStartOfLine();
5447 } else if (input.isKey(Key_End)) {
5448 moveBehindEndOfLine();
5449 m_targetColumn = -1;
5450 } else if (input.isReturn() || input.isControl('j') || input.isControl('m')) {
5451 if (!input.isReturn() || !handleInsertInEditor(input)) {
5452 joinPreviousEditBlock();
5453 g.submode = NoSubMode;
5454 insertNewLine();
5455 endEditBlock();
5456 }
5457 } else if (input.isBackspace()) {
5458 // pass C-h as backspace, too
5459 if (!handleInsertInEditor(Input(Qt::Key_Backspace, Qt::NoModifier))) {
5460 joinPreviousEditBlock();
5461 if (!m_buffer->lastInsertion.isEmpty()
5462 || s.backspace.value().contains("start")
5463 || s.backspace.value().contains("2")) {
5464 const int line = cursorLine() + 1;
5465 const Column col = cursorColumn();
5466 QString data = lineContents(line);
5467 const Column ind = indentation(data);
5468 if (col.logical <= ind.logical && col.logical
5469 && startsWithWhitespace(data, col.physical)) {
5470 const int ts = s.tabStop.value();
5471 const int newl = col.logical - 1 - (col.logical - 1) % ts;
5472 const QString prefix = tabExpand(newl);
5473 setLineContents(line, prefix + data.mid(col.physical));
5474 moveToStartOfLine();
5475 moveRight(prefix.size());
5476 } else {
5477 setAnchor();
5478 m_cursor.deletePreviousChar();
5479 }
5480 }
5481 endEditBlock();
5482 }
5483 } else if (input.isKey(Key_Delete)) {
5484 if (!handleInsertInEditor(input)) {
5485 joinPreviousEditBlock();
5486 m_cursor.deleteChar();
5487 endEditBlock();
5488 }
5489 } else if (input.isKey(Key_PageDown) || input.isControl('f')) {
5490 movePageDown();
5491 } else if (input.isKey(Key_PageUp) || input.isControl('b')) {
5492 movePageUp();
5493 } else if (input.isKey(Key_Tab)) {
5494 m_buffer->insertState.insertingSpaces = true;
5495 if (s.expandTab.value()) {
5496 const int ts = s.tabStop.value();
5497 const int col = logicalCursorColumn();
5498 QString str = QString(ts - col % ts, ' ');
5499 insertText(str);
5500 } else {
5501 insertInInsertMode(input.raw());
5502 }
5503 m_buffer->insertState.insertingSpaces = false;
5504 } else if (input.isControl('d')) {
5505 // remove one level of indentation from the current line
5506 const int shift = s.shiftWidth.value();
5507 const int tab = s.tabStop.value();
5508 int line = cursorLine() + 1;
5509 int pos = firstPositionInLine(line);
5510 QString text = lineContents(line);
5511 int amount = 0;
5512 int i = 0;
5513 for (; i < text.size() && amount < shift; ++i) {
5514 if (text.at(i) == ' ')
5515 ++amount;
5516 else if (text.at(i) == '\t')
5517 amount += tab; // FIXME: take position into consideration
5518 else
5519 break;
5520 }
5521 removeText(Range(pos, pos+i));
5522 } else if (input.isControl('p') || input.isControl('n')) {
5523 QTextCursor tc = m_cursor;
5524 moveToNextWordStart(1, false, false);
5525 QString str = selectText(Range(position(), tc.position()));
5526 m_cursor = tc;
5527 q->simpleCompletionRequested(str, input.isControl('n'));
5528 } else if (input.isShift(Qt::Key_Insert)) {
5529 // Insert text from clipboard.
5530 QClipboard *clipboard = QApplication::clipboard();
5531 const QMimeData *data = clipboard->mimeData();
5532 if (data && data->hasText())
5533 insertInInsertMode(data->text());
5534 } else {
5535 m_buffer->insertState.insertingSpaces = input.isKey(Key_Space);
5536 if (!handleInsertInEditor(input)) {
5537 const QString toInsert = input.text();
5538 if (toInsert.isEmpty())
5539 return;
5540 insertInInsertMode(toInsert);
5541 }
5542 m_buffer->insertState.insertingSpaces = false;
5543 }
5544 }
5545
insertInInsertMode(const QString & text)5546 void FakeVimHandler::Private::insertInInsertMode(const QString &text)
5547 {
5548 joinPreviousEditBlock();
5549 insertText(text);
5550 if (s.smartIndent.value() && isElectricCharacter(text.at(0))) {
5551 const QString leftText = block().text()
5552 .left(position() - 1 - block().position());
5553 if (leftText.simplified().isEmpty()) {
5554 Range range(position(), position(), g.rangemode);
5555 indentText(range, text.at(0));
5556 }
5557 }
5558 setTargetColumn();
5559 endEditBlock();
5560 g.submode = NoSubMode;
5561 }
5562
startRecording(const Input & input)5563 bool FakeVimHandler::Private::startRecording(const Input &input)
5564 {
5565 QChar reg = input.asChar();
5566 if (reg == '"' || reg.isLetterOrNumber()) {
5567 g.currentRegister = reg.unicode();
5568 g.isRecording = true;
5569 g.recorded.clear();
5570 return true;
5571 }
5572
5573 return false;
5574 }
5575
record(const Input & input)5576 void FakeVimHandler::Private::record(const Input &input)
5577 {
5578 if (g.isRecording)
5579 g.recorded.append(input.toString());
5580 }
5581
stopRecording()5582 void FakeVimHandler::Private::stopRecording()
5583 {
5584 // Remove q from end (stop recording command).
5585 g.isRecording = false;
5586 g.recorded.chop(1);
5587 setRegister(g.currentRegister, g.recorded, g.rangemode);
5588 g.currentRegister = 0;
5589 g.recorded.clear();
5590 }
5591
handleAs(const QString & command)5592 void FakeVimHandler::Private::handleAs(const QString &command)
5593 {
5594 QString cmd = QString("\"%1").arg(QChar(m_register));
5595
5596 if (command.contains("%1"))
5597 cmd.append(command.arg(count()));
5598 else
5599 cmd.append(command);
5600
5601 leaveVisualMode();
5602 beginLargeEditBlock();
5603 replay(cmd);
5604 endEditBlock();
5605 }
5606
executeRegister(int reg)5607 bool FakeVimHandler::Private::executeRegister(int reg)
5608 {
5609 QChar regChar(reg);
5610
5611 // TODO: Prompt for an expression to execute if register is '='.
5612 if (reg == '@' && g.lastExecutedRegister != 0)
5613 reg = g.lastExecutedRegister;
5614 else if (QString("\".*+").contains(regChar) || regChar.isLetterOrNumber())
5615 g.lastExecutedRegister = reg;
5616 else
5617 return false;
5618
5619 // FIXME: In Vim it's possible to interrupt recursive macro with <C-c>.
5620 // One solution may be to call QApplication::processEvents() and check if <C-c> was
5621 // used when a mapping is active.
5622 // According to Vim, register is executed like mapping.
5623 prependMapping(Inputs(registerContents(reg), false, false));
5624
5625 return true;
5626 }
5627
handleExMode(const Input & input)5628 EventResult FakeVimHandler::Private::handleExMode(const Input &input)
5629 {
5630 // handle C-R, C-R C-W, C-R {register}
5631 if (handleCommandBufferPaste(input))
5632 return EventHandled;
5633
5634 if (input.isEscape()) {
5635 g.commandBuffer.clear();
5636 leaveCurrentMode();
5637 g.submode = NoSubMode;
5638 } else if (g.submode == CtrlVSubMode) {
5639 g.commandBuffer.insertChar(input.raw());
5640 g.submode = NoSubMode;
5641 } else if (input.isControl('v')) {
5642 g.submode = CtrlVSubMode;
5643 g.subsubmode = NoSubSubMode;
5644 return EventHandled;
5645 } else if (input.isBackspace()) {
5646 if (g.commandBuffer.isEmpty()) {
5647 leaveVisualMode();
5648 leaveCurrentMode();
5649 } else if (g.commandBuffer.hasSelection()) {
5650 g.commandBuffer.deleteSelected();
5651 } else {
5652 g.commandBuffer.deleteChar();
5653 }
5654 } else if (input.isKey(Key_Tab)) {
5655 // FIXME: Complete actual commands.
5656 g.commandBuffer.historyUp();
5657 } else if (input.isReturn()) {
5658 showMessage(MessageCommand, g.commandBuffer.display());
5659 handleExCommand(g.commandBuffer.contents());
5660 g.commandBuffer.clear();
5661 } else if (!g.commandBuffer.handleInput(input)) {
5662 qDebug() << "IGNORED IN EX-MODE: " << input.key() << input.text();
5663 return EventUnhandled;
5664 }
5665
5666 return EventHandled;
5667 }
5668
handleSearchSubSubMode(const Input & input)5669 EventResult FakeVimHandler::Private::handleSearchSubSubMode(const Input &input)
5670 {
5671 EventResult handled = EventHandled;
5672
5673 // handle C-R, C-R C-W, C-R {register}
5674 if (handleCommandBufferPaste(input))
5675 return handled;
5676
5677 if (input.isEscape()) {
5678 g.currentMessage.clear();
5679 setPosition(m_searchStartPosition);
5680 scrollToLine(m_searchFromScreenLine);
5681 } else if (input.isBackspace()) {
5682 if (g.searchBuffer.isEmpty())
5683 leaveCurrentMode();
5684 else if (g.searchBuffer.hasSelection())
5685 g.searchBuffer.deleteSelected();
5686 else
5687 g.searchBuffer.deleteChar();
5688 } else if (input.isReturn()) {
5689 const QString &needle = g.searchBuffer.contents();
5690 if (!needle.isEmpty())
5691 g.lastSearch = needle;
5692 else
5693 g.searchBuffer.setContents(g.lastSearch);
5694
5695 updateFind(true);
5696
5697 if (finishSearch()) {
5698 if (g.submode != NoSubMode)
5699 finishMovement(g.searchBuffer.prompt() + g.lastSearch + '\n');
5700 if (g.currentMessage.isEmpty())
5701 showMessage(MessageCommand, g.searchBuffer.display());
5702 } else {
5703 handled = EventCancelled; // Not found so cancel mapping if any.
5704 }
5705 } else if (input.isKey(Key_Tab)) {
5706 g.searchBuffer.insertChar(QChar(9));
5707 } else if (!g.searchBuffer.handleInput(input)) {
5708 //qDebug() << "IGNORED IN SEARCH MODE: " << input.key() << input.text();
5709 return EventUnhandled;
5710 }
5711
5712 if (input.isReturn() || input.isEscape()) {
5713 g.searchBuffer.clear();
5714 leaveCurrentMode();
5715 } else {
5716 updateFind(false);
5717 }
5718
5719 return handled;
5720 }
5721
5722 // This uses 0 based line counting (hidden lines included).
parseLineAddress(QString * cmd)5723 int FakeVimHandler::Private::parseLineAddress(QString *cmd)
5724 {
5725 //qDebug() << "CMD: " << cmd;
5726 if (cmd->isEmpty())
5727 return -1;
5728
5729 int result = -1;
5730 QChar c = cmd->at(0);
5731 if (c == '.') { // current line
5732 result = cursorBlockNumber();
5733 cmd->remove(0, 1);
5734 } else if (c == '$') { // last line
5735 result = document()->blockCount() - 1;
5736 cmd->remove(0, 1);
5737 } else if (c == '\'') { // mark
5738 cmd->remove(0, 1);
5739 if (cmd->isEmpty()) {
5740 showMessage(MessageError, msgMarkNotSet(QString()));
5741 return -1;
5742 }
5743 c = cmd->at(0);
5744 Mark m = mark(c);
5745 if (!m.isValid() || !m.isLocal(m_currentFileName)) {
5746 showMessage(MessageError, msgMarkNotSet(c));
5747 return -1;
5748 }
5749 cmd->remove(0, 1);
5750 result = m.position(document()).line;
5751 } else if (c.isDigit()) { // line with given number
5752 result = 0;
5753 } else if (c == '-' || c == '+') { // add or subtract from current line number
5754 result = cursorBlockNumber();
5755 } else if (c == '/' || c == '?'
5756 || (c == '\\' && cmd->size() > 1 && QString("/?&").contains(cmd->at(1)))) {
5757 // search for expression
5758 SearchData sd;
5759 if (c == '/' || c == '?') {
5760 const int end = findUnescaped(c, *cmd, 1);
5761 if (end == -1)
5762 return -1;
5763 sd.needle = cmd->mid(1, end - 1);
5764 cmd->remove(0, end + 1);
5765 } else {
5766 c = cmd->at(1);
5767 cmd->remove(0, 2);
5768 sd.needle = (c == '&') ? g.lastSubstitutePattern : g.lastSearch;
5769 }
5770 sd.forward = (c != '?');
5771 const QTextBlock b = block();
5772 const int pos = b.position() + (sd.forward ? b.length() - 1 : 0);
5773 QTextCursor tc = search(sd, pos, 1, true);
5774 g.lastSearch = sd.needle;
5775 if (tc.isNull())
5776 return -1;
5777 result = tc.block().blockNumber();
5778 } else {
5779 return cursorBlockNumber();
5780 }
5781
5782 // basic arithmetic ("-3+5" or "++" means "+2" etc.)
5783 int n = 0;
5784 bool add = true;
5785 int i = 0;
5786 for (; i < cmd->size(); ++i) {
5787 c = cmd->at(i);
5788 if (c == '-' || c == '+') {
5789 if (n != 0)
5790 result = result + (add ? n - 1 : -(n - 1));
5791 add = c == '+';
5792 result = result + (add ? 1 : -1);
5793 n = 0;
5794 } else if (c.isDigit()) {
5795 n = n * 10 + c.digitValue();
5796 } else if (!c.isSpace()) {
5797 break;
5798 }
5799 }
5800 if (n != 0)
5801 result = result + (add ? n - 1 : -(n - 1));
5802 *cmd = cmd->mid(i).trimmed();
5803
5804 return result;
5805 }
5806
setCurrentRange(const Range & range)5807 void FakeVimHandler::Private::setCurrentRange(const Range &range)
5808 {
5809 setAnchorAndPosition(range.beginPos, range.endPos);
5810 g.rangemode = range.rangemode;
5811 }
5812
parseExCommand(QString * line,ExCommand * cmd)5813 bool FakeVimHandler::Private::parseExCommand(QString *line, ExCommand *cmd)
5814 {
5815 *cmd = ExCommand();
5816 if (line->isEmpty())
5817 return false;
5818
5819 // parse range first
5820 if (!parseLineRange(line, cmd))
5821 return false;
5822
5823 // get first command from command line
5824 QChar close;
5825 bool subst = false;
5826 int i = 0;
5827 for (; i < line->size(); ++i) {
5828 const QChar &c = line->at(i);
5829 if (c == '\\') {
5830 ++i; // skip escaped character
5831 } else if (close.isNull()) {
5832 if (c == '|') {
5833 // split on |
5834 break;
5835 } else if (c == '/') {
5836 subst = i > 0 && (line->at(i - 1) == 's');
5837 close = c;
5838 } else if (c == '"' || c == '\'') {
5839 close = c;
5840 }
5841 } else if (c == close) {
5842 if (subst)
5843 subst = false;
5844 else
5845 close = QChar();
5846 }
5847 }
5848
5849 cmd->cmd = line->mid(0, i).trimmed();
5850
5851 // command arguments starts with first non-letter character
5852 cmd->args = cmd->cmd.section(QRegularExpression("(?=[^a-zA-Z])"), 1);
5853 if (!cmd->args.isEmpty()) {
5854 cmd->cmd.chop(cmd->args.size());
5855 cmd->args = cmd->args.trimmed();
5856
5857 // '!' at the end of command
5858 cmd->hasBang = cmd->args.startsWith('!');
5859 if (cmd->hasBang)
5860 cmd->args = cmd->args.mid(1).trimmed();
5861 }
5862
5863 // remove the first command from command line
5864 line->remove(0, i + 1);
5865
5866 return true;
5867 }
5868
parseLineRange(QString * line,ExCommand * cmd)5869 bool FakeVimHandler::Private::parseLineRange(QString *line, ExCommand *cmd)
5870 {
5871 // remove leading colons and spaces
5872 line->remove(QRegularExpression("^\\s*(:+\\s*)*"));
5873
5874 // special case ':!...' (use invalid range)
5875 if (line->startsWith('!')) {
5876 cmd->range = Range();
5877 return true;
5878 }
5879
5880 // FIXME: that seems to be different for %w and %s
5881 if (line->startsWith('%'))
5882 line->replace(0, 1, "1,$");
5883
5884 int beginLine = parseLineAddress(line);
5885 int endLine;
5886 if (line->startsWith(',')) {
5887 *line = line->mid(1).trimmed();
5888 endLine = parseLineAddress(line);
5889 } else {
5890 endLine = beginLine;
5891 }
5892 if (beginLine == -1 || endLine == -1)
5893 return false;
5894
5895 const int beginPos = firstPositionInLine(qMin(beginLine, endLine) + 1, false);
5896 const int endPos = lastPositionInLine(qMax(beginLine, endLine) + 1, false);
5897 cmd->range = Range(beginPos, endPos, RangeLineMode);
5898 cmd->count = beginLine;
5899
5900 return true;
5901 }
5902
parseRangeCount(const QString & line,Range * range) const5903 void FakeVimHandler::Private::parseRangeCount(const QString &line, Range *range) const
5904 {
5905 bool ok;
5906 const int count = qAbs(line.trimmed().toInt(&ok));
5907 if (ok) {
5908 const int beginLine = blockAt(range->endPos).blockNumber() + 1;
5909 const int endLine = qMin(beginLine + count - 1, document()->blockCount());
5910 range->beginPos = firstPositionInLine(beginLine, false);
5911 range->endPos = lastPositionInLine(endLine, false);
5912 }
5913 }
5914
5915 // use handleExCommand for invoking commands that might move the cursor
handleCommand(const QString & cmd)5916 void FakeVimHandler::Private::handleCommand(const QString &cmd)
5917 {
5918 handleExCommand(cmd);
5919 }
5920
handleExSubstituteCommand(const ExCommand & cmd)5921 bool FakeVimHandler::Private::handleExSubstituteCommand(const ExCommand &cmd)
5922 {
5923 // :substitute
5924 if (!cmd.matches("s", "substitute")
5925 && !(cmd.cmd.isEmpty() && !cmd.args.isEmpty() && QString("&~").contains(cmd.args[0]))) {
5926 return false;
5927 }
5928
5929 int count = 1;
5930 QString line = cmd.args;
5931 const QRegularExpressionMatch match = QRegularExpression("\\d+$").match(line);
5932 if (match.hasMatch()) {
5933 count = match.captured().toInt();
5934 line = line.left(match.capturedStart()).trimmed();
5935 }
5936
5937 if (cmd.cmd.isEmpty()) {
5938 // keep previous substitution flags on '&&' and '~&'
5939 if (line.size() > 1 && line[1] == '&')
5940 g.lastSubstituteFlags += line.mid(2);
5941 else
5942 g.lastSubstituteFlags = line.mid(1);
5943 if (line[0] == '~')
5944 g.lastSubstitutePattern = g.lastSearch;
5945 } else {
5946 if (line.isEmpty()) {
5947 g.lastSubstituteFlags.clear();
5948 } else {
5949 // we have /{pattern}/{string}/[flags] now
5950 const QChar separator = line.at(0);
5951 int pos1 = findUnescaped(separator, line, 1);
5952 if (pos1 == -1)
5953 return false;
5954 int pos2 = findUnescaped(separator, line, pos1 + 1);
5955 if (pos2 == -1)
5956 pos2 = line.size();
5957
5958 g.lastSubstitutePattern = line.mid(1, pos1 - 1);
5959 g.lastSubstituteReplacement = line.mid(pos1 + 1, pos2 - pos1 - 1);
5960 g.lastSubstituteFlags = line.mid(pos2 + 1);
5961 }
5962 }
5963
5964 count = qMax(1, count);
5965 QString needle = g.lastSubstitutePattern;
5966
5967 if (g.lastSubstituteFlags.contains('i'))
5968 needle.prepend("\\c");
5969
5970 const QRegularExpression pattern = vimPatternToQtPattern(needle);
5971
5972 QTextBlock lastBlock;
5973 QTextBlock firstBlock;
5974 const bool global = g.lastSubstituteFlags.contains('g');
5975 for (int a = 0; a != count; ++a) {
5976 for (QTextBlock block = blockAt(cmd.range.endPos);
5977 block.isValid() && block.position() + block.length() > cmd.range.beginPos;
5978 block = block.previous()) {
5979 QString text = block.text();
5980 if (substituteText(&text, pattern, g.lastSubstituteReplacement, global)) {
5981 firstBlock = block;
5982 if (!lastBlock.isValid()) {
5983 lastBlock = block;
5984 beginEditBlock();
5985 }
5986 QTextCursor tc = m_cursor;
5987 const int pos = block.position();
5988 const int anchor = pos + block.length() - 1;
5989 tc.setPosition(anchor);
5990 tc.setPosition(pos, KeepAnchor);
5991 tc.insertText(text);
5992 }
5993 }
5994 }
5995
5996 if (lastBlock.isValid()) {
5997 m_buffer->undoState.position = CursorPosition(firstBlock.blockNumber(), 0);
5998
5999 leaveVisualMode();
6000 setPosition(lastBlock.position());
6001 setAnchor();
6002 moveToFirstNonBlankOnLine();
6003
6004 endEditBlock();
6005 }
6006
6007 return true;
6008 }
6009
handleExTabNextCommand(const ExCommand & cmd)6010 bool FakeVimHandler::Private::handleExTabNextCommand(const ExCommand &cmd)
6011 {
6012 if (!cmd.matches("tabn", "tabnext"))
6013 return false;
6014
6015 q->tabNextRequested();
6016 return true;
6017 }
6018
handleExTabPreviousCommand(const ExCommand & cmd)6019 bool FakeVimHandler::Private::handleExTabPreviousCommand(const ExCommand &cmd)
6020 {
6021 if (!cmd.matches("tabp", "tabprevious"))
6022 return false;
6023
6024 q->tabPreviousRequested();
6025 return true;
6026 }
6027
handleExMapCommand(const ExCommand & cmd0)6028 bool FakeVimHandler::Private::handleExMapCommand(const ExCommand &cmd0) // :map
6029 {
6030 QByteArray modes;
6031 enum Type { Map, Noremap, Unmap } type;
6032
6033 QByteArray cmd = cmd0.cmd.toLatin1();
6034
6035 // Strange formatting. But everything else is even uglier.
6036 if (cmd == "map") { modes = "nvo"; type = Map; } else
6037 if (cmd == "nm" || cmd == "nmap") { modes = "n"; type = Map; } else
6038 if (cmd == "vm" || cmd == "vmap") { modes = "v"; type = Map; } else
6039 if (cmd == "xm" || cmd == "xmap") { modes = "x"; type = Map; } else
6040 if (cmd == "smap") { modes = "s"; type = Map; } else
6041 if (cmd == "omap") { modes = "o"; type = Map; } else
6042 if (cmd == "map!") { modes = "ic"; type = Map; } else
6043 if (cmd == "im" || cmd == "imap") { modes = "i"; type = Map; } else
6044 if (cmd == "lm" || cmd == "lmap") { modes = "l"; type = Map; } else
6045 if (cmd == "cm" || cmd == "cmap") { modes = "c"; type = Map; } else
6046
6047 if (cmd == "no" || cmd == "noremap") { modes = "nvo"; type = Noremap; } else
6048 if (cmd == "nn" || cmd == "nnoremap") { modes = "n"; type = Noremap; } else
6049 if (cmd == "vn" || cmd == "vnoremap") { modes = "v"; type = Noremap; } else
6050 if (cmd == "xn" || cmd == "xnoremap") { modes = "x"; type = Noremap; } else
6051 if (cmd == "snor" || cmd == "snoremap") { modes = "s"; type = Noremap; } else
6052 if (cmd == "ono" || cmd == "onoremap") { modes = "o"; type = Noremap; } else
6053 if (cmd == "no!" || cmd == "noremap!") { modes = "ic"; type = Noremap; } else
6054 if (cmd == "ino" || cmd == "inoremap") { modes = "i"; type = Noremap; } else
6055 if (cmd == "ln" || cmd == "lnoremap") { modes = "l"; type = Noremap; } else
6056 if (cmd == "cno" || cmd == "cnoremap") { modes = "c"; type = Noremap; } else
6057
6058 if (cmd == "unm" || cmd == "unmap") { modes = "nvo"; type = Unmap; } else
6059 if (cmd == "nun" || cmd == "nunmap") { modes = "n"; type = Unmap; } else
6060 if (cmd == "vu" || cmd == "vunmap") { modes = "v"; type = Unmap; } else
6061 if (cmd == "xu" || cmd == "xunmap") { modes = "x"; type = Unmap; } else
6062 if (cmd == "sunm" || cmd == "sunmap") { modes = "s"; type = Unmap; } else
6063 if (cmd == "ou" || cmd == "ounmap") { modes = "o"; type = Unmap; } else
6064 if (cmd == "unm!" || cmd == "unmap!") { modes = "ic"; type = Unmap; } else
6065 if (cmd == "iu" || cmd == "iunmap") { modes = "i"; type = Unmap; } else
6066 if (cmd == "lu" || cmd == "lunmap") { modes = "l"; type = Unmap; } else
6067 if (cmd == "cu" || cmd == "cunmap") { modes = "c"; type = Unmap; }
6068
6069 else
6070 return false;
6071
6072 QString args = cmd0.args;
6073 bool silent = false;
6074 bool unique = false;
6075 forever {
6076 if (eatString("<silent>", &args)) {
6077 silent = true;
6078 } else if (eatString("<unique>", &args)) {
6079 continue;
6080 } else if (eatString("<special>", &args)) {
6081 continue;
6082 } else if (eatString("<buffer>", &args)) {
6083 notImplementedYet();
6084 continue;
6085 } else if (eatString("<script>", &args)) {
6086 notImplementedYet();
6087 continue;
6088 } else if (eatString("<expr>", &args)) {
6089 notImplementedYet();
6090 return true;
6091 }
6092 break;
6093 }
6094
6095 const QString lhs = args.section(QRegularExpression("\\s+"), 0, 0);
6096 const QString rhs = args.section(QRegularExpression("\\s+"), 1);
6097 if ((rhs.isNull() && type != Unmap) || (!rhs.isNull() && type == Unmap)) {
6098 // FIXME: Dump mappings here.
6099 //qDebug() << g.mappings;
6100 return true;
6101 }
6102
6103 Inputs key(lhs);
6104 //qDebug() << "MAPPING: " << modes << lhs << rhs;
6105 switch (type) {
6106 case Unmap:
6107 foreach (char c, modes)
6108 MappingsIterator(&g.mappings, c, key).remove();
6109 break;
6110 case Map: Q_FALLTHROUGH();
6111 case Noremap: {
6112 Inputs inputs(rhs, type == Noremap, silent);
6113 foreach (char c, modes)
6114 MappingsIterator(&g.mappings, c).setInputs(key, inputs, unique);
6115 break;
6116 }
6117 }
6118 return true;
6119 }
6120
handleExHistoryCommand(const ExCommand & cmd)6121 bool FakeVimHandler::Private::handleExHistoryCommand(const ExCommand &cmd)
6122 {
6123 // :his[tory]
6124 if (!cmd.matches("his", "history"))
6125 return false;
6126
6127 if (cmd.args.isEmpty()) {
6128 QString info;
6129 info += "# command history\n";
6130 int i = 0;
6131 foreach (const QString &item, g.commandBuffer.historyItems()) {
6132 ++i;
6133 info += QString("%1 %2\n").arg(i, -8).arg(item);
6134 }
6135 q->extraInformationChanged(info);
6136 } else {
6137 notImplementedYet();
6138 }
6139
6140 return true;
6141 }
6142
handleExRegisterCommand(const ExCommand & cmd)6143 bool FakeVimHandler::Private::handleExRegisterCommand(const ExCommand &cmd)
6144 {
6145 // :reg[isters] and :di[splay]
6146 if (!cmd.matches("reg", "registers") && !cmd.matches("di", "display"))
6147 return false;
6148
6149 QByteArray regs = cmd.args.toLatin1();
6150 if (regs.isEmpty()) {
6151 regs = "\"0123456789";
6152 for (auto it = g.registers.cbegin(), end = g.registers.cend(); it != end; ++it) {
6153 if (it.key() > '9')
6154 regs += char(it.key());
6155 }
6156 }
6157 QString info;
6158 info += "--- Registers ---\n";
6159 for (char reg : qAsConst(regs)) {
6160 QString value = quoteUnprintable(registerContents(reg));
6161 info += QString("\"%1 %2\n").arg(reg).arg(value);
6162 }
6163 q->extraInformationChanged(info);
6164
6165 return true;
6166 }
6167
handleExSetCommand(const ExCommand & cmd)6168 bool FakeVimHandler::Private::handleExSetCommand(const ExCommand &cmd)
6169 {
6170 // :se[t]
6171 if (!cmd.matches("se", "set"))
6172 return false;
6173
6174 clearMessage();
6175
6176 if (cmd.args.contains('=')) {
6177 // Non-boolean config to set.
6178 int p = cmd.args.indexOf('=');
6179 QString error = s.trySetValue(cmd.args.left(p), cmd.args.mid(p + 1));
6180 if (!error.isEmpty())
6181 showMessage(MessageError, error);
6182 } else {
6183 QString optionName = cmd.args;
6184
6185 bool toggleOption = optionName.endsWith('!');
6186 bool printOption = !toggleOption && optionName.endsWith('?');
6187 if (printOption || toggleOption)
6188 optionName.chop(1);
6189
6190 bool negateOption = optionName.startsWith("no");
6191 if (negateOption)
6192 optionName.remove(0, 2);
6193
6194 FvBaseAspect *act = s.item(optionName);
6195 if (!act) {
6196 showMessage(MessageError, Tr::tr("Unknown option:") + ' ' + cmd.args);
6197 } else if (act->defaultValue().type() == QVariant::Bool) {
6198 bool oldValue = act->value().toBool();
6199 if (printOption) {
6200 showMessage(MessageInfo, QLatin1String(oldValue ? "" : "no")
6201 + act->settingsKey().toLower());
6202 } else if (toggleOption || negateOption == oldValue) {
6203 act->setValue(!oldValue);
6204 }
6205 } else if (negateOption && !printOption) {
6206 showMessage(MessageError, Tr::tr("Invalid argument:") + ' ' + cmd.args);
6207 } else if (toggleOption) {
6208 showMessage(MessageError, Tr::tr("Trailing characters:") + ' ' + cmd.args);
6209 } else {
6210 showMessage(MessageInfo, act->settingsKey().toLower() + "="
6211 + act->value().toString());
6212 }
6213 }
6214 updateEditor();
6215 updateHighlights();
6216 return true;
6217 }
6218
handleExNormalCommand(const ExCommand & cmd)6219 bool FakeVimHandler::Private::handleExNormalCommand(const ExCommand &cmd)
6220 {
6221 // :norm[al]
6222 if (!cmd.matches("norm", "normal"))
6223 return false;
6224 //qDebug() << "REPLAY NORMAL: " << quoteUnprintable(reNormal.cap(3));
6225 replay(cmd.args);
6226 return true;
6227 }
6228
handleExYankDeleteCommand(const ExCommand & cmd)6229 bool FakeVimHandler::Private::handleExYankDeleteCommand(const ExCommand &cmd)
6230 {
6231 // :[range]d[elete] [x] [count]
6232 // :[range]y[ank] [x] [count]
6233 const bool remove = cmd.matches("d", "delete");
6234 if (!remove && !cmd.matches("y", "yank"))
6235 return false;
6236
6237 // get register from arguments
6238 const bool hasRegisterArg = !cmd.args.isEmpty() && !cmd.args.at(0).isDigit();
6239 const int r = hasRegisterArg ? cmd.args.at(0).unicode() : m_register;
6240
6241 // get [count] from arguments
6242 Range range = cmd.range;
6243 parseRangeCount(cmd.args.mid(hasRegisterArg ? 1 : 0).trimmed(), &range);
6244
6245 yankText(range, r);
6246
6247 if (remove) {
6248 leaveVisualMode();
6249 setPosition(range.beginPos);
6250 pushUndoState();
6251 setCurrentRange(range);
6252 removeText(currentRange());
6253 }
6254
6255 return true;
6256 }
6257
handleExChangeCommand(const ExCommand & cmd)6258 bool FakeVimHandler::Private::handleExChangeCommand(const ExCommand &cmd)
6259 {
6260 // :[range]c[hange]
6261 if (!cmd.matches("c", "change"))
6262 return false;
6263
6264 Range range = cmd.range;
6265 range.rangemode = RangeLineModeExclusive;
6266 removeText(range);
6267 insertAutomaticIndentation(true, cmd.hasBang);
6268
6269 // FIXME: In Vim same or less number of lines can be inserted and position after insertion is
6270 // beginning of last inserted line.
6271 enterInsertMode();
6272
6273 return true;
6274 }
6275
handleExMoveCommand(const ExCommand & cmd)6276 bool FakeVimHandler::Private::handleExMoveCommand(const ExCommand &cmd)
6277 {
6278 // :[range]m[ove] {address}
6279 if (!cmd.matches("m", "move"))
6280 return false;
6281
6282 QString lineCode = cmd.args;
6283
6284 const int startLine = blockAt(cmd.range.beginPos).blockNumber();
6285 const int endLine = blockAt(cmd.range.endPos).blockNumber();
6286 const int lines = endLine - startLine + 1;
6287
6288 int targetLine = lineCode == "0" ? -1 : parseLineAddress(&lineCode);
6289 if (targetLine >= startLine && targetLine < endLine) {
6290 showMessage(MessageError, Tr::tr("Move lines into themselves."));
6291 return true;
6292 }
6293
6294 CursorPosition lastAnchor = markLessPosition();
6295 CursorPosition lastPosition = markGreaterPosition();
6296
6297 recordJump();
6298 setPosition(cmd.range.beginPos);
6299 pushUndoState();
6300
6301 setCurrentRange(cmd.range);
6302 QString text = selectText(cmd.range);
6303 removeText(currentRange());
6304
6305 const bool insertAtEnd = targetLine == document()->blockCount();
6306 if (targetLine >= startLine)
6307 targetLine -= lines;
6308 QTextBlock block = document()->findBlockByNumber(insertAtEnd ? targetLine : targetLine + 1);
6309 setPosition(block.position());
6310 setAnchor();
6311
6312 if (insertAtEnd) {
6313 moveBehindEndOfLine();
6314 text.chop(1);
6315 insertText(QString("\n"));
6316 }
6317 insertText(text);
6318
6319 if (!insertAtEnd)
6320 moveUp(1);
6321 if (s.startOfLine.value())
6322 moveToFirstNonBlankOnLine();
6323
6324 if (lastAnchor.line >= startLine && lastAnchor.line <= endLine)
6325 lastAnchor.line += targetLine - startLine + 1;
6326 if (lastPosition.line >= startLine && lastPosition.line <= endLine)
6327 lastPosition.line += targetLine - startLine + 1;
6328 setMark('<', lastAnchor);
6329 setMark('>', lastPosition);
6330
6331 if (lines > 2)
6332 showMessage(MessageInfo, Tr::tr("%n lines moved.", nullptr, lines));
6333
6334 return true;
6335 }
6336
handleExJoinCommand(const ExCommand & cmd)6337 bool FakeVimHandler::Private::handleExJoinCommand(const ExCommand &cmd)
6338 {
6339 // :[range]j[oin][!] [count]
6340 // FIXME: Argument [count] can follow immediately.
6341 if (!cmd.matches("j", "join"))
6342 return false;
6343
6344 // get [count] from arguments
6345 bool ok;
6346 int count = cmd.args.toInt(&ok);
6347
6348 if (ok) {
6349 setPosition(cmd.range.endPos);
6350 } else {
6351 setPosition(cmd.range.beginPos);
6352 const int startLine = blockAt(cmd.range.beginPos).blockNumber();
6353 const int endLine = blockAt(cmd.range.endPos).blockNumber();
6354 count = endLine - startLine + 1;
6355 }
6356
6357 moveToStartOfLine();
6358 pushUndoState();
6359 joinLines(count, cmd.hasBang);
6360
6361 moveToFirstNonBlankOnLine();
6362
6363 return true;
6364 }
6365
handleExWriteCommand(const ExCommand & cmd)6366 bool FakeVimHandler::Private::handleExWriteCommand(const ExCommand &cmd)
6367 {
6368 // Note: The cmd.args.isEmpty() case is handled by handleExPluginCommand.
6369 // :w, :x, :wq, ...
6370 //static QRegularExpression reWrite("^[wx]q?a?!?( (.*))?$");
6371 if (cmd.cmd != "w" && cmd.cmd != "x" && cmd.cmd != "wq")
6372 return false;
6373
6374 int beginLine = lineForPosition(cmd.range.beginPos);
6375 int endLine = lineForPosition(cmd.range.endPos);
6376 const bool noArgs = (beginLine == -1);
6377 if (beginLine == -1)
6378 beginLine = 0;
6379 if (endLine == -1)
6380 endLine = linesInDocument();
6381 //qDebug() << "LINES: " << beginLine << endLine;
6382 //QString prefix = cmd.args;
6383 const bool forced = cmd.hasBang;
6384 //const bool quit = prefix.contains('q') || prefix.contains('x');
6385 //const bool quitAll = quit && prefix.contains('a');
6386 QString fileName = replaceTildeWithHome(cmd.args);
6387 if (fileName.isEmpty())
6388 fileName = m_currentFileName;
6389 QFile file1(fileName);
6390 const bool exists = file1.exists();
6391 if (exists && !forced && !noArgs) {
6392 showMessage(MessageError, Tr::tr
6393 ("File \"%1\" exists (add ! to override)").arg(fileName));
6394 } else if (file1.open(QIODevice::ReadWrite)) {
6395 // Nobody cared, so act ourselves.
6396 file1.close();
6397 Range range(firstPositionInLine(beginLine),
6398 firstPositionInLine(endLine), RangeLineMode);
6399 QString contents = selectText(range);
6400 QFile::remove(fileName);
6401 QFile file2(fileName);
6402 if (file2.open(QIODevice::ReadWrite)) {
6403 QTextStream ts(&file2);
6404 ts << contents;
6405 } else {
6406 showMessage(MessageError, Tr::tr
6407 ("Cannot open file \"%1\" for writing").arg(fileName));
6408 }
6409 // Check result by reading back.
6410 QFile file3(fileName);
6411 file3.open(QIODevice::ReadOnly);
6412 QByteArray ba = file3.readAll();
6413 showMessage(MessageInfo, Tr::tr("\"%1\" %2 %3L, %4C written.")
6414 .arg(fileName).arg(exists ? QString(" ") : Tr::tr(" [New] "))
6415 .arg(ba.count('\n')).arg(ba.size()));
6416 //if (quitAll)
6417 // passUnknownExCommand(forced ? "qa!" : "qa");
6418 //else if (quit)
6419 // passUnknownExCommand(forced ? "q!" : "q");
6420 } else {
6421 showMessage(MessageError, Tr::tr
6422 ("Cannot open file \"%1\" for reading").arg(fileName));
6423 }
6424 return true;
6425 }
6426
handleExReadCommand(const ExCommand & cmd)6427 bool FakeVimHandler::Private::handleExReadCommand(const ExCommand &cmd)
6428 {
6429 // :r[ead]
6430 if (!cmd.matches("r", "read"))
6431 return false;
6432
6433 beginEditBlock();
6434
6435 moveToStartOfLine();
6436 moveDown();
6437 int pos = position();
6438
6439 m_currentFileName = replaceTildeWithHome(cmd.args);
6440 QFile file(m_currentFileName);
6441 file.open(QIODevice::ReadOnly);
6442 QTextStream ts(&file);
6443 QString data = ts.readAll();
6444 insertText(data);
6445
6446 setAnchorAndPosition(pos, pos);
6447
6448 endEditBlock();
6449
6450 showMessage(MessageInfo, Tr::tr("\"%1\" %2L, %3C")
6451 .arg(m_currentFileName).arg(data.count('\n')).arg(data.size()));
6452
6453 return true;
6454 }
6455
handleExBangCommand(const ExCommand & cmd)6456 bool FakeVimHandler::Private::handleExBangCommand(const ExCommand &cmd) // :!
6457 {
6458 if (!cmd.cmd.isEmpty() || !cmd.hasBang)
6459 return false;
6460
6461 bool replaceText = cmd.range.isValid();
6462 const QString command = QString(cmd.cmd.mid(1) + ' ' + cmd.args).trimmed();
6463 const QString input = replaceText ? selectText(cmd.range) : QString();
6464
6465 const QString result = getProcessOutput(command, input);
6466
6467 if (replaceText) {
6468 setCurrentRange(cmd.range);
6469 int targetPosition = firstPositionInLine(lineForPosition(cmd.range.beginPos));
6470 beginEditBlock();
6471 removeText(currentRange());
6472 insertText(result);
6473 setPosition(targetPosition);
6474 endEditBlock();
6475 leaveVisualMode();
6476 //qDebug() << "FILTER: " << command;
6477 showMessage(MessageInfo, Tr::tr("%n lines filtered.", nullptr,
6478 input.count('\n')));
6479 } else if (!result.isEmpty()) {
6480 q->extraInformationChanged(result);
6481 }
6482
6483 return true;
6484 }
6485
handleExShiftCommand(const ExCommand & cmd)6486 bool FakeVimHandler::Private::handleExShiftCommand(const ExCommand &cmd)
6487 {
6488 // :[range]{<|>}* [count]
6489 if (!cmd.cmd.isEmpty() || (!cmd.args.startsWith('<') && !cmd.args.startsWith('>')))
6490 return false;
6491
6492 const QChar c = cmd.args.at(0);
6493
6494 // get number of repetition
6495 int repeat = 1;
6496 int i = 1;
6497 for (; i < cmd.args.size(); ++i) {
6498 const QChar c2 = cmd.args.at(i);
6499 if (c2 == c)
6500 ++repeat;
6501 else if (!c2.isSpace())
6502 break;
6503 }
6504
6505 // get [count] from arguments
6506 Range range = cmd.range;
6507 parseRangeCount(cmd.args.mid(i), &range);
6508
6509 setCurrentRange(range);
6510 if (c == '<')
6511 shiftRegionLeft(repeat);
6512 else
6513 shiftRegionRight(repeat);
6514
6515 leaveVisualMode();
6516
6517 return true;
6518 }
6519
handleExSortCommand(const ExCommand & cmd)6520 bool FakeVimHandler::Private::handleExSortCommand(const ExCommand &cmd)
6521 {
6522 // :[range]sor[t][!] [b][f][i][n][o][r][u][x] [/{pattern}/]
6523 // FIXME: Only the ! for reverse is implemented.
6524 if (!cmd.matches("sor", "sort"))
6525 return false;
6526
6527 // Force operation on full lines, and full document if only
6528 // one line (the current one...) is specified
6529 int beginLine = lineForPosition(cmd.range.beginPos);
6530 int endLine = lineForPosition(cmd.range.endPos);
6531 if (beginLine == endLine) {
6532 beginLine = 0;
6533 endLine = lineForPosition(lastPositionInDocument());
6534 }
6535 Range range(firstPositionInLine(beginLine),
6536 firstPositionInLine(endLine), RangeLineMode);
6537
6538 QString input = selectText(range);
6539 if (input.endsWith('\n')) // It should always...
6540 input.chop(1);
6541
6542 QStringList lines = input.split('\n');
6543 lines.sort();
6544 if (cmd.hasBang)
6545 std::reverse(lines.begin(), lines.end());
6546 QString res = lines.join('\n') + '\n';
6547
6548 replaceText(range, res);
6549
6550 return true;
6551 }
6552
handleExNohlsearchCommand(const ExCommand & cmd)6553 bool FakeVimHandler::Private::handleExNohlsearchCommand(const ExCommand &cmd)
6554 {
6555 // :noh, :nohl, ..., :nohlsearch
6556 if (cmd.cmd.size() < 3 || !QString("nohlsearch").startsWith(cmd.cmd))
6557 return false;
6558
6559 g.highlightsCleared = true;
6560 updateHighlights();
6561 return true;
6562 }
6563
handleExUndoRedoCommand(const ExCommand & cmd)6564 bool FakeVimHandler::Private::handleExUndoRedoCommand(const ExCommand &cmd)
6565 {
6566 // :undo
6567 // :redo
6568 bool undo = (cmd.cmd == "u" || cmd.cmd == "un" || cmd.cmd == "undo");
6569 if (!undo && cmd.cmd != "red" && cmd.cmd != "redo")
6570 return false;
6571
6572 undoRedo(undo);
6573
6574 return true;
6575 }
6576
handleExGotoCommand(const ExCommand & cmd)6577 bool FakeVimHandler::Private::handleExGotoCommand(const ExCommand &cmd)
6578 {
6579 // :{address}
6580 if (!cmd.cmd.isEmpty() || !cmd.args.isEmpty())
6581 return false;
6582
6583 const int beginLine = lineForPosition(cmd.range.endPos);
6584 setPosition(firstPositionInLine(beginLine));
6585 clearMessage();
6586 return true;
6587 }
6588
handleExSourceCommand(const ExCommand & cmd)6589 bool FakeVimHandler::Private::handleExSourceCommand(const ExCommand &cmd)
6590 {
6591 // :source
6592 if (cmd.cmd != "so" && cmd.cmd != "source")
6593 return false;
6594
6595 QString fileName = replaceTildeWithHome(cmd.args);
6596 QFile file(fileName);
6597 if (!file.open(QIODevice::ReadOnly)) {
6598 showMessage(MessageError, Tr::tr("Cannot open file %1").arg(fileName));
6599 return true;
6600 }
6601
6602 bool inFunction = false;
6603 QByteArray line;
6604 while (!file.atEnd() || !line.isEmpty()) {
6605 QByteArray nextline = !file.atEnd() ? file.readLine() : QByteArray();
6606
6607 // remove comment
6608 int i = nextline.lastIndexOf('"');
6609 if (i != -1)
6610 nextline = nextline.remove(i, nextline.size() - i);
6611
6612 nextline = nextline.trimmed();
6613
6614 // multi-line command?
6615 if (nextline.startsWith('\\')) {
6616 line += nextline.mid(1);
6617 continue;
6618 }
6619
6620 if (line.startsWith("function")) {
6621 //qDebug() << "IGNORING FUNCTION" << line;
6622 inFunction = true;
6623 } else if (inFunction && line.startsWith("endfunction")) {
6624 inFunction = false;
6625 } else if (!line.isEmpty() && !inFunction) {
6626 //qDebug() << "EXECUTING: " << line;
6627 ExCommand cmd;
6628 QString commandLine = QString::fromLocal8Bit(line);
6629 while (parseExCommand(&commandLine, &cmd)) {
6630 if (!handleExCommandHelper(cmd))
6631 break;
6632 }
6633 }
6634
6635 line = nextline;
6636 }
6637 file.close();
6638 return true;
6639 }
6640
handleExEchoCommand(const ExCommand & cmd)6641 bool FakeVimHandler::Private::handleExEchoCommand(const ExCommand &cmd)
6642 {
6643 // :echo
6644 if (cmd.cmd != "echo")
6645 return false;
6646 showMessage(MessageInfo, cmd.args);
6647 return true;
6648 }
6649
handleExCommand(const QString & line0)6650 void FakeVimHandler::Private::handleExCommand(const QString &line0)
6651 {
6652 QString line = line0; // Make sure we have a copy to prevent aliasing.
6653
6654 if (line.endsWith('%')) {
6655 line.chop(1);
6656 int percent = line.toInt();
6657 setPosition(firstPositionInLine(percent * linesInDocument() / 100));
6658 clearMessage();
6659 return;
6660 }
6661
6662 //qDebug() << "CMD: " << cmd;
6663
6664 enterCommandMode(g.returnToMode);
6665
6666 beginLargeEditBlock();
6667 ExCommand cmd;
6668 QString lastCommand = line;
6669 while (parseExCommand(&line, &cmd)) {
6670 if (!handleExCommandHelper(cmd)) {
6671 showMessage(MessageError, Tr::tr("Not an editor command: %1").arg(lastCommand));
6672 break;
6673 }
6674 lastCommand = line;
6675 }
6676
6677 // if the last command closed the editor, we would crash here (:vs and then :on)
6678 if (!(m_textedit || m_plaintextedit))
6679 return;
6680
6681 endEditBlock();
6682
6683 if (isVisualMode())
6684 leaveVisualMode();
6685 leaveCurrentMode();
6686 }
6687
handleExCommandHelper(ExCommand & cmd)6688 bool FakeVimHandler::Private::handleExCommandHelper(ExCommand &cmd)
6689 {
6690 return handleExPluginCommand(cmd)
6691 || handleExGotoCommand(cmd)
6692 || handleExBangCommand(cmd)
6693 || handleExHistoryCommand(cmd)
6694 || handleExRegisterCommand(cmd)
6695 || handleExYankDeleteCommand(cmd)
6696 || handleExChangeCommand(cmd)
6697 || handleExMoveCommand(cmd)
6698 || handleExJoinCommand(cmd)
6699 || handleExMapCommand(cmd)
6700 || handleExNohlsearchCommand(cmd)
6701 || handleExNormalCommand(cmd)
6702 || handleExReadCommand(cmd)
6703 || handleExUndoRedoCommand(cmd)
6704 || handleExSetCommand(cmd)
6705 || handleExShiftCommand(cmd)
6706 || handleExSortCommand(cmd)
6707 || handleExSourceCommand(cmd)
6708 || handleExSubstituteCommand(cmd)
6709 || handleExTabNextCommand(cmd)
6710 || handleExTabPreviousCommand(cmd)
6711 || handleExWriteCommand(cmd)
6712 || handleExEchoCommand(cmd);
6713 }
6714
handleExPluginCommand(const ExCommand & cmd)6715 bool FakeVimHandler::Private::handleExPluginCommand(const ExCommand &cmd)
6716 {
6717 bool handled = false;
6718 int pos = m_cursor.position();
6719 commitCursor();
6720 q->handleExCommandRequested(&handled, cmd);
6721 //qDebug() << "HANDLER REQUEST: " << cmd.cmd << handled;
6722 if (handled && (m_textedit || m_plaintextedit)) {
6723 pullCursor();
6724 if (m_cursor.position() != pos)
6725 recordJump(pos);
6726 }
6727 return handled;
6728 }
6729
searchBalanced(bool forward,QChar needle,QChar other)6730 void FakeVimHandler::Private::searchBalanced(bool forward, QChar needle, QChar other)
6731 {
6732 int level = 1;
6733 int pos = position();
6734 const int npos = forward ? lastPositionInDocument() : 0;
6735 while (true) {
6736 if (forward)
6737 ++pos;
6738 else
6739 --pos;
6740 if (pos == npos)
6741 return;
6742 QChar c = characterAt(pos);
6743 if (c == other)
6744 ++level;
6745 else if (c == needle)
6746 --level;
6747 if (level == 0) {
6748 const int oldLine = cursorLine() - cursorLineOnScreen();
6749 // Making this unconditional feels better, but is not "vim like".
6750 if (oldLine != cursorLine() - cursorLineOnScreen())
6751 scrollToLine(cursorLine() - linesOnScreen() / 2);
6752 recordJump();
6753 setPosition(pos);
6754 setTargetColumn();
6755 return;
6756 }
6757 }
6758 }
6759
search(const SearchData & sd,int startPos,int count,bool showMessages)6760 QTextCursor FakeVimHandler::Private::search(const SearchData &sd, int startPos, int count,
6761 bool showMessages)
6762 {
6763 const QRegularExpression needleExp = vimPatternToQtPattern(sd.needle);
6764
6765 if (!needleExp.isValid()) {
6766 if (showMessages) {
6767 QString error = needleExp.errorString();
6768 showMessage(MessageError, Tr::tr("Invalid regular expression: %1").arg(error));
6769 }
6770 if (sd.highlightMatches)
6771 highlightMatches(QString());
6772 return QTextCursor();
6773 }
6774
6775 int repeat = count;
6776 const int pos = startPos + (sd.forward ? 1 : -1);
6777
6778 QTextCursor tc;
6779 if (pos >= 0 && pos < document()->characterCount()) {
6780 tc = QTextCursor(document());
6781 tc.setPosition(pos);
6782 if (sd.forward && afterEndOfLine(document(), pos))
6783 tc.movePosition(Right);
6784
6785 if (!tc.isNull()) {
6786 if (sd.forward)
6787 searchForward(&tc, needleExp, &repeat);
6788 else
6789 searchBackward(&tc, needleExp, &repeat);
6790 }
6791 }
6792
6793 if (tc.isNull()) {
6794 if (s.wrapScan.value()) {
6795 tc = QTextCursor(document());
6796 tc.movePosition(sd.forward ? StartOfDocument : EndOfDocument);
6797 if (sd.forward)
6798 searchForward(&tc, needleExp, &repeat);
6799 else
6800 searchBackward(&tc, needleExp, &repeat);
6801 if (tc.isNull()) {
6802 if (showMessages) {
6803 showMessage(MessageError,
6804 Tr::tr("Pattern not found: %1").arg(sd.needle));
6805 }
6806 } else if (showMessages) {
6807 QString msg = sd.forward
6808 ? Tr::tr("Search hit BOTTOM, continuing at TOP.")
6809 : Tr::tr("Search hit TOP, continuing at BOTTOM.");
6810 showMessage(MessageWarning, msg);
6811 }
6812 } else if (showMessages) {
6813 QString msg = sd.forward
6814 ? Tr::tr("Search hit BOTTOM without match for: %1")
6815 : Tr::tr("Search hit TOP without match for: %1");
6816 showMessage(MessageError, msg.arg(sd.needle));
6817 }
6818 }
6819
6820 if (sd.highlightMatches)
6821 highlightMatches(needleExp.pattern());
6822
6823 return tc;
6824 }
6825
search(const SearchData & sd,bool showMessages)6826 void FakeVimHandler::Private::search(const SearchData &sd, bool showMessages)
6827 {
6828 const int oldLine = cursorLine() - cursorLineOnScreen();
6829
6830 QTextCursor tc = search(sd, m_searchStartPosition, count(), showMessages);
6831 if (tc.isNull()) {
6832 tc = m_cursor;
6833 tc.setPosition(m_searchStartPosition);
6834 }
6835
6836 if (isVisualMode()) {
6837 int d = tc.anchor() - tc.position();
6838 setPosition(tc.position() + d);
6839 } else {
6840 // Set Cursor. In contrast to the main editor we have the cursor
6841 // position before the anchor position.
6842 setAnchorAndPosition(tc.position(), tc.anchor());
6843 }
6844
6845 // Making this unconditional feels better, but is not "vim like".
6846 if (oldLine != cursorLine() - cursorLineOnScreen())
6847 scrollToLine(cursorLine() - linesOnScreen() / 2);
6848
6849 m_searchCursor = m_cursor;
6850
6851 setTargetColumn();
6852 }
6853
searchNext(bool forward)6854 bool FakeVimHandler::Private::searchNext(bool forward)
6855 {
6856 SearchData sd;
6857 sd.needle = g.lastSearch;
6858 sd.forward = forward ? g.lastSearchForward : !g.lastSearchForward;
6859 sd.highlightMatches = true;
6860 m_searchStartPosition = position();
6861 showMessage(MessageCommand, QLatin1Char(g.lastSearchForward ? '/' : '?') + sd.needle);
6862 recordJump();
6863 search(sd);
6864 return finishSearch();
6865 }
6866
highlightMatches(const QString & needle)6867 void FakeVimHandler::Private::highlightMatches(const QString &needle)
6868 {
6869 g.lastNeedle = needle;
6870 g.highlightsCleared = false;
6871 updateHighlights();
6872 }
6873
moveToFirstNonBlankOnLine()6874 void FakeVimHandler::Private::moveToFirstNonBlankOnLine()
6875 {
6876 g.movetype = MoveLineWise;
6877 moveToFirstNonBlankOnLine(&m_cursor);
6878 setTargetColumn();
6879 }
6880
moveToFirstNonBlankOnLine(QTextCursor * tc)6881 void FakeVimHandler::Private::moveToFirstNonBlankOnLine(QTextCursor *tc)
6882 {
6883 tc->setPosition(tc->block().position(), KeepAnchor);
6884 moveToNonBlankOnLine(tc);
6885 }
6886
moveToFirstNonBlankOnLineVisually()6887 void FakeVimHandler::Private::moveToFirstNonBlankOnLineVisually()
6888 {
6889 moveToStartOfLineVisually();
6890 moveToNonBlankOnLine(&m_cursor);
6891 setTargetColumn();
6892 }
6893
moveToNonBlankOnLine(QTextCursor * tc)6894 void FakeVimHandler::Private::moveToNonBlankOnLine(QTextCursor *tc)
6895 {
6896 const QTextBlock block = tc->block();
6897 const int maxPos = block.position() + block.length() - 1;
6898 int i = tc->position();
6899 while (characterAt(i).isSpace() && i < maxPos)
6900 ++i;
6901 tc->setPosition(i, KeepAnchor);
6902 }
6903
indentSelectedText(QChar typedChar)6904 void FakeVimHandler::Private::indentSelectedText(QChar typedChar)
6905 {
6906 beginEditBlock();
6907 setTargetColumn();
6908 int beginLine = qMin(lineForPosition(position()), lineForPosition(anchor()));
6909 int endLine = qMax(lineForPosition(position()), lineForPosition(anchor()));
6910
6911 Range range(anchor(), position(), g.rangemode);
6912 indentText(range, typedChar);
6913
6914 setPosition(firstPositionInLine(beginLine));
6915 handleStartOfLine();
6916 setTargetColumn();
6917 setDotCommand("%1==", endLine - beginLine + 1);
6918 endEditBlock();
6919
6920 const int lines = endLine - beginLine + 1;
6921 if (lines > 2)
6922 showMessage(MessageInfo, Tr::tr("%n lines indented.", nullptr, lines));
6923 }
6924
indentText(const Range & range,QChar typedChar)6925 void FakeVimHandler::Private::indentText(const Range &range, QChar typedChar)
6926 {
6927 int beginBlock = blockAt(range.beginPos).blockNumber();
6928 int endBlock = blockAt(range.endPos).blockNumber();
6929 if (beginBlock > endBlock)
6930 std::swap(beginBlock, endBlock);
6931
6932 // Don't remember current indentation in last text insertion.
6933 const QString lastInsertion = m_buffer->lastInsertion;
6934 q->indentRegion(beginBlock, endBlock, typedChar);
6935 m_buffer->lastInsertion = lastInsertion;
6936 }
6937
isElectricCharacter(QChar c) const6938 bool FakeVimHandler::Private::isElectricCharacter(QChar c) const
6939 {
6940 bool result = false;
6941 q->checkForElectricCharacter(&result, c);
6942 return result;
6943 }
6944
shiftRegionRight(int repeat)6945 void FakeVimHandler::Private::shiftRegionRight(int repeat)
6946 {
6947 int beginLine = lineForPosition(anchor());
6948 int endLine = lineForPosition(position());
6949 int targetPos = anchor();
6950 if (beginLine > endLine) {
6951 std::swap(beginLine, endLine);
6952 targetPos = position();
6953 }
6954 if (s.startOfLine.value())
6955 targetPos = firstPositionInLine(beginLine);
6956
6957 const int sw = s.shiftWidth.value();
6958 g.movetype = MoveLineWise;
6959 beginEditBlock();
6960 QTextBlock block = document()->findBlockByLineNumber(beginLine - 1);
6961 while (block.isValid() && lineNumber(block) <= endLine) {
6962 const Column col = indentation(block.text());
6963 QTextCursor tc = m_cursor;
6964 tc.setPosition(block.position());
6965 if (col.physical > 0)
6966 tc.setPosition(tc.position() + col.physical, KeepAnchor);
6967 tc.insertText(tabExpand(col.logical + sw * repeat));
6968 block = block.next();
6969 }
6970 endEditBlock();
6971
6972 setPosition(targetPos);
6973 handleStartOfLine();
6974
6975 const int lines = endLine - beginLine + 1;
6976 if (lines > 2) {
6977 showMessage(MessageInfo,
6978 Tr::tr("%n lines %1ed %2 time.", nullptr, lines)
6979 .arg(repeat > 0 ? '>' : '<').arg(qAbs(repeat)));
6980 }
6981 }
6982
shiftRegionLeft(int repeat)6983 void FakeVimHandler::Private::shiftRegionLeft(int repeat)
6984 {
6985 shiftRegionRight(-repeat);
6986 }
6987
moveToTargetColumn()6988 void FakeVimHandler::Private::moveToTargetColumn()
6989 {
6990 const QTextBlock &bl = block();
6991 //Column column = cursorColumn();
6992 //int logical = logical
6993 const int pos = lastPositionInLine(bl.blockNumber() + 1, false);
6994 if (m_targetColumn == -1) {
6995 setPosition(pos);
6996 return;
6997 }
6998 const int physical = bl.position() + logicalToPhysicalColumn(m_targetColumn, bl.text());
6999 //qDebug() << "CORRECTING COLUMN FROM: " << logical << "TO" << m_targetColumn;
7000 setPosition(qMin(pos, physical));
7001 }
7002
setTargetColumn()7003 void FakeVimHandler::Private::setTargetColumn()
7004 {
7005 m_targetColumn = logicalCursorColumn();
7006 m_visualTargetColumn = m_targetColumn;
7007
7008 QTextCursor tc = m_cursor;
7009 tc.movePosition(StartOfLine);
7010 m_targetColumnWrapped = m_cursor.position() - tc.position();
7011 }
7012
7013 /* if simple is given:
7014 * class 0: spaces
7015 * class 1: non-spaces
7016 * else
7017 * class 0: spaces
7018 * class 1: non-space-or-letter-or-number
7019 * class 2: letter-or-number
7020 */
7021
7022
charClass(QChar c,bool simple) const7023 int FakeVimHandler::Private::charClass(QChar c, bool simple) const
7024 {
7025 if (simple)
7026 return c.isSpace() ? 0 : 1;
7027 // FIXME: This means that only characters < 256 in the
7028 // ConfigIsKeyword setting are handled properly.
7029 if (c.unicode() < 256) {
7030 //int old = (c.isLetterOrNumber() || c.unicode() == '_') ? 2
7031 // : c.isSpace() ? 0 : 1;
7032 //qDebug() << c.unicode() << old << m_charClass[c.unicode()];
7033 return m_charClass[c.unicode()];
7034 }
7035 if (c.isLetterOrNumber() || c == '_')
7036 return 2;
7037 return c.isSpace() ? 0 : 1;
7038 }
7039
miniBufferTextEdited(const QString & text,int cursorPos,int anchorPos)7040 void FakeVimHandler::Private::miniBufferTextEdited(const QString &text, int cursorPos,
7041 int anchorPos)
7042 {
7043 if (!isCommandLineMode()) {
7044 editor()->setFocus();
7045 } else if (text.isEmpty()) {
7046 // editing cancelled
7047 enterFakeVim();
7048 handleDefaultKey(Input(Qt::Key_Escape, Qt::NoModifier, QString()));
7049 leaveFakeVim();
7050 editor()->setFocus();
7051 } else {
7052 CommandBuffer &cmdBuf = (g.mode == ExMode) ? g.commandBuffer : g.searchBuffer;
7053 int pos = qMax(1, cursorPos);
7054 int anchor = anchorPos == -1 ? pos : qMax(1, anchorPos);
7055 QString buffer = text;
7056 // prepend prompt character if missing
7057 if (!buffer.startsWith(cmdBuf.prompt())) {
7058 buffer.prepend(cmdBuf.prompt());
7059 ++pos;
7060 ++anchor;
7061 }
7062 // update command/search buffer
7063 cmdBuf.setContents(buffer.mid(1), pos - 1, anchor - 1);
7064 if (pos != cursorPos || anchor != anchorPos || buffer != text)
7065 q->commandBufferChanged(buffer, pos, anchor, 0);
7066 // update search expression
7067 if (g.subsubmode == SearchSubSubMode) {
7068 updateFind(false);
7069 commitCursor();
7070 }
7071 }
7072 }
7073
pullOrCreateBufferData()7074 void FakeVimHandler::Private::pullOrCreateBufferData()
7075 {
7076 const QVariant data = document()->property("FakeVimSharedData");
7077 if (data.isValid()) {
7078 // FakeVimHandler has been already created for this document (e.g. in other split).
7079 m_buffer = data.value<BufferDataPtr>();
7080 } else {
7081 // FakeVimHandler has not been created for this document yet.
7082 m_buffer = BufferDataPtr(new BufferData);
7083 document()->setProperty("FakeVimSharedData", QVariant::fromValue(m_buffer));
7084 }
7085
7086 if (editor()->hasFocus())
7087 m_buffer->currentHandler = this;
7088 }
7089
7090 // Helper to parse a-z,A-Z,48-57,_
someInt(const QString & str)7091 static int someInt(const QString &str)
7092 {
7093 if (str.toInt())
7094 return str.toInt();
7095 if (!str.isEmpty())
7096 return str.at(0).unicode();
7097 return 0;
7098 }
7099
setupCharClass()7100 void FakeVimHandler::Private::setupCharClass()
7101 {
7102 for (int i = 0; i < 256; ++i) {
7103 const QChar c = QLatin1Char(i);
7104 m_charClass[i] = c.isSpace() ? 0 : 1;
7105 }
7106 const QString conf = s.isKeyword.value();
7107 for (const QString &part : conf.split(',')) {
7108 if (part.contains('-')) {
7109 const int from = someInt(part.section('-', 0, 0));
7110 const int to = someInt(part.section('-', 1, 1));
7111 for (int i = qMax(0, from); i <= qMin(255, to); ++i)
7112 m_charClass[i] = 2;
7113 } else {
7114 m_charClass[qMin(255, someInt(part))] = 2;
7115 }
7116 }
7117 }
7118
moveToBoundary(bool simple,bool forward)7119 void FakeVimHandler::Private::moveToBoundary(bool simple, bool forward)
7120 {
7121 QTextCursor tc(document());
7122 tc.setPosition(position());
7123 if (forward ? tc.atBlockEnd() : tc.atBlockStart())
7124 return;
7125
7126 QChar c = characterAt(tc.position() + (forward ? -1 : 1));
7127 int lastClass = tc.atStart() ? -1 : charClass(c, simple);
7128 QTextCursor::MoveOperation op = forward ? Right : Left;
7129 while (true) {
7130 c = characterAt(tc.position());
7131 int thisClass = charClass(c, simple);
7132 if (thisClass != lastClass || (forward ? tc.atBlockEnd() : tc.atBlockStart())) {
7133 if (tc != m_cursor)
7134 tc.movePosition(forward ? Left : Right);
7135 break;
7136 }
7137 lastClass = thisClass;
7138 tc.movePosition(op);
7139 }
7140 setPosition(tc.position());
7141 }
7142
moveToNextBoundary(bool end,int count,bool simple,bool forward)7143 void FakeVimHandler::Private::moveToNextBoundary(bool end, int count, bool simple, bool forward)
7144 {
7145 int repeat = count;
7146 while (repeat > 0 && !(forward ? atDocumentEnd() : atDocumentStart())) {
7147 setPosition(position() + (forward ? 1 : -1));
7148 moveToBoundary(simple, forward);
7149 if (atBoundary(end, simple))
7150 --repeat;
7151 }
7152 }
7153
moveToNextBoundaryStart(int count,bool simple,bool forward)7154 void FakeVimHandler::Private::moveToNextBoundaryStart(int count, bool simple, bool forward)
7155 {
7156 moveToNextBoundary(false, count, simple, forward);
7157 }
7158
moveToNextBoundaryEnd(int count,bool simple,bool forward)7159 void FakeVimHandler::Private::moveToNextBoundaryEnd(int count, bool simple, bool forward)
7160 {
7161 moveToNextBoundary(true, count, simple, forward);
7162 }
7163
moveToBoundaryStart(int count,bool simple,bool forward)7164 void FakeVimHandler::Private::moveToBoundaryStart(int count, bool simple, bool forward)
7165 {
7166 moveToNextBoundaryStart(atBoundary(false, simple) ? count - 1 : count, simple, forward);
7167 }
7168
moveToBoundaryEnd(int count,bool simple,bool forward)7169 void FakeVimHandler::Private::moveToBoundaryEnd(int count, bool simple, bool forward)
7170 {
7171 moveToNextBoundaryEnd(atBoundary(true, simple) ? count - 1 : count, simple, forward);
7172 }
7173
moveToNextWord(bool end,int count,bool simple,bool forward,bool emptyLines)7174 void FakeVimHandler::Private::moveToNextWord(bool end, int count, bool simple, bool forward, bool emptyLines)
7175 {
7176 int repeat = count;
7177 while (repeat > 0 && !(forward ? atDocumentEnd() : atDocumentStart())) {
7178 setPosition(position() + (forward ? 1 : -1));
7179 moveToBoundary(simple, forward);
7180 if (atWordBoundary(end, simple) && (emptyLines || !atEmptyLine()) )
7181 --repeat;
7182 }
7183 }
7184
moveToNextWordStart(int count,bool simple,bool forward,bool emptyLines)7185 void FakeVimHandler::Private::moveToNextWordStart(int count, bool simple, bool forward, bool emptyLines)
7186 {
7187 g.movetype = MoveExclusive;
7188 moveToNextWord(false, count, simple, forward, emptyLines);
7189 setTargetColumn();
7190 }
7191
moveToNextWordEnd(int count,bool simple,bool forward,bool emptyLines)7192 void FakeVimHandler::Private::moveToNextWordEnd(int count, bool simple, bool forward, bool emptyLines)
7193 {
7194 g.movetype = MoveInclusive;
7195 moveToNextWord(true, count, simple, forward, emptyLines);
7196 setTargetColumn();
7197 }
7198
moveToWordStart(int count,bool simple,bool forward,bool emptyLines)7199 void FakeVimHandler::Private::moveToWordStart(int count, bool simple, bool forward, bool emptyLines)
7200 {
7201 moveToNextWordStart(atWordStart(simple) ? count - 1 : count, simple, forward, emptyLines);
7202 }
7203
moveToWordEnd(int count,bool simple,bool forward,bool emptyLines)7204 void FakeVimHandler::Private::moveToWordEnd(int count, bool simple, bool forward, bool emptyLines)
7205 {
7206 moveToNextWordEnd(atWordEnd(simple) ? count - 1 : count, simple, forward, emptyLines);
7207 }
7208
handleFfTt(const QString & key,bool repeats)7209 bool FakeVimHandler::Private::handleFfTt(const QString &key, bool repeats)
7210 {
7211 int key0 = key.size() == 1 ? key.at(0).unicode() : 0;
7212 // g.subsubmode \in { 'f', 'F', 't', 'T' }
7213 bool forward = g.subsubdata.is('f') || g.subsubdata.is('t');
7214 bool exclusive = g.subsubdata.is('t') || g.subsubdata.is('T');
7215 int repeat = count();
7216 int n = block().position() + (forward ? block().length() : - 1);
7217 const int d = forward ? 1 : -1;
7218 // FIXME: This also depends on whether 'cpositions' Vim option contains ';'.
7219 const int skip = (repeats && repeat == 1 && exclusive) ? d : 0;
7220 int pos = position() + d + skip;
7221
7222 for (; repeat > 0 && (forward ? pos < n : pos > n); pos += d) {
7223 if (characterAt(pos).unicode() == key0)
7224 --repeat;
7225 }
7226
7227 if (repeat == 0) {
7228 setPosition(pos - d - (exclusive ? d : 0));
7229 setTargetColumn();
7230 return true;
7231 }
7232
7233 return false;
7234 }
7235
moveToMatchingParanthesis()7236 void FakeVimHandler::Private::moveToMatchingParanthesis()
7237 {
7238 bool moved = false;
7239 bool forward = false;
7240
7241 const int anc = anchor();
7242 QTextCursor tc = m_cursor;
7243
7244 // If no known parenthesis symbol is under cursor find one on the current line after cursor.
7245 static const QString parenthesesChars("([{}])");
7246 while (!parenthesesChars.contains(characterAt(tc.position())) && !tc.atBlockEnd())
7247 tc.setPosition(tc.position() + 1);
7248
7249 if (tc.atBlockEnd())
7250 tc = m_cursor;
7251
7252 q->moveToMatchingParenthesis(&moved, &forward, &tc);
7253 if (moved) {
7254 if (forward)
7255 tc.movePosition(Left, KeepAnchor, 1);
7256 setAnchorAndPosition(anc, tc.position());
7257 setTargetColumn();
7258 }
7259 }
7260
cursorLineOnScreen() const7261 int FakeVimHandler::Private::cursorLineOnScreen() const
7262 {
7263 if (!editor())
7264 return 0;
7265 const QRect rect = EDITOR(cursorRect(m_cursor));
7266 return rect.height() > 0 ? rect.y() / rect.height() : 0;
7267 }
7268
linesOnScreen() const7269 int FakeVimHandler::Private::linesOnScreen() const
7270 {
7271 if (!editor())
7272 return 1;
7273 const int h = EDITOR(cursorRect(m_cursor)).height();
7274 return h > 0 ? EDITOR(viewport()->height()) / h : 1;
7275 }
7276
cursorLine() const7277 int FakeVimHandler::Private::cursorLine() const
7278 {
7279 return lineForPosition(position()) - 1;
7280 }
7281
cursorBlockNumber() const7282 int FakeVimHandler::Private::cursorBlockNumber() const
7283 {
7284 return blockAt(qMin(anchor(), position())).blockNumber();
7285 }
7286
physicalCursorColumn() const7287 int FakeVimHandler::Private::physicalCursorColumn() const
7288 {
7289 return position() - block().position();
7290 }
7291
physicalToLogicalColumn(const int physical,const QString & line) const7292 int FakeVimHandler::Private::physicalToLogicalColumn
7293 (const int physical, const QString &line) const
7294 {
7295 const int ts = s.tabStop.value();
7296 int p = 0;
7297 int logical = 0;
7298 while (p < physical) {
7299 QChar c = line.at(p);
7300 //if (c == ' ')
7301 // ++logical;
7302 //else
7303 if (c == '\t')
7304 logical += ts - logical % ts;
7305 else
7306 ++logical;
7307 //break;
7308 ++p;
7309 }
7310 return logical;
7311 }
7312
logicalToPhysicalColumn(const int logical,const QString & line) const7313 int FakeVimHandler::Private::logicalToPhysicalColumn
7314 (const int logical, const QString &line) const
7315 {
7316 const int ts = s.tabStop.value();
7317 int physical = 0;
7318 for (int l = 0; l < logical && physical < line.size(); ++physical) {
7319 QChar c = line.at(physical);
7320 if (c == '\t')
7321 l += ts - l % ts;
7322 else
7323 ++l;
7324 }
7325 return physical;
7326 }
7327
windowScrollOffset() const7328 int FakeVimHandler::Private::windowScrollOffset() const
7329 {
7330 return qMin(static_cast<int>(s.scrollOff.value()), linesOnScreen() / 2);
7331 }
7332
logicalCursorColumn() const7333 int FakeVimHandler::Private::logicalCursorColumn() const
7334 {
7335 const int physical = physicalCursorColumn();
7336 const QString line = block().text();
7337 return physicalToLogicalColumn(physical, line);
7338 }
7339
cursorColumn() const7340 Column FakeVimHandler::Private::cursorColumn() const
7341 {
7342 return Column(physicalCursorColumn(), logicalCursorColumn());
7343 }
7344
linesInDocument() const7345 int FakeVimHandler::Private::linesInDocument() const
7346 {
7347 if (m_cursor.isNull())
7348 return 0;
7349 return document()->blockCount();
7350 }
7351
scrollToLine(int line)7352 void FakeVimHandler::Private::scrollToLine(int line)
7353 {
7354 // Don't scroll if the line is already at the top.
7355 updateFirstVisibleLine();
7356 if (line == m_firstVisibleLine)
7357 return;
7358
7359 const QTextCursor tc = m_cursor;
7360
7361 QTextCursor tc2 = tc;
7362 tc2.setPosition(document()->lastBlock().position());
7363 EDITOR(setTextCursor(tc2));
7364 EDITOR(ensureCursorVisible());
7365
7366 int offset = 0;
7367 const QTextBlock block = document()->findBlockByLineNumber(line);
7368 if (block.isValid()) {
7369 const int blockLineCount = block.layout()->lineCount();
7370 const int lineInBlock = line - block.firstLineNumber();
7371 if (0 <= lineInBlock && lineInBlock < blockLineCount) {
7372 QTextLine textLine = block.layout()->lineAt(lineInBlock);
7373 offset = textLine.textStart();
7374 } else {
7375 // QTC_CHECK(false);
7376 }
7377 }
7378 tc2.setPosition(block.position() + offset);
7379 EDITOR(setTextCursor(tc2));
7380 EDITOR(ensureCursorVisible());
7381
7382 EDITOR(setTextCursor(tc));
7383
7384 m_firstVisibleLine = line;
7385 }
7386
updateFirstVisibleLine()7387 void FakeVimHandler::Private::updateFirstVisibleLine()
7388 {
7389 const QTextCursor tc = EDITOR(cursorForPosition(QPoint(0,0)));
7390 m_firstVisibleLine = lineForPosition(tc.position()) - 1;
7391 }
7392
firstVisibleLine() const7393 int FakeVimHandler::Private::firstVisibleLine() const
7394 {
7395 return m_firstVisibleLine;
7396 }
7397
lastVisibleLine() const7398 int FakeVimHandler::Private::lastVisibleLine() const
7399 {
7400 const int line = m_firstVisibleLine + linesOnScreen();
7401 const QTextBlock block = document()->findBlockByLineNumber(line);
7402 return block.isValid() ? line : document()->lastBlock().firstLineNumber();
7403 }
7404
lineOnTop(int count) const7405 int FakeVimHandler::Private::lineOnTop(int count) const
7406 {
7407 const int scrollOffset = qMax(count - 1, windowScrollOffset());
7408 const int line = firstVisibleLine();
7409 return line == 0 ? count - 1 : scrollOffset + line;
7410 }
7411
lineOnBottom(int count) const7412 int FakeVimHandler::Private::lineOnBottom(int count) const
7413 {
7414 const int scrollOffset = qMax(count - 1, windowScrollOffset());
7415 const int line = lastVisibleLine();
7416 return line >= document()->lastBlock().firstLineNumber() ? line - count + 1
7417 : line - scrollOffset - 1;
7418 }
7419
scrollUp(int count)7420 void FakeVimHandler::Private::scrollUp(int count)
7421 {
7422 scrollToLine(cursorLine() - cursorLineOnScreen() - count);
7423 }
7424
updateScrollOffset()7425 void FakeVimHandler::Private::updateScrollOffset()
7426 {
7427 const int line = cursorLine();
7428 if (line < lineOnTop())
7429 scrollToLine(qMax(0, line - windowScrollOffset()));
7430 else if (line > lineOnBottom())
7431 scrollToLine(firstVisibleLine() + line - lineOnBottom());
7432 }
7433
alignViewportToCursor(AlignmentFlag align,int line,bool moveToNonBlank)7434 void FakeVimHandler::Private::alignViewportToCursor(AlignmentFlag align, int line,
7435 bool moveToNonBlank)
7436 {
7437 if (line > 0)
7438 setPosition(firstPositionInLine(line));
7439 if (moveToNonBlank)
7440 moveToFirstNonBlankOnLine();
7441
7442 if (align == Qt::AlignTop)
7443 scrollUp(- cursorLineOnScreen());
7444 else if (align == Qt::AlignVCenter)
7445 scrollUp(linesOnScreen() / 2 - cursorLineOnScreen());
7446 else if (align == Qt::AlignBottom)
7447 scrollUp(linesOnScreen() - cursorLineOnScreen() - 1);
7448 }
7449
lineToBlockNumber(int line) const7450 int FakeVimHandler::Private::lineToBlockNumber(int line) const
7451 {
7452 return document()->findBlockByLineNumber(line).blockNumber();
7453 }
7454
setCursorPosition(const CursorPosition & p)7455 void FakeVimHandler::Private::setCursorPosition(const CursorPosition &p)
7456 {
7457 const int firstLine = firstVisibleLine();
7458 const int firstBlock = lineToBlockNumber(firstLine);
7459 const int lastBlock = lineToBlockNumber(firstLine + linesOnScreen() - 2);
7460 bool isLineVisible = firstBlock <= p.line && p.line <= lastBlock;
7461 setCursorPosition(&m_cursor, p);
7462 if (!isLineVisible)
7463 alignViewportToCursor(Qt::AlignVCenter);
7464 }
7465
setCursorPosition(QTextCursor * tc,const CursorPosition & p)7466 void FakeVimHandler::Private::setCursorPosition(QTextCursor *tc, const CursorPosition &p)
7467 {
7468 const int line = qMin(document()->blockCount() - 1, p.line);
7469 QTextBlock block = document()->findBlockByNumber(line);
7470 const int column = qMin(p.column, block.length() - 1);
7471 tc->setPosition(block.position() + column, KeepAnchor);
7472 }
7473
lastPositionInDocument(bool ignoreMode) const7474 int FakeVimHandler::Private::lastPositionInDocument(bool ignoreMode) const
7475 {
7476 return document()->characterCount()
7477 - (ignoreMode || isVisualMode() || isInsertMode() ? 1 : 2);
7478 }
7479
selectText(const Range & range) const7480 QString FakeVimHandler::Private::selectText(const Range &range) const
7481 {
7482 QString contents;
7483 const QString lineEnd = range.rangemode == RangeBlockMode ? QString('\n') : QString();
7484 QTextCursor tc = m_cursor;
7485 transformText(range, tc,
7486 [&tc, &contents, &lineEnd]() { contents.append(tc.selection().toPlainText() + lineEnd); });
7487 return contents;
7488 }
7489
yankText(const Range & range,int reg)7490 void FakeVimHandler::Private::yankText(const Range &range, int reg)
7491 {
7492 const QString text = selectText(range);
7493 setRegister(reg, text, range.rangemode);
7494
7495 // If register is not specified or " ...
7496 if (m_register == '"') {
7497 // with delete and change commands set register 1 (if text contains more lines) or
7498 // small delete register -
7499 if (g.submode == DeleteSubMode || g.submode == ChangeSubMode) {
7500 if (text.contains('\n'))
7501 setRegister('1', text, range.rangemode);
7502 else
7503 setRegister('-', text, range.rangemode);
7504 } else {
7505 // copy to yank register 0 too
7506 setRegister('0', text, range.rangemode);
7507 }
7508 } else if (m_register != '_') {
7509 // Always copy to " register too (except black hole register).
7510 setRegister('"', text, range.rangemode);
7511 }
7512
7513 const int lines = blockAt(range.endPos).blockNumber()
7514 - blockAt(range.beginPos).blockNumber() + 1;
7515 if (lines > 2)
7516 showMessage(MessageInfo, Tr::tr("%n lines yanked.", nullptr, lines));
7517 }
7518
transformText(const Range & range,QTextCursor & tc,const std::function<void ()> & transform) const7519 void FakeVimHandler::Private::transformText(
7520 const Range &range, QTextCursor &tc, const std::function<void()> &transform) const
7521 {
7522 switch (range.rangemode) {
7523 case RangeCharMode: {
7524 // This can span multiple lines.
7525 tc.setPosition(range.beginPos, MoveAnchor);
7526 tc.setPosition(range.endPos, KeepAnchor);
7527 transform();
7528 tc.setPosition(range.beginPos);
7529 break;
7530 }
7531 case RangeLineMode:
7532 case RangeLineModeExclusive: {
7533 tc.setPosition(range.beginPos, MoveAnchor);
7534 tc.movePosition(StartOfLine, MoveAnchor);
7535 tc.setPosition(range.endPos, KeepAnchor);
7536 tc.movePosition(EndOfLine, KeepAnchor);
7537 if (range.rangemode != RangeLineModeExclusive) {
7538 // make sure that complete lines are removed
7539 // - also at the beginning and at the end of the document
7540 if (tc.atEnd()) {
7541 tc.setPosition(range.beginPos, MoveAnchor);
7542 tc.movePosition(StartOfLine, MoveAnchor);
7543 if (!tc.atStart()) {
7544 // also remove first line if it is the only one
7545 tc.movePosition(Left, MoveAnchor, 1);
7546 tc.movePosition(EndOfLine, MoveAnchor, 1);
7547 }
7548 tc.setPosition(range.endPos, KeepAnchor);
7549 tc.movePosition(EndOfLine, KeepAnchor);
7550 } else {
7551 tc.movePosition(Right, KeepAnchor, 1);
7552 }
7553 }
7554 const int posAfter = tc.anchor();
7555 transform();
7556 tc.setPosition(posAfter);
7557 break;
7558 }
7559 case RangeBlockAndTailMode:
7560 case RangeBlockMode: {
7561 int beginColumn = columnAt(range.beginPos);
7562 int endColumn = columnAt(range.endPos);
7563 if (endColumn < beginColumn)
7564 std::swap(beginColumn, endColumn);
7565 if (range.rangemode == RangeBlockAndTailMode)
7566 endColumn = INT_MAX - 1;
7567 QTextBlock block = document()->findBlock(range.beginPos);
7568 const QTextBlock lastBlock = document()->findBlock(range.endPos);
7569 while (block.isValid() && block.position() <= lastBlock.position()) {
7570 int bCol = qMin(beginColumn, block.length() - 1);
7571 int eCol = qMin(endColumn + 1, block.length() - 1);
7572 tc.setPosition(block.position() + bCol, MoveAnchor);
7573 tc.setPosition(block.position() + eCol, KeepAnchor);
7574 transform();
7575 block = block.next();
7576 }
7577 tc.setPosition(range.beginPos);
7578 break;
7579 }
7580 }
7581 }
7582
transformText(const Range & range,const Transformation & transform)7583 void FakeVimHandler::Private::transformText(const Range &range, const Transformation &transform)
7584 {
7585 beginEditBlock();
7586 transformText(range, m_cursor,
7587 [this, &transform] { m_cursor.insertText(transform(m_cursor.selection().toPlainText())); });
7588 endEditBlock();
7589 setTargetColumn();
7590 }
7591
insertText(QTextCursor & tc,const QString & text)7592 void FakeVimHandler::Private::insertText(QTextCursor &tc, const QString &text)
7593 {
7594 if (s.passKeys.value()) {
7595 if (tc.hasSelection() && text.isEmpty()) {
7596 QKeyEvent event(QEvent::KeyPress, Qt::Key_Delete, Qt::NoModifier, QString());
7597 passEventToEditor(event, tc);
7598 }
7599
7600 for (QChar c : text) {
7601 QKeyEvent event(QEvent::KeyPress, -1, Qt::NoModifier, QString(c));
7602 passEventToEditor(event, tc);
7603 }
7604 } else {
7605 tc.insertText(text);
7606 }
7607 }
7608
insertText(const Register & reg)7609 void FakeVimHandler::Private::insertText(const Register ®)
7610 {
7611 if (reg.rangemode != RangeCharMode) {
7612 qWarning() << "WRONG INSERT MODE: " << reg.rangemode;
7613 return;
7614 }
7615 setAnchor();
7616 m_cursor.insertText(reg.contents);
7617 //dump("AFTER INSERT");
7618 }
7619
removeText(const Range & range)7620 void FakeVimHandler::Private::removeText(const Range &range)
7621 {
7622 transformText(range, [](const QString &) { return QString(); });
7623 }
7624
downCase(const Range & range)7625 void FakeVimHandler::Private::downCase(const Range &range)
7626 {
7627 transformText(range, [](const QString &text) { return text.toLower(); } );
7628 }
7629
upCase(const Range & range)7630 void FakeVimHandler::Private::upCase(const Range &range)
7631 {
7632 transformText(range, [](const QString &text) { return text.toUpper(); } );
7633 }
7634
invertCase(const Range & range)7635 void FakeVimHandler::Private::invertCase(const Range &range)
7636 {
7637 transformText(range,
7638 [] (const QString &text) -> QString {
7639 QString result = text;
7640 for (int i = 0; i < result.length(); ++i) {
7641 const QChar c = result[i];
7642 result[i] = c.isUpper() ? c.toLower() : c.toUpper();
7643 }
7644 return result;
7645 });
7646 }
7647
toggleComment(const Range & range)7648 void FakeVimHandler::Private::toggleComment(const Range &range)
7649 {
7650 static const QMap<QString, QString> extensionToCommentString {
7651 {"pri", "#"},
7652 {"pro", "#"},
7653 {"h", "//"},
7654 {"hpp", "//"},
7655 {"cpp", "//"},
7656 };
7657 const QString commentString = extensionToCommentString.value(QFileInfo(m_currentFileName).suffix(), "//");
7658
7659 transformText(range,
7660 [&commentString] (const QString &text) -> QString {
7661
7662 QStringList lines = text.split('\n');
7663
7664 const QRegularExpression checkForComment("^\\s*"
7665 + QRegularExpression::escape(commentString));
7666
7667 const bool firstLineIsComment
7668 = !lines.empty() && lines.front().contains(checkForComment);
7669
7670 for (auto& line : lines) {
7671 if (!line.isEmpty()) {
7672 if (firstLineIsComment) {
7673 const bool hasSpaceAfterCommentString = line.contains(
7674 QRegularExpression(checkForComment.pattern() + "\\s"));
7675 const int sizeToReplace = hasSpaceAfterCommentString ? commentString.size() + 1
7676 : commentString.size();
7677 line.replace(line.indexOf(commentString), sizeToReplace, "");
7678 } else {
7679 const int indexOfFirstNonSpace = line.indexOf(QRegularExpression("[^\\s]"));
7680 line = line.left(indexOfFirstNonSpace) + commentString + " " + line.right(line.size() - indexOfFirstNonSpace);
7681 }
7682 }
7683 }
7684
7685 return lines.size() == 1 ? lines.front() : lines.join("\n");
7686 });
7687 }
7688
exchangeRange(const Range & range)7689 void FakeVimHandler::Private::exchangeRange(const Range &range)
7690 {
7691 if (g.exchangeRange) {
7692 pushUndoState(false);
7693 beginEditBlock();
7694
7695 Range leftRange = *g.exchangeRange;
7696 Range rightRange = range;
7697 if (leftRange.beginPos > rightRange.beginPos)
7698 std::swap(leftRange, rightRange);
7699
7700 // First replace the right range, then left one
7701 // If we did it the other way around, we would invalidate the positions
7702 // of the right range
7703 const QString rightText = selectText(rightRange);
7704 replaceText(rightRange, selectText(leftRange));
7705 replaceText(leftRange, rightText);
7706
7707 g.exchangeRange.reset();
7708
7709 endEditBlock();
7710 } else {
7711 g.exchangeRange = range;
7712 }
7713 }
7714
replaceWithRegister(const Range & range)7715 void FakeVimHandler::Private::replaceWithRegister(const Range &range)
7716 {
7717 replaceText(range, registerContents(m_register));
7718 }
7719
surroundCurrentRange(const Input & input,const QString & prefix)7720 void FakeVimHandler::Private::surroundCurrentRange(const Input &input, const QString &prefix)
7721 {
7722 QString dotCommand;
7723 if (isVisualMode())
7724 dotCommand = visualDotCommand() + "S" + input.asChar();
7725
7726 const bool wasVisualCharMode = isVisualCharMode();
7727 const bool wasVisualLineMode = isVisualLineMode();
7728 leaveVisualMode();
7729
7730 if (dotCommand.isEmpty()) { // i.e. we came from normal mode
7731 dotCommand = dotCommandFromSubMode(g.submode)
7732 + QLatin1Char(g.surroundUpperCaseS ? 'S' : 's')
7733 + g.dotCommand + input.asChar();
7734 }
7735
7736 if (wasVisualCharMode)
7737 setPosition(position() + 1);
7738
7739 QString newFront, newBack;
7740
7741 if (input.is('(') || input.is(')') || input.is('b')) {
7742 newFront = '(';
7743 newBack = ')';
7744 } else if (input.is('{') || input.is('}') || input.is('B')) {
7745 newFront = '{';
7746 newBack = '}';
7747 } else if (input.is('[') || input.is(']')) {
7748 newFront = '[';
7749 newBack = ']';
7750 } else if (input.is('<') || input.is('>') || input.is('t')) {
7751 newFront = '<';
7752 newBack = '>';
7753 } else if (input.is('"') || input.is('\'') || input.is('`')) {
7754 newFront = input.asChar();
7755 newBack = input.asChar();
7756 }
7757
7758 if (g.surroundUpperCaseS || wasVisualLineMode) {
7759 // yS and cS add a new line before and after the surrounded text
7760 newFront += "\n";
7761 if (wasVisualLineMode)
7762 newBack += "\n";
7763 else
7764 newBack = "\n" + newBack;
7765 } else if (input.is('(') || input.is('{') || input.is('[') || input.is('[')) {
7766 // Opening characters add an extra space
7767 newFront = newFront + " ";
7768 newBack = " " + newBack;
7769 }
7770
7771
7772 if (!newFront.isEmpty()) {
7773 transformText(currentRange(), [&](QString text) -> QString {
7774 if (newFront == QChar())
7775 return text.mid(1, text.size() - 2);
7776
7777 const QString newMiddle = (g.submode == ChangeSurroundingSubMode) ?
7778 text.mid(1, text.size() - 2) : text;
7779
7780 return prefix + newFront + newMiddle + newBack;
7781 });
7782 }
7783
7784 // yS, cS and VS also indent the surrounded text
7785 if (g.surroundUpperCaseS || wasVisualLineMode)
7786 replay("=a" + input.asChar());
7787
7788 // Indenting has changed the dotCommand, so now set it back to the correct one
7789 g.dotCommand = dotCommand;
7790 }
7791
replaceText(const Range & range,const QString & str)7792 void FakeVimHandler::Private::replaceText(const Range &range, const QString &str)
7793 {
7794 transformText(range, [&str](const QString &) { return str; } );
7795 }
7796
pasteText(bool afterCursor)7797 void FakeVimHandler::Private::pasteText(bool afterCursor)
7798 {
7799 const QString text = registerContents(m_register);
7800 const RangeMode rangeMode = registerRangeMode(m_register);
7801
7802 beginEditBlock();
7803
7804 // In visual mode paste text only inside selection.
7805 bool pasteAfter = isVisualMode() ? false : afterCursor;
7806
7807 if (isVisualMode())
7808 cutSelectedText(g.submode == ReplaceWithRegisterSubMode ? '-' : '"');
7809
7810 switch (rangeMode) {
7811 case RangeCharMode: {
7812 m_targetColumn = 0;
7813 const int pos = position() + 1;
7814 if (pasteAfter && rightDist() > 0)
7815 moveRight();
7816 insertText(text.repeated(count()));
7817 if (text.contains('\n'))
7818 setPosition(pos);
7819 else
7820 moveLeft();
7821 break;
7822 }
7823 case RangeLineMode:
7824 case RangeLineModeExclusive: {
7825 QTextCursor tc = m_cursor;
7826 moveToStartOfLine();
7827 m_targetColumn = 0;
7828 bool lastLine = false;
7829 if (pasteAfter) {
7830 lastLine = document()->lastBlock() == this->block();
7831 if (lastLine) {
7832 tc.movePosition(EndOfLine, MoveAnchor);
7833 tc.insertBlock();
7834 }
7835 moveDown();
7836 }
7837 const int pos = position();
7838 if (lastLine)
7839 insertText(text.repeated(count()).left(text.size() * count() - 1));
7840 else
7841 insertText(text.repeated(count()));
7842 setPosition(pos);
7843 moveToFirstNonBlankOnLine();
7844 break;
7845 }
7846 case RangeBlockAndTailMode:
7847 case RangeBlockMode: {
7848 const int pos = position();
7849 if (pasteAfter && rightDist() > 0)
7850 moveRight();
7851 QTextCursor tc = m_cursor;
7852 const int col = tc.columnNumber();
7853 QTextBlock block = tc.block();
7854 const QStringList lines = text.split('\n');
7855 for (int i = 0; i < lines.size() - 1; ++i) {
7856 if (!block.isValid()) {
7857 tc.movePosition(EndOfDocument);
7858 tc.insertBlock();
7859 block = tc.block();
7860 }
7861
7862 // resize line
7863 int length = block.length();
7864 int begin = block.position();
7865 if (col >= length) {
7866 tc.setPosition(begin + length - 1);
7867 tc.insertText(QString(col - length + 1, ' '));
7868 } else {
7869 tc.setPosition(begin + col);
7870 }
7871
7872 // insert text
7873 const QString line = lines.at(i).repeated(count());
7874 tc.insertText(line);
7875
7876 // next line
7877 block = block.next();
7878 }
7879 setPosition(pos);
7880 if (pasteAfter)
7881 moveRight();
7882 break;
7883 }
7884 }
7885
7886 endEditBlock();
7887 }
7888
cutSelectedText(int reg)7889 void FakeVimHandler::Private::cutSelectedText(int reg)
7890 {
7891 pushUndoState();
7892
7893 bool visualMode = isVisualMode();
7894 leaveVisualMode();
7895
7896 Range range = currentRange();
7897 if (visualMode && g.rangemode == RangeCharMode)
7898 ++range.endPos;
7899
7900 if (!reg)
7901 reg = m_register;
7902
7903 g.submode = DeleteSubMode;
7904 yankText(range, reg);
7905 removeText(range);
7906 g.submode = NoSubMode;
7907
7908 if (g.rangemode == RangeLineMode)
7909 handleStartOfLine();
7910 else if (g.rangemode == RangeBlockMode)
7911 setPosition(qMin(position(), anchor()));
7912 }
7913
joinLines(int count,bool preserveSpace)7914 void FakeVimHandler::Private::joinLines(int count, bool preserveSpace)
7915 {
7916 int pos = position();
7917 const int blockNumber = m_cursor.blockNumber();
7918
7919 const QString currentLine = lineContents(blockNumber + 1);
7920 const bool startingLineIsComment
7921 = currentLine.contains(QRegularExpression("^\\s*\\/\\/")) // Cpp-style
7922 || currentLine.contains(QRegularExpression("^\\s*\\/?\\*")) // C-style
7923 || currentLine.contains(QRegularExpression("^\\s*#")); // Python/Shell-style
7924
7925 for (int i = qMax(count - 2, 0); i >= 0 && blockNumber < document()->blockCount(); --i) {
7926 moveBehindEndOfLine();
7927 pos = position();
7928 setAnchor();
7929 moveRight();
7930 if (preserveSpace) {
7931 removeText(currentRange());
7932 } else {
7933 while (characterAtCursor() == ' ' || characterAtCursor() == '\t')
7934 moveRight();
7935
7936 // If the line we started from is a comment, remove the comment string from the next line
7937 if (startingLineIsComment && s.formatOptions.value().contains('f')) {
7938 if (characterAtCursor() == '/' && characterAt(position() + 1) == '/')
7939 moveRight(2);
7940 else if (characterAtCursor() == '*' || characterAtCursor() == '#')
7941 moveRight(1);
7942
7943 if (characterAtCursor() == ' ')
7944 moveRight();
7945 }
7946
7947 m_cursor.insertText(QString(' '));
7948 }
7949 }
7950 setPosition(pos);
7951 }
7952
insertNewLine()7953 void FakeVimHandler::Private::insertNewLine()
7954 {
7955 if (m_buffer->editBlockLevel <= 1 && s.passKeys.value()) {
7956 QKeyEvent event(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier, "\n");
7957 if (passEventToEditor(event, m_cursor))
7958 return;
7959 }
7960
7961 insertText(QString("\n"));
7962 insertAutomaticIndentation(true);
7963 }
7964
handleInsertInEditor(const Input & input)7965 bool FakeVimHandler::Private::handleInsertInEditor(const Input &input)
7966 {
7967 if (m_buffer->editBlockLevel > 0 || !s.passKeys.value())
7968 return false;
7969
7970 joinPreviousEditBlock();
7971
7972 QKeyEvent event(QEvent::KeyPress, input.key(), input.modifiers(), input.text());
7973 setAnchor();
7974 if (!passEventToEditor(event, m_cursor))
7975 return !m_textedit && !m_plaintextedit; // Mark event as handled if it has destroyed editor.
7976
7977 endEditBlock();
7978
7979 setTargetColumn();
7980
7981 return true;
7982 }
7983
passEventToEditor(QEvent & event,QTextCursor & tc)7984 bool FakeVimHandler::Private::passEventToEditor(QEvent &event, QTextCursor &tc)
7985 {
7986 removeEventFilter();
7987 q->requestDisableBlockSelection();
7988
7989 setThinCursor();
7990 EDITOR(setTextCursor(tc));
7991
7992 bool accepted = QApplication::sendEvent(editor(), &event);
7993 if (!m_textedit && !m_plaintextedit)
7994 return false;
7995
7996 if (accepted)
7997 tc = editorCursor();
7998
7999 return accepted;
8000 }
8001
lineContents(int line) const8002 QString FakeVimHandler::Private::lineContents(int line) const
8003 {
8004 return document()->findBlockByLineNumber(line - 1).text();
8005 }
8006
textAt(int from,int to) const8007 QString FakeVimHandler::Private::textAt(int from, int to) const
8008 {
8009 QTextCursor tc(document());
8010 tc.setPosition(from);
8011 tc.setPosition(to, KeepAnchor);
8012 return tc.selectedText().replace(ParagraphSeparator, '\n');
8013 }
8014
setLineContents(int line,const QString & contents)8015 void FakeVimHandler::Private::setLineContents(int line, const QString &contents)
8016 {
8017 QTextBlock block = document()->findBlockByLineNumber(line - 1);
8018 QTextCursor tc = m_cursor;
8019 const int begin = block.position();
8020 const int len = block.length();
8021 tc.setPosition(begin);
8022 tc.setPosition(begin + len - 1, KeepAnchor);
8023 tc.insertText(contents);
8024 }
8025
blockBoundary(const QString & left,const QString & right,bool closing,int count) const8026 int FakeVimHandler::Private::blockBoundary(const QString &left,
8027 const QString &right, bool closing, int count) const
8028 {
8029 const QString &begin = closing ? left : right;
8030 const QString &end = closing ? right : left;
8031
8032 // shift cursor if it is already on opening/closing string
8033 QTextCursor tc1 = m_cursor;
8034 int pos = tc1.position();
8035 int max = document()->characterCount();
8036 int sz = left.size();
8037 int from = qMax(pos - sz + 1, 0);
8038 int to = qMin(pos + sz, max);
8039 tc1.setPosition(from);
8040 tc1.setPosition(to, KeepAnchor);
8041 int i = tc1.selectedText().indexOf(left);
8042 if (i != -1) {
8043 // - on opening string:
8044 tc1.setPosition(from + i + sz);
8045 } else {
8046 sz = right.size();
8047 from = qMax(pos - sz + 1, 0);
8048 to = qMin(pos + sz, max);
8049 tc1.setPosition(from);
8050 tc1.setPosition(to, KeepAnchor);
8051 i = tc1.selectedText().indexOf(right);
8052 if (i != -1) {
8053 // - on closing string:
8054 tc1.setPosition(from + i);
8055 } else {
8056 tc1 = m_cursor;
8057 }
8058 }
8059
8060 QTextCursor tc2 = tc1;
8061 QTextDocument::FindFlags flags(closing ? 0 : QTextDocument::FindBackward);
8062 int level = 0;
8063 int counter = 0;
8064 while (true) {
8065 tc2 = document()->find(end, tc2, flags);
8066 if (tc2.isNull())
8067 return -1;
8068 if (!tc1.isNull())
8069 tc1 = document()->find(begin, tc1, flags);
8070
8071 while (!tc1.isNull() && (closing ? (tc1 < tc2) : (tc2 < tc1))) {
8072 ++level;
8073 tc1 = document()->find(begin, tc1, flags);
8074 }
8075
8076 while (level > 0
8077 && (tc1.isNull() || (closing ? (tc2 < tc1) : (tc1 < tc2)))) {
8078 --level;
8079 tc2 = document()->find(end, tc2, flags);
8080 if (tc2.isNull())
8081 return -1;
8082 }
8083
8084 if (level == 0
8085 && (tc1.isNull() || (closing ? (tc2 < tc1) : (tc1 < tc2)))) {
8086 ++counter;
8087 if (counter >= count)
8088 break;
8089 }
8090 }
8091
8092 return tc2.position() - end.size();
8093 }
8094
lineNumber(const QTextBlock & block) const8095 int FakeVimHandler::Private::lineNumber(const QTextBlock &block) const
8096 {
8097 if (block.isVisible())
8098 return block.firstLineNumber() + 1;
8099
8100 // Folded block has line number of the nearest previous visible line.
8101 QTextBlock block2 = block;
8102 while (block2.isValid() && !block2.isVisible())
8103 block2 = block2.previous();
8104 return block2.firstLineNumber() + 1;
8105 }
8106
columnAt(int pos) const8107 int FakeVimHandler::Private::columnAt(int pos) const
8108 {
8109 return pos - blockAt(pos).position();
8110 }
8111
blockNumberAt(int pos) const8112 int FakeVimHandler::Private::blockNumberAt(int pos) const
8113 {
8114 return blockAt(pos).blockNumber();
8115 }
8116
blockAt(int pos) const8117 QTextBlock FakeVimHandler::Private::blockAt(int pos) const
8118 {
8119 return document()->findBlock(pos);
8120 }
8121
nextLine(const QTextBlock & block) const8122 QTextBlock FakeVimHandler::Private::nextLine(const QTextBlock &block) const
8123 {
8124 return blockAt(block.position() + block.length());
8125 }
8126
previousLine(const QTextBlock & block) const8127 QTextBlock FakeVimHandler::Private::previousLine(const QTextBlock &block) const
8128 {
8129 return blockAt(block.position() - 1);
8130 }
8131
firstPositionInLine(int line,bool onlyVisibleLines) const8132 int FakeVimHandler::Private::firstPositionInLine(int line, bool onlyVisibleLines) const
8133 {
8134 QTextBlock block = onlyVisibleLines ? document()->findBlockByLineNumber(line - 1)
8135 : document()->findBlockByNumber(line - 1);
8136 return block.position();
8137 }
8138
lastPositionInLine(int line,bool onlyVisibleLines) const8139 int FakeVimHandler::Private::lastPositionInLine(int line, bool onlyVisibleLines) const
8140 {
8141 QTextBlock block;
8142 if (onlyVisibleLines) {
8143 block = document()->findBlockByLineNumber(line - 1);
8144 // respect folds and wrapped lines
8145 do {
8146 block = nextLine(block);
8147 } while (block.isValid() && !block.isVisible());
8148 if (block.isValid()) {
8149 if (line > 0)
8150 block = block.previous();
8151 } else {
8152 block = document()->lastBlock();
8153 }
8154 } else {
8155 block = document()->findBlockByNumber(line - 1);
8156 }
8157
8158 const int position = block.position() + block.length() - 1;
8159 if (block.length() > 1 && !isVisualMode() && !isInsertMode())
8160 return position - 1;
8161 return position;
8162 }
8163
lineForPosition(int pos) const8164 int FakeVimHandler::Private::lineForPosition(int pos) const
8165 {
8166 const QTextBlock block = blockAt(pos);
8167 if (!block.isValid())
8168 return 0;
8169 const int positionInBlock = pos - block.position();
8170 const int lineNumberInBlock = block.layout()->lineForTextPosition(positionInBlock).lineNumber();
8171 return block.firstLineNumber() + lineNumberInBlock + 1;
8172 }
8173
toggleVisualMode(VisualMode visualMode)8174 void FakeVimHandler::Private::toggleVisualMode(VisualMode visualMode)
8175 {
8176 if (visualMode == g.visualMode) {
8177 leaveVisualMode();
8178 } else {
8179 m_positionPastEnd = false;
8180 m_anchorPastEnd = false;
8181 g.visualMode = visualMode;
8182 m_buffer->lastVisualMode = visualMode;
8183 }
8184 }
8185
leaveVisualMode()8186 void FakeVimHandler::Private::leaveVisualMode()
8187 {
8188 if (!isVisualMode())
8189 return;
8190
8191 if (isVisualLineMode()) {
8192 g.rangemode = RangeLineMode;
8193 g.movetype = MoveLineWise;
8194 } else if (isVisualCharMode()) {
8195 g.rangemode = RangeCharMode;
8196 g.movetype = MoveInclusive;
8197 } else if (isVisualBlockMode()) {
8198 g.rangemode = m_visualTargetColumn == -1 ? RangeBlockAndTailMode : RangeBlockMode;
8199 g.movetype = MoveInclusive;
8200 }
8201
8202 g.visualMode = NoVisualMode;
8203 }
8204
saveLastVisualMode()8205 void FakeVimHandler::Private::saveLastVisualMode()
8206 {
8207 if (isVisualMode() && g.mode == CommandMode && g.submode == NoSubMode) {
8208 setMark('<', markLessPosition());
8209 setMark('>', markGreaterPosition());
8210 m_buffer->lastVisualModeInverted = anchor() > position();
8211 m_buffer->lastVisualMode = g.visualMode;
8212 }
8213 }
8214
editor() const8215 QWidget *FakeVimHandler::Private::editor() const
8216 {
8217 return m_textedit
8218 ? static_cast<QWidget *>(m_textedit)
8219 : static_cast<QWidget *>(m_plaintextedit);
8220 }
8221
joinPreviousEditBlock()8222 void FakeVimHandler::Private::joinPreviousEditBlock()
8223 {
8224 UNDO_DEBUG("JOIN");
8225 if (m_buffer->breakEditBlock) {
8226 beginEditBlock();
8227 QTextCursor tc(m_cursor);
8228 tc.setPosition(tc.position());
8229 tc.beginEditBlock();
8230 tc.insertText("X");
8231 tc.deletePreviousChar();
8232 tc.endEditBlock();
8233 m_buffer->breakEditBlock = false;
8234 } else {
8235 if (m_buffer->editBlockLevel == 0 && !m_buffer->undo.empty())
8236 m_buffer->undoState = m_buffer->undo.pop();
8237 beginEditBlock();
8238 }
8239 }
8240
beginEditBlock(bool largeEditBlock)8241 void FakeVimHandler::Private::beginEditBlock(bool largeEditBlock)
8242 {
8243 UNDO_DEBUG("BEGIN EDIT BLOCK" << m_buffer->editBlockLevel + 1);
8244 if (!largeEditBlock && !m_buffer->undoState.isValid())
8245 pushUndoState(false);
8246 if (m_buffer->editBlockLevel == 0)
8247 m_buffer->breakEditBlock = true;
8248 ++m_buffer->editBlockLevel;
8249 }
8250
endEditBlock()8251 void FakeVimHandler::Private::endEditBlock()
8252 {
8253 UNDO_DEBUG("END EDIT BLOCK" << m_buffer->editBlockLevel);
8254 if (m_buffer->editBlockLevel <= 0) {
8255 qWarning("beginEditBlock() not called before endEditBlock()!");
8256 return;
8257 }
8258 --m_buffer->editBlockLevel;
8259 if (m_buffer->editBlockLevel == 0 && m_buffer->undoState.isValid()) {
8260 m_buffer->undo.push(m_buffer->undoState);
8261 m_buffer->undoState = State();
8262 }
8263 if (m_buffer->editBlockLevel == 0)
8264 m_buffer->breakEditBlock = false;
8265 }
8266
onContentsChanged(int position,int charsRemoved,int charsAdded)8267 void FakeVimHandler::Private::onContentsChanged(int position, int charsRemoved, int charsAdded)
8268 {
8269 // Record inserted and deleted text in insert mode.
8270 if (isInsertMode() && (charsAdded > 0 || charsRemoved > 0) && canModifyBufferData()) {
8271 BufferData::InsertState &insertState = m_buffer->insertState;
8272 const int oldPosition = insertState.pos2;
8273 if (!isInsertStateValid()) {
8274 insertState.pos1 = oldPosition;
8275 g.dotCommand = "i";
8276 resetCount();
8277 }
8278
8279 // Ignore changes outside inserted text (e.g. renaming other occurrences of a variable).
8280 if (position + charsRemoved >= insertState.pos1 && position <= insertState.pos2) {
8281 if (charsRemoved > 0) {
8282 // Assume that in a manual edit operation a text can be removed only
8283 // in front of cursor (<DELETE>) or behind it (<BACKSPACE>).
8284
8285 // If the recorded amount of backspace/delete keys doesn't correspond with
8286 // number of removed characters, assume that the document has been changed
8287 // externally and invalidate current insert state.
8288
8289 const bool wholeDocumentChanged =
8290 charsRemoved > 1
8291 && charsAdded > 0
8292 && charsAdded + 1 == document()->characterCount();
8293
8294 if (position < insertState.pos1) {
8295 // <BACKSPACE>
8296 const int backspaceCount = insertState.pos1 - position;
8297 if (backspaceCount != charsRemoved || (oldPosition == charsRemoved && wholeDocumentChanged)) {
8298 invalidateInsertState();
8299 } else {
8300 const QString inserted = textAt(position, oldPosition);
8301 const QString removed = insertState.textBeforeCursor.right(backspaceCount);
8302 // Ignore backspaces if same text was just inserted.
8303 if ( !inserted.endsWith(removed) ) {
8304 insertState.backspaces += backspaceCount;
8305 insertState.pos1 = position;
8306 insertState.pos2 = qMax(position, insertState.pos2 - backspaceCount);
8307 }
8308 }
8309 } else if (position + charsRemoved > insertState.pos2) {
8310 // <DELETE>
8311 const int deleteCount = position + charsRemoved - insertState.pos2;
8312 if (deleteCount != charsRemoved || (oldPosition == 0 && wholeDocumentChanged))
8313 invalidateInsertState();
8314 else
8315 insertState.deletes += deleteCount;
8316 }
8317 } else if (charsAdded > 0 && insertState.insertingSpaces) {
8318 for (int i = position; i < position + charsAdded; ++i) {
8319 const QChar c = characterAt(i);
8320 if (c.unicode() == ' ' || c.unicode() == '\t')
8321 insertState.spaces.insert(i);
8322 }
8323 }
8324
8325 const int newPosition = position + charsAdded;
8326 insertState.pos2 = qMax(insertState.pos2 + charsAdded - charsRemoved, newPosition);
8327 insertState.textBeforeCursor = textAt(block().position(), newPosition);
8328 }
8329 }
8330
8331 if (!m_highlighted.isEmpty())
8332 q->highlightMatches(m_highlighted);
8333 }
8334
onCursorPositionChanged()8335 void FakeVimHandler::Private::onCursorPositionChanged()
8336 {
8337 if (!m_inFakeVim) {
8338 m_cursorNeedsUpdate = true;
8339
8340 // Selecting text with mouse disables the thick cursor so it's more obvious
8341 // that extra character under cursor is not selected when moving text around or
8342 // making operations on text outside FakeVim mode.
8343 setThinCursor(g.mode == InsertMode || editorCursor().hasSelection());
8344 }
8345 }
8346
onUndoCommandAdded()8347 void FakeVimHandler::Private::onUndoCommandAdded()
8348 {
8349 if (!canModifyBufferData())
8350 return;
8351
8352 // Undo commands removed?
8353 UNDO_DEBUG("Undo added" << "previous: REV" << m_buffer->lastRevision);
8354 if (m_buffer->lastRevision >= revision()) {
8355 UNDO_DEBUG("UNDO REMOVED!");
8356 const int removed = m_buffer->lastRevision - revision();
8357 for (int i = m_buffer->undo.size() - 1; i >= 0; --i) {
8358 if ((m_buffer->undo[i].revision -= removed) < 0) {
8359 m_buffer->undo.remove(0, i + 1);
8360 break;
8361 }
8362 }
8363 }
8364
8365 m_buffer->redo.clear();
8366 // External change while FakeVim disabled.
8367 if (m_buffer->editBlockLevel == 0 && !m_buffer->undo.isEmpty() && !isInsertMode())
8368 m_buffer->undo.push(State());
8369 }
8370
onInputTimeout()8371 void FakeVimHandler::Private::onInputTimeout()
8372 {
8373 enterFakeVim();
8374 EventResult result = handleKey(Input());
8375 leaveFakeVim(result);
8376 }
8377
onFixCursorTimeout()8378 void FakeVimHandler::Private::onFixCursorTimeout()
8379 {
8380 if (editor())
8381 fixExternalCursorPosition(editor()->hasFocus() && !isCommandLineMode());
8382 }
8383
currentModeCode() const8384 char FakeVimHandler::Private::currentModeCode() const
8385 {
8386 if (g.mode == ExMode)
8387 return 'c';
8388 else if (isVisualMode())
8389 return 'v';
8390 else if (isOperatorPending())
8391 return 'o';
8392 else if (g.mode == CommandMode)
8393 return 'n';
8394 else if (g.submode != NoSubMode)
8395 return ' ';
8396 else
8397 return 'i';
8398 }
8399
undoRedo(bool undo)8400 void FakeVimHandler::Private::undoRedo(bool undo)
8401 {
8402 UNDO_DEBUG((undo ? "UNDO" : "REDO"));
8403
8404 // FIXME: That's only an approximaxtion. The real solution might
8405 // be to store marks and old userData with QTextBlock setUserData
8406 // and retrieve them afterward.
8407 QStack<State> &stack = undo ? m_buffer->undo : m_buffer->redo;
8408 QStack<State> &stack2 = undo ? m_buffer->redo : m_buffer->undo;
8409
8410 State state = m_buffer->undoState.isValid() ? m_buffer->undoState
8411 : !stack.empty() ? stack.pop() : State();
8412
8413 CursorPosition lastPos(m_cursor);
8414 if (undo ? !document()->isUndoAvailable() : !document()->isRedoAvailable()) {
8415 const QString msg = undo ? Tr::tr("Already at oldest change.")
8416 : Tr::tr("Already at newest change.");
8417 showMessage(MessageInfo, msg);
8418 UNDO_DEBUG(msg);
8419 return;
8420 }
8421 clearMessage();
8422
8423 ++m_buffer->editBlockLevel;
8424
8425 // Do undo/redo [count] times to reach previous revision.
8426 const int previousRevision = revision();
8427 if (undo) {
8428 do {
8429 EDITOR(undo());
8430 } while (document()->isUndoAvailable() && state.revision >= 0 && state.revision < revision());
8431 } else {
8432 do {
8433 EDITOR(redo());
8434 } while (document()->isRedoAvailable() && state.revision > revision());
8435 }
8436
8437 --m_buffer->editBlockLevel;
8438
8439 if (state.isValid()) {
8440 Marks marks = m_buffer->marks;
8441 marks.swap(state.marks);
8442 updateMarks(marks);
8443 m_buffer->lastVisualMode = state.lastVisualMode;
8444 m_buffer->lastVisualModeInverted = state.lastVisualModeInverted;
8445 setMark('.', state.position);
8446 setMark('\'', lastPos);
8447 setMark('`', lastPos);
8448 setCursorPosition(state.position);
8449 setAnchor();
8450 state.revision = previousRevision;
8451 } else {
8452 updateFirstVisibleLine();
8453 pullCursor();
8454 }
8455 stack2.push(state);
8456
8457 setTargetColumn();
8458 if (atEndOfLine())
8459 moveLeft();
8460
8461 UNDO_DEBUG((undo ? "UNDONE" : "REDONE"));
8462 }
8463
undo()8464 void FakeVimHandler::Private::undo()
8465 {
8466 undoRedo(true);
8467 }
8468
redo()8469 void FakeVimHandler::Private::redo()
8470 {
8471 undoRedo(false);
8472 }
8473
updateCursorShape()8474 void FakeVimHandler::Private::updateCursorShape()
8475 {
8476 setThinCursor(
8477 g.mode == InsertMode
8478 || isVisualLineMode()
8479 || isVisualBlockMode()
8480 || isCommandLineMode()
8481 || !editor()->hasFocus());
8482 }
8483
setThinCursor(bool enable)8484 void FakeVimHandler::Private::setThinCursor(bool enable)
8485 {
8486 EDITOR(setOverwriteMode(!enable));
8487 }
8488
hasThinCursor() const8489 bool FakeVimHandler::Private::hasThinCursor() const
8490 {
8491 return !EDITOR(overwriteMode());
8492 }
8493
enterReplaceMode()8494 void FakeVimHandler::Private::enterReplaceMode()
8495 {
8496 enterInsertOrReplaceMode(ReplaceMode);
8497 }
8498
enterInsertMode()8499 void FakeVimHandler::Private::enterInsertMode()
8500 {
8501 enterInsertOrReplaceMode(InsertMode);
8502 }
8503
enterInsertOrReplaceMode(Mode mode)8504 void FakeVimHandler::Private::enterInsertOrReplaceMode(Mode mode)
8505 {
8506 if (mode != InsertMode && mode != ReplaceMode) {
8507 qWarning("Unexpected mode");
8508 return;
8509 }
8510 if (g.mode == mode)
8511 return;
8512
8513 g.mode = mode;
8514
8515 if (g.returnToMode == mode) {
8516 // Returning to insert mode after <C-O>.
8517 clearCurrentMode();
8518 moveToTargetColumn();
8519 invalidateInsertState();
8520 } else {
8521 // Entering insert mode from command mode.
8522 if (mode == InsertMode) {
8523 // m_targetColumn shouldn't be -1 (end of line).
8524 if (m_targetColumn == -1)
8525 setTargetColumn();
8526 }
8527
8528 g.submode = NoSubMode;
8529 g.subsubmode = NoSubSubMode;
8530 g.returnToMode = mode;
8531 clearLastInsertion();
8532 }
8533 }
8534
enterVisualInsertMode(QChar command)8535 void FakeVimHandler::Private::enterVisualInsertMode(QChar command)
8536 {
8537 if (isVisualBlockMode()) {
8538 bool append = command == 'A';
8539 bool change = command == 's' || command == 'c';
8540
8541 leaveVisualMode();
8542
8543 const CursorPosition lastAnchor = markLessPosition();
8544 const CursorPosition lastPosition = markGreaterPosition();
8545 CursorPosition pos(lastAnchor.line,
8546 append ? qMax(lastPosition.column, lastAnchor.column) + 1
8547 : qMin(lastPosition.column, lastAnchor.column));
8548
8549 if (append) {
8550 m_visualBlockInsert = m_visualTargetColumn == -1 ? AppendToEndOfLineBlockInsertMode
8551 : AppendBlockInsertMode;
8552 } else if (change) {
8553 m_visualBlockInsert = ChangeBlockInsertMode;
8554 beginEditBlock();
8555 cutSelectedText();
8556 endEditBlock();
8557 } else {
8558 m_visualBlockInsert = InsertBlockInsertMode;
8559 }
8560
8561 setCursorPosition(pos);
8562 if (m_visualBlockInsert == AppendToEndOfLineBlockInsertMode)
8563 moveBehindEndOfLine();
8564 } else {
8565 m_visualBlockInsert = NoneBlockInsertMode;
8566 leaveVisualMode();
8567 if (command == 'I') {
8568 if (lineForPosition(anchor()) <= lineForPosition(position())) {
8569 setPosition(qMin(anchor(), position()));
8570 moveToStartOfLine();
8571 }
8572 } else if (command == 'A') {
8573 if (lineForPosition(anchor()) <= lineForPosition(position())) {
8574 setPosition(position());
8575 moveRight(qMin(rightDist(), 1));
8576 } else {
8577 setPosition(anchor());
8578 moveToStartOfLine();
8579 }
8580 }
8581 }
8582
8583 setAnchor();
8584 if (m_visualBlockInsert != ChangeBlockInsertMode)
8585 breakEditBlock();
8586 enterInsertMode();
8587 }
8588
enterCommandMode(Mode returnToMode)8589 void FakeVimHandler::Private::enterCommandMode(Mode returnToMode)
8590 {
8591 if (g.isRecording && isCommandLineMode())
8592 record(Input(Key_Escape, NoModifier));
8593
8594 if (isNoVisualMode()) {
8595 if (atEndOfLine()) {
8596 m_cursor.movePosition(Left, KeepAnchor);
8597 if (m_targetColumn != -1)
8598 setTargetColumn();
8599 }
8600 setAnchor();
8601 }
8602
8603 g.mode = CommandMode;
8604 clearCurrentMode();
8605 g.returnToMode = returnToMode;
8606 m_positionPastEnd = false;
8607 m_anchorPastEnd = false;
8608 }
8609
enterExMode(const QString & contents)8610 void FakeVimHandler::Private::enterExMode(const QString &contents)
8611 {
8612 g.currentMessage.clear();
8613 g.commandBuffer.clear();
8614 if (isVisualMode())
8615 g.commandBuffer.setContents(QString("'<,'>") + contents, contents.size() + 5);
8616 else
8617 g.commandBuffer.setContents(contents, contents.size());
8618 g.mode = ExMode;
8619 g.submode = NoSubMode;
8620 g.subsubmode = NoSubSubMode;
8621 unfocus();
8622 }
8623
recordJump(int position)8624 void FakeVimHandler::Private::recordJump(int position)
8625 {
8626 CursorPosition pos = position >= 0 ? CursorPosition(document(), position)
8627 : CursorPosition(m_cursor);
8628 setMark('\'', pos);
8629 setMark('`', pos);
8630 if (m_buffer->jumpListUndo.isEmpty() || m_buffer->jumpListUndo.top() != pos)
8631 m_buffer->jumpListUndo.push(pos);
8632 m_buffer->jumpListRedo.clear();
8633 UNDO_DEBUG("jumps: " << m_buffer->jumpListUndo);
8634 }
8635
jump(int distance)8636 void FakeVimHandler::Private::jump(int distance)
8637 {
8638 QStack<CursorPosition> &from = (distance > 0) ? m_buffer->jumpListRedo : m_buffer->jumpListUndo;
8639 QStack<CursorPosition> &to = (distance > 0) ? m_buffer->jumpListUndo : m_buffer->jumpListRedo;
8640 int len = qMin(qAbs(distance), from.size());
8641 CursorPosition m(m_cursor);
8642 setMark('\'', m);
8643 setMark('`', m);
8644 for (int i = 0; i < len; ++i) {
8645 to.push(m);
8646 setCursorPosition(from.top());
8647 from.pop();
8648 }
8649 setTargetColumn();
8650 }
8651
indentation(const QString & line) const8652 Column FakeVimHandler::Private::indentation(const QString &line) const
8653 {
8654 int ts = s.tabStop.value();
8655 int physical = 0;
8656 int logical = 0;
8657 int n = line.size();
8658 while (physical < n) {
8659 QChar c = line.at(physical);
8660 if (c == ' ')
8661 ++logical;
8662 else if (c == '\t')
8663 logical += ts - logical % ts;
8664 else
8665 break;
8666 ++physical;
8667 }
8668 return Column(physical, logical);
8669 }
8670
tabExpand(int n) const8671 QString FakeVimHandler::Private::tabExpand(int n) const
8672 {
8673 int ts = s.tabStop.value();
8674 if (s.expandTab.value() || ts < 1)
8675 return QString(n, ' ');
8676 return QString(n / ts, '\t')
8677 + QString(n % ts, ' ');
8678 }
8679
insertAutomaticIndentation(bool goingDown,bool forceAutoIndent)8680 void FakeVimHandler::Private::insertAutomaticIndentation(bool goingDown, bool forceAutoIndent)
8681 {
8682 if (!forceAutoIndent && !s.autoIndent.value() && !s.smartIndent.value())
8683 return;
8684
8685 if (s.smartIndent.value()) {
8686 QTextBlock bl = block();
8687 Range range(bl.position(), bl.position());
8688 indentText(range, '\n');
8689 } else {
8690 QTextBlock bl = goingDown ? block().previous() : block().next();
8691 QString text = bl.text();
8692 int pos = 0;
8693 int n = text.size();
8694 while (pos < n && text.at(pos).isSpace())
8695 ++pos;
8696 text.truncate(pos);
8697 // FIXME: handle 'smartindent' and 'cindent'
8698 insertText(text);
8699 }
8700 }
8701
handleStartOfLine()8702 void FakeVimHandler::Private::handleStartOfLine()
8703 {
8704 if (s.startOfLine.value())
8705 moveToFirstNonBlankOnLine();
8706 }
8707
replay(const QString & command,int repeat)8708 void FakeVimHandler::Private::replay(const QString &command, int repeat)
8709 {
8710 if (repeat <= 0)
8711 return;
8712
8713 //qDebug() << "REPLAY: " << quoteUnprintable(command);
8714 clearCurrentMode();
8715 const Inputs inputs(command);
8716 for (int i = 0; i < repeat; ++i) {
8717 for (const Input &in : inputs) {
8718 if (handleDefaultKey(in) != EventHandled)
8719 return;
8720 }
8721 }
8722 }
8723
visualDotCommand() const8724 QString FakeVimHandler::Private::visualDotCommand() const
8725 {
8726 QTextCursor start(m_cursor);
8727 QTextCursor end(start);
8728 end.setPosition(end.anchor());
8729
8730 QString command;
8731
8732 if (isVisualCharMode())
8733 command = "v";
8734 else if (isVisualLineMode())
8735 command = "V";
8736 else if (isVisualBlockMode())
8737 command = "<c-v>";
8738 else
8739 return QString();
8740
8741 const int down = qAbs(start.blockNumber() - end.blockNumber());
8742 if (down != 0)
8743 command.append(QString("%1j").arg(down));
8744
8745 const int right = start.positionInBlock() - end.positionInBlock();
8746 if (right != 0) {
8747 command.append(QString::number(qAbs(right)));
8748 command.append(QLatin1Char(right < 0 && isVisualBlockMode() ? 'h' : 'l'));
8749 }
8750
8751 return command;
8752 }
8753
selectTextObject(bool simple,bool inner)8754 void FakeVimHandler::Private::selectTextObject(bool simple, bool inner)
8755 {
8756 const int position1 = this->position();
8757 const int anchor1 = this->anchor();
8758 bool setupAnchor = (position1 == anchor1);
8759 bool forward = anchor1 <= position1;
8760 const int repeat = count();
8761
8762 // set anchor if not already set
8763 if (setupAnchor) {
8764 // Select nothing with 'inner' on empty line.
8765 if (inner && atEmptyLine() && repeat == 1) {
8766 g.movetype = MoveExclusive;
8767 return;
8768 }
8769 moveToBoundaryStart(1, simple, false);
8770 setAnchor();
8771 } else if (forward) {
8772 moveToNextCharacter();
8773 } else {
8774 moveToPreviousCharacter();
8775 }
8776
8777 if (inner) {
8778 moveToBoundaryEnd(repeat, simple);
8779 } else {
8780 const int direction = forward ? 1 : -1;
8781 for (int i = 0; i < repeat; ++i) {
8782 // select leading spaces
8783 bool leadingSpace = characterAtCursor().isSpace();
8784 if (leadingSpace) {
8785 if (forward)
8786 moveToNextBoundaryStart(1, simple);
8787 else
8788 moveToNextBoundaryEnd(1, simple, false);
8789 }
8790
8791 // select word
8792 if (forward)
8793 moveToWordEnd(1, simple);
8794 else
8795 moveToWordStart(1, simple, false);
8796
8797 // select trailing spaces if no leading space
8798 QChar afterCursor = characterAt(position() + direction);
8799 if (!leadingSpace && afterCursor.isSpace() && afterCursor != ParagraphSeparator
8800 && !atBlockStart()) {
8801 if (forward)
8802 moveToNextBoundaryEnd(1, simple);
8803 else
8804 moveToNextBoundaryStart(1, simple, false);
8805 }
8806
8807 // if there are no trailing spaces in selection select all leading spaces
8808 // after previous character
8809 if (setupAnchor && (!characterAtCursor().isSpace() || atBlockEnd())) {
8810 int min = block().position();
8811 int pos = anchor();
8812 while (pos >= min && characterAt(--pos).isSpace()) {}
8813 if (pos >= min)
8814 setAnchorAndPosition(pos + 1, position());
8815 }
8816
8817 if (i + 1 < repeat) {
8818 if (forward)
8819 moveToNextCharacter();
8820 else
8821 moveToPreviousCharacter();
8822 }
8823 }
8824 }
8825
8826 if (inner) {
8827 g.movetype = MoveInclusive;
8828 } else {
8829 g.movetype = MoveExclusive;
8830 if (isNoVisualMode())
8831 moveToNextCharacter();
8832 else if (isVisualLineMode())
8833 g.visualMode = VisualCharMode;
8834 }
8835
8836 setTargetColumn();
8837 }
8838
selectWordTextObject(bool inner)8839 void FakeVimHandler::Private::selectWordTextObject(bool inner)
8840 {
8841 selectTextObject(false, inner);
8842 }
8843
selectWORDTextObject(bool inner)8844 void FakeVimHandler::Private::selectWORDTextObject(bool inner)
8845 {
8846 selectTextObject(true, inner);
8847 }
8848
selectSentenceTextObject(bool inner)8849 void FakeVimHandler::Private::selectSentenceTextObject(bool inner)
8850 {
8851 Q_UNUSED(inner)
8852 }
8853
selectParagraphTextObject(bool inner)8854 void FakeVimHandler::Private::selectParagraphTextObject(bool inner)
8855 {
8856 const QTextCursor oldCursor = m_cursor;
8857 const VisualMode oldVisualMode = g.visualMode;
8858
8859 const int anchorBlock = blockNumberAt(anchor());
8860 const int positionBlock = blockNumberAt(position());
8861 const bool setupAnchor = anchorBlock == positionBlock;
8862 int repeat = count();
8863
8864 // If anchor and position are in the same block,
8865 // start line selection at beginning of current paragraph.
8866 if (setupAnchor) {
8867 moveToParagraphStartOrEnd(-1);
8868 setAnchor();
8869
8870 if (!isVisualLineMode() && isVisualMode())
8871 toggleVisualMode(VisualLineMode);
8872 }
8873
8874 const bool forward = anchor() <= position();
8875 const int d = forward ? 1 : -1;
8876
8877 bool startsAtParagraph = !atEmptyLine(position());
8878
8879 moveToParagraphStartOrEnd(d);
8880
8881 // If selection already changed, decreate count.
8882 if ((setupAnchor && g.submode != NoSubMode)
8883 || oldVisualMode != g.visualMode
8884 || m_cursor != oldCursor)
8885 {
8886 --repeat;
8887 if (!inner) {
8888 moveDown(d);
8889 moveToParagraphStartOrEnd(d);
8890 startsAtParagraph = !startsAtParagraph;
8891 }
8892 }
8893
8894 if (repeat > 0) {
8895 bool isCountEven = repeat % 2 == 0;
8896 bool endsOnParagraph =
8897 inner ? isCountEven == startsAtParagraph : startsAtParagraph;
8898
8899 if (inner) {
8900 repeat = repeat / 2;
8901 if (!isCountEven || endsOnParagraph)
8902 ++repeat;
8903 } else {
8904 if (endsOnParagraph)
8905 ++repeat;
8906 }
8907
8908 if (!moveToNextParagraph(d * repeat)) {
8909 m_cursor = oldCursor;
8910 g.visualMode = oldVisualMode;
8911 return;
8912 }
8913
8914 if (endsOnParagraph && atEmptyLine())
8915 moveUp(d);
8916 else
8917 moveToParagraphStartOrEnd(d);
8918 }
8919
8920 if (!inner && setupAnchor && !atEmptyLine() && !atEmptyLine(anchor())) {
8921 // If position cannot select empty lines, try to select them with anchor.
8922 setAnchorAndPosition(position(), anchor());
8923 moveToNextParagraph(-d);
8924 moveToParagraphStartOrEnd(-d);
8925 setAnchorAndPosition(position(), anchor());
8926 }
8927
8928 recordJump(oldCursor.position());
8929 setTargetColumn();
8930 g.movetype = MoveLineWise;
8931 }
8932
selectBlockTextObject(bool inner,QChar left,QChar right)8933 bool FakeVimHandler::Private::selectBlockTextObject(bool inner,
8934 QChar left, QChar right)
8935 {
8936 int p1 = blockBoundary(left, right, false, count());
8937 if (p1 == -1)
8938 return false;
8939
8940 int p2 = blockBoundary(left, right, true, count());
8941 if (p2 == -1)
8942 return false;
8943
8944 g.movetype = MoveExclusive;
8945
8946 if (inner) {
8947 p1 += 1;
8948 bool moveStart = characterAt(p1) == ParagraphSeparator;
8949 bool moveEnd = isFirstNonBlankOnLine(p2);
8950 if (moveStart)
8951 ++p1;
8952 if (moveEnd)
8953 p2 = blockAt(p2).position() - 1;
8954 if (moveStart && moveEnd)
8955 g.movetype = MoveLineWise;
8956 } else {
8957 p2 += 1;
8958 }
8959
8960 if (isVisualMode())
8961 --p2;
8962
8963 setAnchorAndPosition(p1, p2);
8964
8965 return true;
8966 }
8967
changeNumberTextObject(int count)8968 bool FakeVimHandler::Private::changeNumberTextObject(int count)
8969 {
8970 const QTextBlock block = this->block();
8971 const QString lineText = block.text();
8972 const int posMin = m_cursor.positionInBlock() + 1;
8973
8974 // find first decimal, hexadecimal or octal number under or after cursor position
8975 QRegularExpression re("(0[xX])(0*[0-9a-fA-F]+)|(0)(0*[0-7]+)(?=\\D|$)|(\\d+)");
8976 QRegularExpressionMatch match;
8977 QRegularExpressionMatchIterator it = re.globalMatch(lineText);
8978 while (true) {
8979 if (!it.hasNext())
8980 return false;
8981 match = it.next();
8982 if (match.capturedEnd() >= posMin)
8983 break;
8984 }
8985 int pos = match.capturedStart();
8986 int len = match.capturedLength();
8987 QString prefix = match.captured(1) + match.captured(3);
8988 bool hex = prefix.length() >= 2 && (prefix[1].toLower() == 'x');
8989 bool octal = !hex && !prefix.isEmpty();
8990 const QString num = hex ? match.captured(2) : octal ? match.captured(4) : match.captured(5);
8991
8992 // parse value
8993 bool ok;
8994 int base = hex ? 16 : octal ? 8 : 10;
8995 qlonglong value = 0; // decimal value
8996 qlonglong uvalue = 0; // hexadecimal or octal value (only unsigned)
8997 if (hex || octal)
8998 uvalue = num.toULongLong(&ok, base);
8999 else
9000 value = num.toLongLong(&ok, base);
9001 if (!ok) {
9002 qWarning() << "Cannot parse number:" << num << "base:" << base;
9003 return false;
9004 }
9005
9006 // negative decimal number
9007 if (!octal && !hex && pos > 0 && lineText[pos - 1] == '-') {
9008 value = -value;
9009 --pos;
9010 ++len;
9011 }
9012
9013 // result to string
9014 QString repl;
9015 if (hex || octal)
9016 repl = QString::number(uvalue + count, base);
9017 else
9018 repl = QString::number(value + count, base);
9019
9020 // convert hexadecimal number to upper-case if last letter was upper-case
9021 if (hex) {
9022 const int lastLetter = num.lastIndexOf(QRegularExpression("[a-fA-F]"));
9023 if (lastLetter != -1 && num[lastLetter].isUpper())
9024 repl = repl.toUpper();
9025 }
9026
9027 // preserve leading zeroes
9028 if ((octal || hex) && repl.size() < num.size())
9029 prefix.append(QString("0").repeated(num.size() - repl.size()));
9030 repl.prepend(prefix);
9031
9032 pos += block.position();
9033 pushUndoState();
9034 setAnchorAndPosition(pos, pos + len);
9035 replaceText(currentRange(), repl);
9036 setPosition(pos + repl.size() - 1);
9037
9038 return true;
9039 }
9040
selectQuotedStringTextObject(bool inner,const QString & quote)9041 bool FakeVimHandler::Private::selectQuotedStringTextObject(bool inner,
9042 const QString "e)
9043 {
9044 QTextCursor tc = m_cursor;
9045 int sz = quote.size();
9046
9047 QTextCursor tc1;
9048 QTextCursor tc2(document());
9049 while (tc2 <= tc) {
9050 tc1 = document()->find(quote, tc2);
9051 if (tc1.isNull())
9052 return false;
9053 tc2 = document()->find(quote, tc1);
9054 if (tc2.isNull())
9055 return false;
9056 }
9057
9058 int p1 = tc1.position();
9059 int p2 = tc2.position();
9060 if (inner) {
9061 p2 = qMax(p1, p2 - sz);
9062 if (characterAt(p1) == ParagraphSeparator)
9063 ++p1;
9064 } else {
9065 p1 -= sz;
9066 p2 -= sz - 1;
9067 }
9068
9069 if (isVisualMode())
9070 --p2;
9071
9072 setAnchorAndPosition(p1, p2);
9073 g.movetype = MoveExclusive;
9074
9075 return true;
9076 }
9077
selectArgumentTextObject(bool inner)9078 bool FakeVimHandler::Private::selectArgumentTextObject(bool inner)
9079 {
9080 // We are just interested whether we're currently inside angled brackets,
9081 // but selectBlockTextObject also moves the cursor, so set it back to
9082 // its original position afterwards
9083 QTextCursor prevCursor = m_cursor;
9084 const bool insideTemplateParameter = selectBlockTextObject(true, '<', '>');
9085 m_cursor = prevCursor;
9086
9087 int openAngleBracketCount = insideTemplateParameter ? 1 : 0;
9088
9089 QTextCursor tcStart(m_cursor);
9090 while (true) {
9091 if (tcStart.atStart())
9092 return true;
9093
9094 const QChar currentChar = characterAt(tcStart.position());
9095
9096 if (openAngleBracketCount == 0
9097 && (currentChar == '(' || currentChar == ','))
9098 break;
9099
9100 if (currentChar == '<')
9101 openAngleBracketCount--;
9102 else if (currentChar == '>')
9103 openAngleBracketCount++;
9104
9105 tcStart.setPosition(tcStart.position() - 1);
9106 }
9107
9108 QTextCursor tcEnd(m_cursor);
9109 openAngleBracketCount = insideTemplateParameter ? 1 : 0;
9110 int openParanthesisCount = 0;
9111
9112 while (true) {
9113 if (tcEnd.atEnd()) {
9114 return true;
9115 }
9116
9117 const QChar currentChar = characterAt(tcEnd.position());
9118 if (openAngleBracketCount == 0
9119 && openParanthesisCount == 0
9120 && (currentChar == ')' || currentChar == ','))
9121 break;
9122
9123 if (currentChar == '<')
9124 openAngleBracketCount++;
9125 else if (currentChar == '>')
9126 openAngleBracketCount--;
9127 else if (currentChar == '(')
9128 openParanthesisCount++;
9129 else if (currentChar == ')')
9130 openParanthesisCount--;
9131
9132
9133 tcEnd.setPosition(tcEnd.position() + 1);
9134 }
9135
9136
9137 if (!inner && characterAt(tcEnd.position()) == ',' && characterAt(tcStart.position()) == '(') {
9138 tcEnd.setPosition(tcEnd.position() + 1);
9139 if (characterAt(tcEnd.position()) == ' ')
9140 tcEnd.setPosition(tcEnd.position() + 1);
9141 }
9142
9143 // Never include the opening paranthesis
9144 if (characterAt(tcStart.position()) == '(') {
9145 tcStart.setPosition(tcStart.position() + 1);
9146 } else if (inner) {
9147 tcStart.setPosition(tcStart.position() + 1);
9148 if (characterAt(tcStart.position()) == ' ')
9149 tcStart.setPosition(tcStart.position() + 1);
9150 }
9151
9152 if (isVisualMode())
9153 tcEnd.setPosition(tcEnd.position() - 1);
9154
9155 g.movetype = MoveExclusive;
9156
9157 setAnchorAndPosition(tcStart.position(), tcEnd.position());
9158 return true;
9159 }
9160
mark(QChar code) const9161 Mark FakeVimHandler::Private::mark(QChar code) const
9162 {
9163 if (isVisualMode()) {
9164 if (code == '<')
9165 return CursorPosition(document(), qMin(anchor(), position()));
9166 if (code == '>')
9167 return CursorPosition(document(), qMax(anchor(), position()));
9168 }
9169
9170 if (code.isUpper())
9171 return g.marks.value(code);
9172
9173 return m_buffer->marks.value(code);
9174 }
9175
setMark(QChar code,CursorPosition position)9176 void FakeVimHandler::Private::setMark(QChar code, CursorPosition position)
9177 {
9178 if (code.isUpper())
9179 g.marks[code] = Mark(position, m_currentFileName);
9180 else
9181 m_buffer->marks[code] = Mark(position);
9182 }
9183
jumpToMark(QChar mark,bool backTickMode)9184 bool FakeVimHandler::Private::jumpToMark(QChar mark, bool backTickMode)
9185 {
9186 Mark m = this->mark(mark);
9187 if (!m.isValid()) {
9188 showMessage(MessageError, msgMarkNotSet(mark));
9189 return false;
9190 }
9191 if (!m.isLocal(m_currentFileName)) {
9192 q->requestJumpToGlobalMark(mark, backTickMode, m.fileName());
9193 return false;
9194 }
9195
9196 if ((mark == '\'' || mark == '`') && !m_buffer->jumpListUndo.isEmpty())
9197 m_buffer->jumpListUndo.pop();
9198 recordJump();
9199 setCursorPosition(m.position(document()));
9200 if (!backTickMode)
9201 moveToFirstNonBlankOnLine();
9202 if (g.submode == NoSubMode)
9203 setAnchor();
9204 setTargetColumn();
9205
9206 return true;
9207 }
9208
updateMarks(const Marks & newMarks)9209 void FakeVimHandler::Private::updateMarks(const Marks &newMarks)
9210 {
9211 for (auto it = newMarks.cbegin(), end = newMarks.cend(); it != end; ++it)
9212 m_buffer->marks[it.key()] = it.value();
9213 }
9214
registerRangeMode(int reg) const9215 RangeMode FakeVimHandler::Private::registerRangeMode(int reg) const
9216 {
9217 bool isClipboard;
9218 bool isSelection;
9219 getRegisterType(®, &isClipboard, &isSelection);
9220
9221 if (isClipboard || isSelection) {
9222 QClipboard *clipboard = QApplication::clipboard();
9223 QClipboard::Mode mode = isClipboard ? QClipboard::Clipboard : QClipboard::Selection;
9224
9225 // Use range mode from Vim's clipboard data if available.
9226 const QMimeData *data = clipboard->mimeData(mode);
9227 if (data && data->hasFormat(vimMimeText)) {
9228 QByteArray bytes = data->data(vimMimeText);
9229 if (bytes.length() > 0)
9230 return static_cast<RangeMode>(bytes.at(0));
9231 }
9232
9233 // If register content is clipboard:
9234 // - return RangeLineMode if text ends with new line char,
9235 // - return RangeCharMode otherwise.
9236 QString text = clipboard->text(mode);
9237 return (text.endsWith('\n') || text.endsWith('\r')) ? RangeLineMode : RangeCharMode;
9238 }
9239
9240 return g.registers[reg].rangemode;
9241 }
9242
setRegister(int reg,const QString & contents,RangeMode mode)9243 void FakeVimHandler::Private::setRegister(int reg, const QString &contents, RangeMode mode)
9244 {
9245 bool copyToClipboard;
9246 bool copyToSelection;
9247 bool append;
9248 getRegisterType(®, ©ToClipboard, ©ToSelection, &append);
9249
9250 QString contents2 = contents;
9251 if ((mode == RangeLineMode || mode == RangeLineModeExclusive)
9252 && !contents2.endsWith('\n'))
9253 {
9254 contents2.append('\n');
9255 }
9256
9257 if (copyToClipboard || copyToSelection) {
9258 if (copyToClipboard)
9259 setClipboardData(contents2, mode, QClipboard::Clipboard);
9260 if (copyToSelection)
9261 setClipboardData(contents2, mode, QClipboard::Selection);
9262 } else {
9263 if (append)
9264 g.registers[reg].contents.append(contents2);
9265 else
9266 g.registers[reg].contents = contents2;
9267 g.registers[reg].rangemode = mode;
9268 }
9269 }
9270
registerContents(int reg) const9271 QString FakeVimHandler::Private::registerContents(int reg) const
9272 {
9273 bool copyFromClipboard;
9274 bool copyFromSelection;
9275 getRegisterType(®, ©FromClipboard, ©FromSelection);
9276
9277 if (copyFromClipboard || copyFromSelection) {
9278 QClipboard *clipboard = QApplication::clipboard();
9279 if (copyFromClipboard)
9280 return clipboard->text(QClipboard::Clipboard);
9281 if (copyFromSelection)
9282 return clipboard->text(QClipboard::Selection);
9283 }
9284
9285 return g.registers[reg].contents;
9286 }
9287
getRegisterType(int * reg,bool * isClipboard,bool * isSelection,bool * append) const9288 void FakeVimHandler::Private::getRegisterType(int *reg, bool *isClipboard, bool *isSelection, bool *append) const
9289 {
9290 bool clipboard = false;
9291 bool selection = false;
9292
9293 // If register is uppercase, append content to lower case register on yank/delete.
9294 const QChar c(*reg);
9295 if (append != nullptr)
9296 *append = c.isUpper();
9297 if (c.isUpper())
9298 *reg = c.toLower().unicode();
9299
9300 if (c == '"') {
9301 QStringList list = s.clipboard.value().split(',');
9302 clipboard = list.contains("unnamedplus");
9303 selection = list.contains("unnamed");
9304 } else if (c == '+') {
9305 clipboard = true;
9306 } else if (c == '*') {
9307 selection = true;
9308 }
9309
9310 // selection (primary) is clipboard on systems without selection support
9311 if (selection && !QApplication::clipboard()->supportsSelection()) {
9312 clipboard = true;
9313 selection = false;
9314 }
9315
9316 if (isClipboard != nullptr)
9317 *isClipboard = clipboard;
9318 if (isSelection != nullptr)
9319 *isSelection = selection;
9320 }
9321
9322 ///////////////////////////////////////////////////////////////////////
9323 //
9324 // FakeVimHandler
9325 //
9326 ///////////////////////////////////////////////////////////////////////
9327
FakeVimHandler(QWidget * widget,QObject * parent)9328 FakeVimHandler::FakeVimHandler(QWidget *widget, QObject *parent)
9329 : QObject(parent), d(new Private(this, widget))
9330 {}
9331
~FakeVimHandler()9332 FakeVimHandler::~FakeVimHandler()
9333 {
9334 delete d;
9335 }
9336
9337 // gracefully handle that the parent editor is deleted
disconnectFromEditor()9338 void FakeVimHandler::disconnectFromEditor()
9339 {
9340 d->m_textedit = nullptr;
9341 d->m_plaintextedit = nullptr;
9342 }
9343
updateGlobalMarksFilenames(const QString & oldFileName,const QString & newFileName)9344 void FakeVimHandler::updateGlobalMarksFilenames(const QString &oldFileName, const QString &newFileName)
9345 {
9346 for (Mark &mark : Private::g.marks) {
9347 if (mark.fileName() == oldFileName)
9348 mark.setFileName(newFileName);
9349 }
9350 }
9351
eventFilter(QObject * ob,QEvent * ev)9352 bool FakeVimHandler::eventFilter(QObject *ob, QEvent *ev)
9353 {
9354 #ifndef FAKEVIM_STANDALONE
9355 if (!fakeVimSettings()->useFakeVim.value())
9356 return QObject::eventFilter(ob, ev);
9357 #endif
9358
9359 if (ev->type() == QEvent::Shortcut) {
9360 d->passShortcuts(false);
9361 return false;
9362 }
9363
9364 if (ev->type() == QEvent::KeyPress &&
9365 (ob == d->editor()
9366 || (Private::g.mode == ExMode || Private::g.subsubmode == SearchSubSubMode))) {
9367 auto kev = static_cast<QKeyEvent *>(ev);
9368 KEY_DEBUG("KEYPRESS" << kev->key() << kev->text() << QChar(kev->key()));
9369 EventResult res = d->handleEvent(kev);
9370 //if (Private::g.mode == InsertMode)
9371 // completionRequested();
9372 // returning false core the app see it
9373 //KEY_DEBUG("HANDLED CODE:" << res);
9374 //return res != EventPassedToCore;
9375 //return true;
9376 return res == EventHandled || res == EventCancelled;
9377 }
9378
9379 if (ev->type() == QEvent::ShortcutOverride && (ob == d->editor()
9380 || (Private::g.mode == ExMode || Private::g.subsubmode == SearchSubSubMode))) {
9381 auto kev = static_cast<QKeyEvent *>(ev);
9382 if (d->wantsOverride(kev)) {
9383 KEY_DEBUG("OVERRIDING SHORTCUT" << kev->key());
9384 ev->accept(); // accepting means "don't run the shortcuts"
9385 return true;
9386 }
9387 KEY_DEBUG("NO SHORTCUT OVERRIDE" << kev->key());
9388 return true;
9389 }
9390
9391 if (ev->type() == QEvent::FocusOut && ob == d->editor()) {
9392 d->unfocus();
9393 return false;
9394 }
9395
9396 if (ev->type() == QEvent::FocusIn && ob == d->editor())
9397 d->focus();
9398
9399 return QObject::eventFilter(ob, ev);
9400 }
9401
installEventFilter()9402 void FakeVimHandler::installEventFilter()
9403 {
9404 d->installEventFilter();
9405 }
9406
setupWidget()9407 void FakeVimHandler::setupWidget()
9408 {
9409 d->setupWidget();
9410 }
9411
restoreWidget(int tabSize)9412 void FakeVimHandler::restoreWidget(int tabSize)
9413 {
9414 d->restoreWidget(tabSize);
9415 }
9416
handleCommand(const QString & cmd)9417 void FakeVimHandler::handleCommand(const QString &cmd)
9418 {
9419 d->enterFakeVim();
9420 d->handleCommand(cmd);
9421 d->leaveFakeVim();
9422 }
9423
handleReplay(const QString & keys)9424 void FakeVimHandler::handleReplay(const QString &keys)
9425 {
9426 d->enterFakeVim();
9427 d->replay(keys);
9428 d->leaveFakeVim();
9429 }
9430
handleInput(const QString & keys)9431 void FakeVimHandler::handleInput(const QString &keys)
9432 {
9433 const Inputs inputs(keys);
9434 d->enterFakeVim();
9435 for (const Input &input : inputs)
9436 d->handleKey(input);
9437 d->leaveFakeVim();
9438 }
9439
enterCommandMode()9440 void FakeVimHandler::enterCommandMode()
9441 {
9442 d->enterCommandMode();
9443 }
9444
setCurrentFileName(const QString & fileName)9445 void FakeVimHandler::setCurrentFileName(const QString &fileName)
9446 {
9447 d->m_currentFileName = fileName;
9448 }
9449
currentFileName() const9450 QString FakeVimHandler::currentFileName() const
9451 {
9452 return d->m_currentFileName;
9453 }
9454
showMessage(MessageLevel level,const QString & msg)9455 void FakeVimHandler::showMessage(MessageLevel level, const QString &msg)
9456 {
9457 d->showMessage(level, msg);
9458 }
9459
widget()9460 QWidget *FakeVimHandler::widget()
9461 {
9462 return d->editor();
9463 }
9464
9465 // Test only
physicalIndentation(const QString & line) const9466 int FakeVimHandler::physicalIndentation(const QString &line) const
9467 {
9468 Column ind = d->indentation(line);
9469 return ind.physical;
9470 }
9471
logicalIndentation(const QString & line) const9472 int FakeVimHandler::logicalIndentation(const QString &line) const
9473 {
9474 Column ind = d->indentation(line);
9475 return ind.logical;
9476 }
9477
tabExpand(int n) const9478 QString FakeVimHandler::tabExpand(int n) const
9479 {
9480 return d->tabExpand(n);
9481 }
9482
miniBufferTextEdited(const QString & text,int cursorPos,int anchorPos)9483 void FakeVimHandler::miniBufferTextEdited(const QString &text, int cursorPos, int anchorPos)
9484 {
9485 d->miniBufferTextEdited(text, cursorPos, anchorPos);
9486 }
9487
setTextCursorPosition(int position)9488 void FakeVimHandler::setTextCursorPosition(int position)
9489 {
9490 int pos = qMax(0, qMin(position, d->lastPositionInDocument()));
9491 if (d->isVisualMode())
9492 d->setPosition(pos);
9493 else
9494 d->setAnchorAndPosition(pos, pos);
9495 d->setTargetColumn();
9496
9497 if (!d->m_inFakeVim)
9498 d->commitCursor();
9499 }
9500
textCursor() const9501 QTextCursor FakeVimHandler::textCursor() const
9502 {
9503 return d->m_cursor;
9504 }
9505
setTextCursor(const QTextCursor & cursor)9506 void FakeVimHandler::setTextCursor(const QTextCursor &cursor)
9507 {
9508 d->m_cursor = cursor;
9509 }
9510
jumpToLocalMark(QChar mark,bool backTickMode)9511 bool FakeVimHandler::jumpToLocalMark(QChar mark, bool backTickMode)
9512 {
9513 return d->jumpToMark(mark, backTickMode);
9514 }
9515
9516 } // namespace Internal
9517 } // namespace FakeVim
9518
9519 Q_DECLARE_METATYPE(FakeVim::Internal::FakeVimHandler::Private::BufferDataPtr)
9520