1/*
2 SPDX-FileCopyrightText: 2008-2011 Erlend Hamberg <ehamberg@gmail.com>
3 SPDX-FileCopyrightText: 2011 Svyatoslav Kuzmich <svatoslav1@gmail.com>
4 SPDX-FileCopyrightText: 2012-2013 Simon St James <kdedevel@etotheipiplusone.com>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "katecompletiontree.h"
10#include "katecompletionwidget.h"
11#include "kateconfig.h"
12#include "katedocument.h"
13#include "kateglobal.h"
14#include "katepartdebug.h"
15#include "katerenderer.h"
16#include "kateview.h"
17#include "kateviewinternal.h"
18#include "kateviinputmode.h"
19#include <vimode/completionrecorder.h>
20#include <vimode/completionreplayer.h>
21#include <vimode/emulatedcommandbar/emulatedcommandbar.h>
22#include <vimode/inputmodemanager.h>
23#include <vimode/keyparser.h>
24#include <vimode/lastchangerecorder.h>
25#include <vimode/macrorecorder.h>
26#include <vimode/marks.h>
27#include <vimode/modes/insertvimode.h>
28
29#include <KLocalizedString>
30
31using namespace KateVi;
32
33InsertViMode::InsertViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal)
34 : ModeBase()
35{
36 m_view = view;
37 m_viewInternal = viewInternal;
38 m_viInputModeManager = viInputModeManager;
39
40 m_waitingRegister = false;
41 m_blockInsert = None;
42 m_eolPos = 0;
43 m_count = 1;
44 m_countedRepeatsBeginOnNewLine = false;
45
46 m_isExecutingCompletion = false;
47
48 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: &InsertViMode::textInserted);
49}
50
51InsertViMode::~InsertViMode() = default;
52
53bool InsertViMode::commandInsertFromAbove()
54{
55 KTextEditor::Cursor c(m_view->cursorPosition());
56
57 if (c.line() <= 0) {
58 return false;
59 }
60
61 QString line = doc()->line(line: c.line() - 1);
62 int tabWidth = doc()->config()->tabWidth();
63 QChar ch = getCharAtVirtualColumn(line, virtualColumn: m_view->virtualCursorColumn(), tabWidht: tabWidth);
64
65 if (ch == QChar::Null) {
66 return false;
67 }
68
69 return doc()->insertText(position: c, s: ch);
70}
71
72bool InsertViMode::commandInsertFromBelow()
73{
74 KTextEditor::Cursor c(m_view->cursorPosition());
75
76 if (c.line() >= doc()->lines() - 1) {
77 return false;
78 }
79
80 QString line = doc()->line(line: c.line() + 1);
81 int tabWidth = doc()->config()->tabWidth();
82 QChar ch = getCharAtVirtualColumn(line, virtualColumn: m_view->virtualCursorColumn(), tabWidht: tabWidth);
83
84 if (ch == QChar::Null) {
85 return false;
86 }
87
88 return doc()->insertText(position: c, s: ch);
89}
90
91bool InsertViMode::commandDeleteWord()
92{
93 KTextEditor::Cursor c1(m_view->cursorPosition());
94 KTextEditor::Cursor c2;
95
96 c2 = findPrevWordStart(fromLine: c1.line(), fromColumn: c1.column());
97
98 if (c2.line() != c1.line()) {
99 if (c1.column() == 0) {
100 c2.setColumn(doc()->line(line: c2.line()).length());
101 } else {
102 c2.setColumn(0);
103 c2.setLine(c2.line() + 1);
104 }
105 }
106
107 Range r(c2, c1, ExclusiveMotion);
108 return deleteRange(r, mode: CharWise, addToRegister: false);
109}
110
111bool InsertViMode::commandDeleteLine()
112{
113 KTextEditor::Cursor c(m_view->cursorPosition());
114 Range r(c.line(), 0, c.line(), c.column(), ExclusiveMotion);
115
116 if (c.column() == 0) {
117 // Try to move the current line to the end of the previous line.
118 if (c.line() == 0) {
119 return true;
120 } else {
121 r.startColumn = doc()->line(line: c.line() - 1).length();
122 r.startLine--;
123 }
124 } else {
125 /*
126 * Remove backwards until the first non-space character. If no
127 * non-space was found, remove backwards to the first column.
128 */
129 static const QRegularExpression nonSpace(QStringLiteral("\\S"), QRegularExpression::UseUnicodePropertiesOption);
130 r.startColumn = getLine().indexOf(re: nonSpace);
131 if (r.startColumn == -1 || r.startColumn >= c.column()) {
132 r.startColumn = 0;
133 }
134 }
135 return deleteRange(r, mode: CharWise, addToRegister: false);
136}
137
138bool InsertViMode::commandDeleteCharBackward()
139{
140 KTextEditor::Cursor c(m_view->cursorPosition());
141
142 Range r(c.line(), c.column() - getCount(), c.line(), c.column(), ExclusiveMotion);
143
144 if (c.column() == 0) {
145 if (c.line() == 0) {
146 return true;
147 } else {
148 r.startColumn = doc()->line(line: c.line() - 1).length();
149 r.startLine--;
150 }
151 }
152
153 return deleteRange(r, mode: CharWise);
154}
155
156bool InsertViMode::commandNewLine()
157{
158 doc()->newLine(view: m_view);
159 return true;
160}
161
162bool InsertViMode::commandIndent()
163{
164 KTextEditor::Cursor c(m_view->cursorPosition());
165 doc()->indent(range: KTextEditor::Range(c.line(), 0, c.line(), 0), change: 1);
166 return true;
167}
168
169bool InsertViMode::commandUnindent()
170{
171 KTextEditor::Cursor c(m_view->cursorPosition());
172 doc()->indent(range: KTextEditor::Range(c.line(), 0, c.line(), 0), change: -1);
173 return true;
174}
175
176bool InsertViMode::commandToFirstCharacterInFile()
177{
178 KTextEditor::Cursor c(0, 0);
179 updateCursor(c);
180 return true;
181}
182
183bool InsertViMode::commandToLastCharacterInFile()
184{
185 int lines = doc()->lines() - 1;
186 KTextEditor::Cursor c(lines, doc()->line(line: lines).length());
187 updateCursor(c);
188 return true;
189}
190
191bool InsertViMode::commandMoveOneWordLeft()
192{
193 KTextEditor::Cursor c(m_view->cursorPosition());
194 c = findPrevWordStart(fromLine: c.line(), fromColumn: c.column());
195
196 if (!c.isValid()) {
197 c = KTextEditor::Cursor(0, 0);
198 }
199
200 updateCursor(c);
201 return true;
202}
203
204bool InsertViMode::commandMoveOneWordRight()
205{
206 KTextEditor::Cursor c(m_view->cursorPosition());
207 c = findNextWordStart(fromLine: c.line(), fromColumn: c.column());
208
209 if (!c.isValid()) {
210 c = doc()->documentEnd();
211 }
212
213 updateCursor(c);
214 return true;
215}
216
217bool InsertViMode::commandCompleteNext()
218{
219 if (m_view->completionWidget()->isCompletionActive()) {
220 const QModelIndex oldCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex();
221 m_view->completionWidget()->cursorDown();
222 const QModelIndex newCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex();
223 if (newCompletionItem == oldCompletionItem) {
224 // Wrap to top.
225 m_view->completionWidget()->top();
226 }
227 } else {
228 m_view->userInvokedCompletion();
229 }
230 return true;
231}
232
233bool InsertViMode::commandCompletePrevious()
234{
235 if (m_view->completionWidget()->isCompletionActive()) {
236 const QModelIndex oldCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex();
237 m_view->completionWidget()->cursorUp();
238 const QModelIndex newCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex();
239 if (newCompletionItem == oldCompletionItem) {
240 // Wrap to bottom.
241 m_view->completionWidget()->bottom();
242 }
243 } else {
244 m_view->userInvokedCompletion();
245 m_view->completionWidget()->bottom();
246 }
247 return true;
248}
249
250bool InsertViMode::commandInsertContentOfRegister()
251{
252 KTextEditor::Cursor c(m_view->cursorPosition());
253 KTextEditor::Cursor cAfter = c;
254 QChar reg = getChosenRegister(defaultReg: m_register);
255
256 OperationMode m = getRegisterFlag(reg);
257 QString textToInsert = getRegisterContent(reg);
258
259 if (textToInsert.isNull()) {
260 error(i18n("Nothing in register %1", reg));
261 return false;
262 }
263
264 if (m == LineWise) {
265 textToInsert.chop(n: 1); // remove the last \n
266 c.setColumn(doc()->lineLength(line: c.line())); // paste after the current line and ...
267 textToInsert.prepend(c: QLatin1Char('\n')); // ... prepend a \n, so the text starts on a new line
268
269 cAfter.setLine(cAfter.line() + 1);
270 cAfter.setColumn(0);
271 } else {
272 cAfter.setColumn(cAfter.column() + textToInsert.length());
273 }
274
275 doc()->insertText(position: c, s: textToInsert, block: m == Block);
276
277 updateCursor(c: cAfter);
278
279 return true;
280}
281
282// Start Normal mode just for one command and return to Insert mode
283bool InsertViMode::commandSwitchToNormalModeForJustOneCommand()
284{
285 m_viInputModeManager->setTemporaryNormalMode(true);
286 m_viInputModeManager->changeViMode(newMode: ViMode::NormalMode);
287 const KTextEditor::Cursor cursorPos = m_view->cursorPosition();
288 // If we're at end of the line, move the cursor back one step, as in Vim.
289 if (doc()->line(line: cursorPos.line()).length() == cursorPos.column()) {
290 m_view->setCursorPosition(KTextEditor::Cursor(cursorPos.line(), cursorPos.column() - 1));
291 }
292 m_viInputModeManager->inputAdapter()->setCaretStyle(KTextEditor::caretStyles::Block);
293 Q_EMIT m_view->viewModeChanged(view: m_view, mode: m_view->viewMode());
294 m_viewInternal->repaint();
295 return true;
296}
297
298/**
299 * checks if the key is a valid command
300 * @return true if a command was completed and executed, false otherwise
301 */
302bool InsertViMode::handleKeypress(const QKeyEvent *e)
303{
304 if (e->key() == Qt::Key_Backspace) {
305 // backspace should work even if the shift key is down
306 if (e->modifiers() != CONTROL_MODIFIER) {
307 m_view->backspace();
308 } else {
309 m_view->deleteWordLeft();
310 }
311 return true;
312 }
313
314 if (m_keys.isEmpty() && !m_waitingRegister) {
315 // on macOS the KeypadModifier is set for arrow keys too
316 if (e->modifiers() == Qt::NoModifier || e->modifiers() == Qt::KeypadModifier) {
317 switch (e->key()) {
318 case Qt::Key_Escape:
319 leaveInsertMode();
320 return true;
321 case Qt::Key_Left:
322 m_view->cursorLeft();
323 return true;
324 case Qt::Key_Right:
325 m_view->cursorRight();
326 return true;
327 case Qt::Key_Up:
328 m_view->up();
329 return true;
330 case Qt::Key_Down:
331 m_view->down();
332 return true;
333 case Qt::Key_Insert:
334 startReplaceMode();
335 return true;
336 case Qt::Key_Delete:
337 m_view->keyDelete();
338 return true;
339 case Qt::Key_Home:
340 m_view->home();
341 return true;
342 case Qt::Key_End:
343 m_view->end();
344 return true;
345 case Qt::Key_PageUp:
346 m_view->pageUp();
347 return true;
348 case Qt::Key_PageDown:
349 m_view->pageDown();
350 return true;
351 case Qt::Key_Enter:
352 case Qt::Key_Return:
353 case Qt::Key_Tab:
354 if (m_view->completionWidget()->isCompletionActive() && !m_viInputModeManager->macroRecorder()->isReplaying()
355 && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) {
356 m_isExecutingCompletion = true;
357 m_textInsertedByCompletion.clear();
358 const bool success = m_view->completionWidget()->execute();
359 m_isExecutingCompletion = false;
360 if (success) {
361 // Filter out Enter/ Return's that trigger a completion when recording macros/ last change stuff; they
362 // will be replaced with the special code "ctrl-space".
363 // (This is why there is a "!m_viInputModeManager->isReplayingMacro()" above.)
364 m_viInputModeManager->doNotLogCurrentKeypress();
365 completionFinished();
366 return true;
367 }
368 } else if (m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->isSendingSyntheticSearchCompletedKeypress()) {
369 // BUG #451076, Do not record/send return for a newline when doing a search via Ctrl+F/Edit->Find menu
370 m_viInputModeManager->doNotLogCurrentKeypress();
371 return true;
372 }
373 Q_FALLTHROUGH();
374 default:
375 return false;
376 }
377 } else if (e->modifiers() == CONTROL_MODIFIER) {
378 switch (e->key()) {
379 case Qt::Key_BracketLeft:
380 case Qt::Key_3:
381 leaveInsertMode();
382 return true;
383 case Qt::Key_Space:
384 // We use Ctrl-space as a special code in macros/ last change, which means: if replaying
385 // a macro/ last change, fetch and execute the next completion for this macro/ last change ...
386 if (!m_viInputModeManager->macroRecorder()->isReplaying() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) {
387 commandCompleteNext();
388 // ... therefore, we should not record ctrl-space indiscriminately.
389 m_viInputModeManager->doNotLogCurrentKeypress();
390 } else {
391 m_viInputModeManager->completionReplayer()->replay();
392 }
393 return true;
394 case Qt::Key_C:
395 leaveInsertMode(force: true);
396 return true;
397 case Qt::Key_D:
398 commandUnindent();
399 return true;
400 case Qt::Key_E:
401 commandInsertFromBelow();
402 return true;
403 case Qt::Key_N:
404 if (!m_viInputModeManager->macroRecorder()->isReplaying()) {
405 commandCompleteNext();
406 }
407 return true;
408 case Qt::Key_P:
409 if (!m_viInputModeManager->macroRecorder()->isReplaying()) {
410 commandCompletePrevious();
411 }
412 return true;
413 case Qt::Key_T:
414 commandIndent();
415 return true;
416 case Qt::Key_W:
417 commandDeleteWord();
418 return true;
419 case Qt::Key_U:
420 return commandDeleteLine();
421 case Qt::Key_J:
422 commandNewLine();
423 return true;
424 case Qt::Key_H:
425 commandDeleteCharBackward();
426 return true;
427 case Qt::Key_Y:
428 commandInsertFromAbove();
429 return true;
430 case Qt::Key_O:
431 commandSwitchToNormalModeForJustOneCommand();
432 return true;
433 case Qt::Key_Home:
434 commandToFirstCharacterInFile();
435 return true;
436 case Qt::Key_R:
437 m_waitingRegister = true;
438 return true;
439 case Qt::Key_End:
440 commandToLastCharacterInFile();
441 return true;
442 case Qt::Key_Left:
443 commandMoveOneWordLeft();
444 return true;
445 case Qt::Key_Right:
446 commandMoveOneWordRight();
447 return true;
448 default:
449 return false;
450 }
451 }
452
453 return false;
454 } else if (m_waitingRegister) {
455 // ignore modifier keys alone
456 if (e->key() == Qt::Key_Shift || e->key() == Qt::Key_Control || e->key() == Qt::Key_Alt || e->key() == Qt::Key_Meta) {
457 return false;
458 }
459
460 QChar key = KeyParser::self()->KeyEventToQChar(keyEvent: *e);
461 key = key.toLower();
462 m_waitingRegister = false;
463
464 // is it register ?
465 // TODO: add registers such as '/'. See :h <c-r>
466 if ((key >= QLatin1Char('0') && key <= QLatin1Char('9')) || (key >= QLatin1Char('a') && key <= QLatin1Char('z')) || key == QLatin1Char('_')
467 || key == QLatin1Char('-') || key == QLatin1Char('+') || key == QLatin1Char('*') || key == QLatin1Char('"')) {
468 m_register = key;
469 } else {
470 return false;
471 }
472 commandInsertContentOfRegister();
473 return true;
474 }
475 return false;
476}
477
478// leave insert mode when esc, etc, is pressed. if leaving block
479// prepend/append, the inserted text will be added to all block lines. if
480// ctrl-c is used to exit insert mode this is not done.
481void InsertViMode::leaveInsertMode(bool force)
482{
483 m_view->abortCompletion();
484 if (!force) {
485 if (m_blockInsert != None) { // block append/prepend
486
487 // make sure cursor haven't been moved
488 if (m_blockRange.startLine == m_view->cursorPosition().line()) {
489 int start;
490 int len;
491 QString added;
492 KTextEditor::Cursor c;
493
494 switch (m_blockInsert) {
495 case Append:
496 case Prepend:
497 if (m_blockInsert == Append) {
498 start = m_blockRange.endColumn + 1;
499 } else {
500 start = m_blockRange.startColumn;
501 }
502
503 len = m_view->cursorPosition().column() - start;
504 added = getLine().mid(position: start, n: len);
505
506 c = KTextEditor::Cursor(m_blockRange.startLine, start);
507 for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) {
508 c.setLine(i);
509 doc()->insertText(position: c, s: added);
510 }
511 break;
512 case AppendEOL:
513 start = m_eolPos;
514 len = m_view->cursorPosition().column() - start;
515 added = getLine().mid(position: start, n: len);
516
517 c = KTextEditor::Cursor(m_blockRange.startLine, start);
518 for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) {
519 c.setLine(i);
520 c.setColumn(doc()->lineLength(line: i));
521 doc()->insertText(position: c, s: added);
522 }
523 break;
524 default:
525 error(QStringLiteral("not supported"));
526 }
527 }
528
529 m_blockInsert = None;
530 } else {
531 const QString added = doc()->text(range: KTextEditor::Range(m_viInputModeManager->marks()->getStartEditYanked(), m_view->cursorPosition()));
532
533 if (m_count > 1) {
534 for (unsigned int i = 0; i < m_count - 1; i++) {
535 if (m_countedRepeatsBeginOnNewLine) {
536 doc()->newLine(view: m_view);
537 }
538 doc()->insertText(position: m_view->cursorPosition(), s: added);
539 }
540 }
541 }
542 }
543 m_countedRepeatsBeginOnNewLine = false;
544 startNormalMode();
545}
546
547void InsertViMode::setBlockPrependMode(Range blockRange)
548{
549 // ignore if not more than one line is selected
550 if (blockRange.startLine != blockRange.endLine) {
551 m_blockInsert = Prepend;
552 m_blockRange = blockRange;
553 }
554}
555
556void InsertViMode::setBlockAppendMode(Range blockRange, BlockInsert b)
557{
558 Q_ASSERT(b == Append || b == AppendEOL);
559
560 // ignore if not more than one line is selected
561 if (blockRange.startLine != blockRange.endLine) {
562 m_blockRange = blockRange;
563 m_blockInsert = b;
564 if (b == AppendEOL) {
565 m_eolPos = doc()->lineLength(line: m_blockRange.startLine);
566 }
567 } else {
568 qCDebug(LOG_KTE) << "cursor moved. ignoring block append/prepend";
569 }
570}
571
572void InsertViMode::completionFinished()
573{
574 Completion::CompletionType completionType = Completion::PlainText;
575 if (m_view->cursorPosition() != m_textInsertedByCompletionEndPos) {
576 completionType = Completion::FunctionWithArgs;
577 } else if (m_textInsertedByCompletion.endsWith(s: QLatin1String("()")) || m_textInsertedByCompletion.endsWith(s: QLatin1String("();"))) {
578 completionType = Completion::FunctionWithoutArgs;
579 }
580 m_viInputModeManager->completionRecorder()->logCompletionEvent(
581 completion: Completion(m_textInsertedByCompletion, KateViewConfig::global()->wordCompletionRemoveTail(), completionType));
582}
583
584void InsertViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range)
585{
586 if (m_isExecutingCompletion) {
587 m_textInsertedByCompletion += document->text(range);
588 m_textInsertedByCompletionEndPos = range.end();
589 }
590}
591

source code of ktexteditor/src/vimode/modes/insertvimode.cpp