KTextEditor

insertvimode.cpp
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(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &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(c.line() - 1);
62 int tabWidth = doc()->config()->tabWidth();
63 QChar ch = getCharAtVirtualColumn(line, m_view->virtualCursorColumn(), tabWidth);
64
65 if (ch == QChar::Null) {
66 return false;
67 }
68
69 return doc()->insertText(c, 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(c.line() + 1);
81 int tabWidth = doc()->config()->tabWidth();
82 QChar ch = getCharAtVirtualColumn(line, m_view->virtualCursorColumn(), tabWidth);
83
84 if (ch == QChar::Null) {
85 return false;
86 }
87
88 return doc()->insertText(c, ch);
89}
90
91bool InsertViMode::commandDeleteWord()
92{
93 KTextEditor::Cursor c1(m_view->cursorPosition());
95
96 c2 = findPrevWordStart(c1.line(), c1.column());
97
98 if (c2.line() != c1.line()) {
99 if (c1.column() == 0) {
100 c2.setColumn(doc()->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, CharWise, 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(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(nonSpace);
131 if (r.startColumn == -1 || r.startColumn >= c.column()) {
132 r.startColumn = 0;
133 }
134 }
135 return deleteRange(r, CharWise, 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(c.line() - 1).length();
149 r.startLine--;
150 }
151 }
152
153 return deleteRange(r, CharWise);
154}
155
156bool InsertViMode::commandNewLine()
157{
158 doc()->newLine(m_view);
159 return true;
160}
161
162bool InsertViMode::commandIndent()
163{
164 KTextEditor::Cursor c(m_view->cursorPosition());
165 doc()->indent(KTextEditor::Range(c.line(), 0, c.line(), 0), 1);
166 return true;
167}
168
169bool InsertViMode::commandUnindent()
170{
171 KTextEditor::Cursor c(m_view->cursorPosition());
172 doc()->indent(KTextEditor::Range(c.line(), 0, c.line(), 0), -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(lines).length());
187 updateCursor(c);
188 return true;
189}
190
191bool InsertViMode::commandMoveOneWordLeft()
192{
193 KTextEditor::Cursor c(m_view->cursorPosition());
194 c = findPrevWordStart(c.line(), 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(c.line(), 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(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(1); // remove the last \n
266 c.setColumn(doc()->lineLength(c.line())); // paste after the current line and ...
267 textToInsert.prepend(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(c, textToInsert, m == Block);
276
277 updateCursor(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(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(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(m_view, 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 // backspace should work even if the shift key is down
305 if (e->modifiers() != CONTROL_MODIFIER && e->key() == Qt::Key_Backspace) {
306 m_view->backspace();
307 return true;
308 }
309
310 if (m_keys.isEmpty() && !m_waitingRegister) {
311 // on macOS the KeypadModifier is set for arrow keys too
312 if (e->modifiers() == Qt::NoModifier || e->modifiers() == Qt::KeypadModifier) {
313 switch (e->key()) {
314 case Qt::Key_Escape:
315 leaveInsertMode();
316 return true;
317 case Qt::Key_Left:
318 m_view->cursorLeft();
319 return true;
320 case Qt::Key_Right:
321 m_view->cursorRight();
322 return true;
323 case Qt::Key_Up:
324 m_view->up();
325 return true;
326 case Qt::Key_Down:
327 m_view->down();
328 return true;
329 case Qt::Key_Insert:
330 startReplaceMode();
331 return true;
332 case Qt::Key_Delete:
333 m_view->keyDelete();
334 return true;
335 case Qt::Key_Home:
336 m_view->home();
337 return true;
338 case Qt::Key_End:
339 m_view->end();
340 return true;
341 case Qt::Key_PageUp:
342 m_view->pageUp();
343 return true;
344 case Qt::Key_PageDown:
345 m_view->pageDown();
346 return true;
347 case Qt::Key_Enter:
348 case Qt::Key_Return:
349 case Qt::Key_Tab:
350 if (m_view->completionWidget()->isCompletionActive() && !m_viInputModeManager->macroRecorder()->isReplaying()
351 && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) {
352 m_isExecutingCompletion = true;
353 m_textInsertedByCompletion.clear();
354 const bool success = m_view->completionWidget()->execute();
355 m_isExecutingCompletion = false;
356 if (success) {
357 // Filter out Enter/ Return's that trigger a completion when recording macros/ last change stuff; they
358 // will be replaced with the special code "ctrl-space".
359 // (This is why there is a "!m_viInputModeManager->isReplayingMacro()" above.)
360 m_viInputModeManager->doNotLogCurrentKeypress();
361 completionFinished();
362 return true;
363 }
364 } else if (m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->isSendingSyntheticSearchCompletedKeypress()) {
365 // BUG #451076, Do not record/send return for a newline when doing a search via Ctrl+F/Edit->Find menu
366 m_viInputModeManager->doNotLogCurrentKeypress();
367 return true;
368 }
369 Q_FALLTHROUGH();
370 default:
371 return false;
372 }
373 } else if (e->modifiers() == CONTROL_MODIFIER) {
374 switch (e->key()) {
376 case Qt::Key_3:
377 leaveInsertMode();
378 return true;
379 case Qt::Key_Space:
380 // We use Ctrl-space as a special code in macros/ last change, which means: if replaying
381 // a macro/ last change, fetch and execute the next completion for this macro/ last change ...
382 if (!m_viInputModeManager->macroRecorder()->isReplaying() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) {
383 commandCompleteNext();
384 // ... therefore, we should not record ctrl-space indiscriminately.
385 m_viInputModeManager->doNotLogCurrentKeypress();
386 } else {
387 m_viInputModeManager->completionReplayer()->replay();
388 }
389 return true;
390 case Qt::Key_C:
391 leaveInsertMode(true);
392 return true;
393 case Qt::Key_D:
394 commandUnindent();
395 return true;
396 case Qt::Key_E:
397 commandInsertFromBelow();
398 return true;
399 case Qt::Key_N:
400 if (!m_viInputModeManager->macroRecorder()->isReplaying()) {
401 commandCompleteNext();
402 }
403 return true;
404 case Qt::Key_P:
405 if (!m_viInputModeManager->macroRecorder()->isReplaying()) {
406 commandCompletePrevious();
407 }
408 return true;
409 case Qt::Key_T:
410 commandIndent();
411 return true;
412 case Qt::Key_W:
413 commandDeleteWord();
414 return true;
415 case Qt::Key_U:
416 return commandDeleteLine();
417 case Qt::Key_J:
418 commandNewLine();
419 return true;
420 case Qt::Key_H:
421 commandDeleteCharBackward();
422 return true;
423 case Qt::Key_Y:
424 commandInsertFromAbove();
425 return true;
426 case Qt::Key_O:
427 commandSwitchToNormalModeForJustOneCommand();
428 return true;
429 case Qt::Key_Home:
430 commandToFirstCharacterInFile();
431 return true;
432 case Qt::Key_R:
433 m_waitingRegister = true;
434 return true;
435 case Qt::Key_End:
436 commandToLastCharacterInFile();
437 return true;
438 case Qt::Key_Left:
439 commandMoveOneWordLeft();
440 return true;
441 case Qt::Key_Right:
442 commandMoveOneWordRight();
443 return true;
444 default:
445 return false;
446 }
447 }
448
449 return false;
450 } else if (m_waitingRegister) {
451 // ignore modifier keys alone
452 if (e->key() == Qt::Key_Shift || e->key() == Qt::Key_Control || e->key() == Qt::Key_Alt || e->key() == Qt::Key_Meta) {
453 return false;
454 }
455
456 QChar key = KeyParser::self()->KeyEventToQChar(*e);
457 key = key.toLower();
458 m_waitingRegister = false;
459
460 // is it register ?
461 // TODO: add registers such as '/'. See :h <c-r>
462 if ((key >= QLatin1Char('0') && key <= QLatin1Char('9')) || (key >= QLatin1Char('a') && key <= QLatin1Char('z')) || key == QLatin1Char('_')
463 || key == QLatin1Char('-') || key == QLatin1Char('+') || key == QLatin1Char('*') || key == QLatin1Char('"')) {
464 m_register = key;
465 } else {
466 return false;
467 }
468 commandInsertContentOfRegister();
469 return true;
470 }
471 return false;
472}
473
474// leave insert mode when esc, etc, is pressed. if leaving block
475// prepend/append, the inserted text will be added to all block lines. if
476// ctrl-c is used to exit insert mode this is not done.
477void InsertViMode::leaveInsertMode(bool force)
478{
479 m_view->abortCompletion();
480 if (!force) {
481 if (m_blockInsert != None) { // block append/prepend
482
483 // make sure cursor haven't been moved
484 if (m_blockRange.startLine == m_view->cursorPosition().line()) {
485 int start;
486 int len;
487 QString added;
489
490 switch (m_blockInsert) {
491 case Append:
492 case Prepend:
493 if (m_blockInsert == Append) {
494 start = m_blockRange.endColumn + 1;
495 } else {
496 start = m_blockRange.startColumn;
497 }
498
499 len = m_view->cursorPosition().column() - start;
500 added = getLine().mid(start, len);
501
502 c = KTextEditor::Cursor(m_blockRange.startLine, start);
503 for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) {
504 c.setLine(i);
505 doc()->insertText(c, added);
506 }
507 break;
508 case AppendEOL:
509 start = m_eolPos;
510 len = m_view->cursorPosition().column() - start;
511 added = getLine().mid(start, len);
512
513 c = KTextEditor::Cursor(m_blockRange.startLine, start);
514 for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) {
515 c.setLine(i);
516 c.setColumn(doc()->lineLength(i));
517 doc()->insertText(c, added);
518 }
519 break;
520 default:
521 error(QStringLiteral("not supported"));
522 }
523 }
524
525 m_blockInsert = None;
526 } else {
527 const QString added = doc()->text(KTextEditor::Range(m_viInputModeManager->marks()->getStartEditYanked(), m_view->cursorPosition()));
528
529 if (m_count > 1) {
530 for (unsigned int i = 0; i < m_count - 1; i++) {
531 if (m_countedRepeatsBeginOnNewLine) {
532 doc()->newLine(m_view);
533 }
534 doc()->insertText(m_view->cursorPosition(), added);
535 }
536 }
537 }
538 }
539 m_countedRepeatsBeginOnNewLine = false;
540 startNormalMode();
541}
542
543void InsertViMode::setBlockPrependMode(Range blockRange)
544{
545 // ignore if not more than one line is selected
546 if (blockRange.startLine != blockRange.endLine) {
547 m_blockInsert = Prepend;
548 m_blockRange = blockRange;
549 }
550}
551
552void InsertViMode::setBlockAppendMode(Range blockRange, BlockInsert b)
553{
554 Q_ASSERT(b == Append || b == AppendEOL);
555
556 // ignore if not more than one line is selected
557 if (blockRange.startLine != blockRange.endLine) {
558 m_blockRange = blockRange;
559 m_blockInsert = b;
560 if (b == AppendEOL) {
561 m_eolPos = doc()->lineLength(m_blockRange.startLine);
562 }
563 } else {
564 qCDebug(LOG_KTE) << "cursor moved. ignoring block append/prepend";
565 }
566}
567
568void InsertViMode::completionFinished()
569{
570 Completion::CompletionType completionType = Completion::PlainText;
571 if (m_view->cursorPosition() != m_textInsertedByCompletionEndPos) {
572 completionType = Completion::FunctionWithArgs;
573 } else if (m_textInsertedByCompletion.endsWith(QLatin1String("()")) || m_textInsertedByCompletion.endsWith(QLatin1String("();"))) {
574 completionType = Completion::FunctionWithoutArgs;
575 }
576 m_viInputModeManager->completionRecorder()->logCompletionEvent(
577 Completion(m_textInsertedByCompletion, KateViewConfig::global()->wordCompletionRemoveTail(), completionType));
578}
579
580void InsertViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range)
581{
582 if (m_isExecutingCompletion) {
583 m_textInsertedByCompletion += document->text(range);
584 m_textInsertedByCompletionEndPos = range.end();
585 }
586}
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
void setColumn(int column) noexcept
Set the cursor column to column.
Definition cursor.h:201
void setLine(int line) noexcept
Set the cursor line to line.
Definition cursor.h:183
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
A KParts derived class representing a text document.
Definition document.h:284
virtual QString text() const =0
Get the document content.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
void viewModeChanged(KTextEditor::View *view, KTextEditor::View::ViewMode mode)
This signal is emitted whenever the view mode of view changes.
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18n(const char *text, const TYPE &arg...)
QItemSelectionModel * selectionModel() const const
char32_t toLower(char32_t ucs4)
QModelIndex currentIndex() const const
int key() const const
Qt::KeyboardModifiers modifiers() const const
Q_EMITQ_EMIT
void chop(qsizetype n)
void clear()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isNull() const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & prepend(QChar ch)
Key_Backspace
NoModifier
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void repaint()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jun 14 2024 11:57:26 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.