1/*
2 SPDX-FileCopyrightText: KDE Developers
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "completionreplayer.h"
8#include "completionrecorder.h"
9#include "katedocument.h"
10#include "katepartdebug.h"
11#include "kateview.h"
12#include "lastchangerecorder.h"
13#include "macrorecorder.h"
14#include <vimode/inputmodemanager.h>
15
16#include <QKeyEvent>
17#include <QRegularExpression>
18
19using namespace KateVi;
20
21CompletionReplayer::CompletionReplayer(InputModeManager *viInputModeManager)
22 : m_viInputModeManager(viInputModeManager)
23{
24}
25
26CompletionReplayer::~CompletionReplayer() = default;
27
28void CompletionReplayer::start(const CompletionList &completions)
29{
30 m_nextCompletionIndex.push(t: 0);
31 m_CompletionsToReplay.push(t: completions);
32}
33
34void CompletionReplayer::stop()
35{
36 m_CompletionsToReplay.pop();
37 m_nextCompletionIndex.pop();
38}
39
40void CompletionReplayer::replay()
41{
42 const Completion completion = nextCompletion();
43 KTextEditor::ViewPrivate *m_view = m_viInputModeManager->view();
44 KTextEditor::DocumentPrivate *doc = m_view->doc();
45
46 // Find beginning of the word.
47 KTextEditor::Cursor cursorPos = m_view->cursorPosition();
48 KTextEditor::Cursor wordStart = KTextEditor::Cursor::invalid();
49 if (!doc->characterAt(position: cursorPos).isLetterOrNumber() && doc->characterAt(position: cursorPos) != QLatin1Char('_')) {
50 cursorPos.setColumn(cursorPos.column() - 1);
51 }
52 while (cursorPos.column() >= 0 && (doc->characterAt(position: cursorPos).isLetterOrNumber() || doc->characterAt(position: cursorPos) == QLatin1Char('_'))) {
53 wordStart = cursorPos;
54 cursorPos.setColumn(cursorPos.column() - 1);
55 }
56 // Find end of current word.
57 cursorPos = m_view->cursorPosition();
58 KTextEditor::Cursor wordEnd = KTextEditor::Cursor(cursorPos.line(), cursorPos.column() - 1);
59 while (cursorPos.column() < doc->lineLength(line: cursorPos.line())
60 && (doc->characterAt(position: cursorPos).isLetterOrNumber() || doc->characterAt(position: cursorPos) == QLatin1Char('_'))) {
61 wordEnd = cursorPos;
62 cursorPos.setColumn(cursorPos.column() + 1);
63 }
64 QString completionText = completion.completedText();
65 const KTextEditor::Range currentWord = KTextEditor::Range(wordStart, KTextEditor::Cursor(wordEnd.line(), wordEnd.column() + 1));
66 // Should we merge opening brackets? Yes, if completion is a function with arguments and after the cursor
67 // there is (optional whitespace) followed by an open bracket.
68 int offsetFinalCursorPosBy = 0;
69 if (completion.completionType() == Completion::FunctionWithArgs) {
70 const int nextMergableBracketAfterCursorPos = findNextMergeableBracketPos(startPos: currentWord.end());
71 if (nextMergableBracketAfterCursorPos != -1) {
72 if (completionText.endsWith(s: QLatin1String("()"))) {
73 // Strip "()".
74 completionText.chop(n: 2);
75 } else if (completionText.endsWith(s: QLatin1String("();"))) {
76 // Strip "();".
77 completionText.chop(n: 3);
78 }
79 // Ensure cursor ends up after the merged open bracket.
80 offsetFinalCursorPosBy = nextMergableBracketAfterCursorPos + 1;
81 } else {
82 if (!completionText.endsWith(s: QLatin1String("()")) && !completionText.endsWith(s: QLatin1String("();"))) {
83 // Original completion merged with an opening bracket; we'll have to add our own brackets.
84 completionText.append(s: QLatin1String("()"));
85 }
86 // Position cursor correctly i.e. we'll have added "functionname()" or "functionname();"; need to step back by
87 // one or two to be after the opening bracket.
88 offsetFinalCursorPosBy = completionText.endsWith(c: QLatin1Char(';')) ? -2 : -1;
89 }
90 }
91 KTextEditor::Cursor deleteEnd =
92 completion.removeTail() ? currentWord.end() : KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + 0);
93
94 if (currentWord.isValid()) {
95 doc->removeText(range: KTextEditor::Range(currentWord.start(), deleteEnd));
96 doc->insertText(position: currentWord.start(), s: completionText);
97 } else {
98 doc->insertText(position: m_view->cursorPosition(), s: completionText);
99 }
100
101 if (offsetFinalCursorPosBy != 0) {
102 m_view->setCursorPosition(KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + offsetFinalCursorPosBy));
103 }
104
105 if (!m_viInputModeManager->lastChangeRecorder()->isReplaying()) {
106 Q_ASSERT(m_viInputModeManager->macroRecorder()->isReplaying());
107 // Post the completion back: it needs to be added to the "last change" list ...
108 m_viInputModeManager->completionRecorder()->logCompletionEvent(completion);
109 // ... but don't log the ctrl-space that led to this call to replayCompletion(), as
110 // a synthetic ctrl-space was just added to the last change keypresses by logCompletionEvent(), and we don't
111 // want to duplicate them!
112 m_viInputModeManager->doNotLogCurrentKeypress();
113 }
114}
115
116Completion CompletionReplayer::nextCompletion()
117{
118 Q_ASSERT(m_viInputModeManager->lastChangeRecorder()->isReplaying() || m_viInputModeManager->macroRecorder()->isReplaying());
119
120 if (m_nextCompletionIndex.top() >= m_CompletionsToReplay.top().length()) {
121 qCDebug(LOG_KTE) << "Something wrong here: requesting more completions for macro than we actually have. Returning dummy.";
122 return Completion(QString(), false, Completion::PlainText);
123 }
124
125 return m_CompletionsToReplay.top()[m_nextCompletionIndex.top()++];
126}
127
128int CompletionReplayer::findNextMergeableBracketPos(const KTextEditor::Cursor startPos) const
129{
130 KTextEditor::DocumentPrivate *doc = m_viInputModeManager->view()->doc();
131 const QString lineAfterCursor = doc->text(range: KTextEditor::Range(startPos, KTextEditor::Cursor(startPos.line(), doc->lineLength(line: startPos.line()))));
132 static const QRegularExpression whitespaceThenOpeningBracket(QStringLiteral("^\\s*(\\()"), QRegularExpression::UseUnicodePropertiesOption);
133 QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(subject: lineAfterCursor);
134 int nextMergableBracketAfterCursorPos = -1;
135 if (match.hasMatch()) {
136 nextMergableBracketAfterCursorPos = match.capturedStart(nth: 1);
137 }
138 return nextMergableBracketAfterCursorPos;
139}
140

source code of ktexteditor/src/vimode/completionreplayer.cpp