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 | |
19 | using namespace KateVi; |
20 | |
21 | CompletionReplayer::CompletionReplayer(InputModeManager *viInputModeManager) |
22 | : m_viInputModeManager(viInputModeManager) |
23 | { |
24 | } |
25 | |
26 | CompletionReplayer::~CompletionReplayer() = default; |
27 | |
28 | void CompletionReplayer::start(const CompletionList &completions) |
29 | { |
30 | m_nextCompletionIndex.push(t: 0); |
31 | m_CompletionsToReplay.push(t: completions); |
32 | } |
33 | |
34 | void CompletionReplayer::stop() |
35 | { |
36 | m_CompletionsToReplay.pop(); |
37 | m_nextCompletionIndex.pop(); |
38 | } |
39 | |
40 | void 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 | |
116 | Completion 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 | |
128 | int 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 | |