File indexing completed on 2024-04-28 07:47:06

0001 /*
0002     SPDX-FileCopyrightText: KDE Developers
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "completionreplayer.h"
0008 #include "completionrecorder.h"
0009 #include "katedocument.h"
0010 #include "katepartdebug.h"
0011 #include "kateview.h"
0012 #include "lastchangerecorder.h"
0013 #include "macrorecorder.h"
0014 #include <vimode/inputmodemanager.h>
0015 
0016 #include <QKeyEvent>
0017 #include <QRegularExpression>
0018 
0019 using namespace KateVi;
0020 
0021 CompletionReplayer::CompletionReplayer(InputModeManager *viInputModeManager)
0022     : m_viInputModeManager(viInputModeManager)
0023 {
0024 }
0025 
0026 CompletionReplayer::~CompletionReplayer() = default;
0027 
0028 void CompletionReplayer::start(const CompletionList &completions)
0029 {
0030     m_nextCompletionIndex.push(0);
0031     m_CompletionsToReplay.push(completions);
0032 }
0033 
0034 void CompletionReplayer::stop()
0035 {
0036     m_CompletionsToReplay.pop();
0037     m_nextCompletionIndex.pop();
0038 }
0039 
0040 void CompletionReplayer::replay()
0041 {
0042     const Completion completion = nextCompletion();
0043     KTextEditor::ViewPrivate *m_view = m_viInputModeManager->view();
0044     KTextEditor::DocumentPrivate *doc = m_view->doc();
0045 
0046     // Find beginning of the word.
0047     KTextEditor::Cursor cursorPos = m_view->cursorPosition();
0048     KTextEditor::Cursor wordStart = KTextEditor::Cursor::invalid();
0049     if (!doc->characterAt(cursorPos).isLetterOrNumber() && doc->characterAt(cursorPos) != QLatin1Char('_')) {
0050         cursorPos.setColumn(cursorPos.column() - 1);
0051     }
0052     while (cursorPos.column() >= 0 && (doc->characterAt(cursorPos).isLetterOrNumber() || doc->characterAt(cursorPos) == QLatin1Char('_'))) {
0053         wordStart = cursorPos;
0054         cursorPos.setColumn(cursorPos.column() - 1);
0055     }
0056     // Find end of current word.
0057     cursorPos = m_view->cursorPosition();
0058     KTextEditor::Cursor wordEnd = KTextEditor::Cursor(cursorPos.line(), cursorPos.column() - 1);
0059     while (cursorPos.column() < doc->lineLength(cursorPos.line())
0060            && (doc->characterAt(cursorPos).isLetterOrNumber() || doc->characterAt(cursorPos) == QLatin1Char('_'))) {
0061         wordEnd = cursorPos;
0062         cursorPos.setColumn(cursorPos.column() + 1);
0063     }
0064     QString completionText = completion.completedText();
0065     const KTextEditor::Range currentWord = KTextEditor::Range(wordStart, KTextEditor::Cursor(wordEnd.line(), wordEnd.column() + 1));
0066     // Should we merge opening brackets? Yes, if completion is a function with arguments and after the cursor
0067     // there is (optional whitespace) followed by an open bracket.
0068     int offsetFinalCursorPosBy = 0;
0069     if (completion.completionType() == Completion::FunctionWithArgs) {
0070         const int nextMergableBracketAfterCursorPos = findNextMergeableBracketPos(currentWord.end());
0071         if (nextMergableBracketAfterCursorPos != -1) {
0072             if (completionText.endsWith(QLatin1String("()"))) {
0073                 // Strip "()".
0074                 completionText.chop(2);
0075             } else if (completionText.endsWith(QLatin1String("();"))) {
0076                 // Strip "();".
0077                 completionText.chop(3);
0078             }
0079             // Ensure cursor ends up after the merged open bracket.
0080             offsetFinalCursorPosBy = nextMergableBracketAfterCursorPos + 1;
0081         } else {
0082             if (!completionText.endsWith(QLatin1String("()")) && !completionText.endsWith(QLatin1String("();"))) {
0083                 // Original completion merged with an opening bracket; we'll have to add our own brackets.
0084                 completionText.append(QLatin1String("()"));
0085             }
0086             // Position cursor correctly i.e. we'll have added "functionname()" or "functionname();"; need to step back by
0087             // one or two to be after the opening bracket.
0088             offsetFinalCursorPosBy = completionText.endsWith(QLatin1Char(';')) ? -2 : -1;
0089         }
0090     }
0091     KTextEditor::Cursor deleteEnd =
0092         completion.removeTail() ? currentWord.end() : KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + 0);
0093 
0094     if (currentWord.isValid()) {
0095         doc->removeText(KTextEditor::Range(currentWord.start(), deleteEnd));
0096         doc->insertText(currentWord.start(), completionText);
0097     } else {
0098         doc->insertText(m_view->cursorPosition(), completionText);
0099     }
0100 
0101     if (offsetFinalCursorPosBy != 0) {
0102         m_view->setCursorPosition(KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + offsetFinalCursorPosBy));
0103     }
0104 
0105     if (!m_viInputModeManager->lastChangeRecorder()->isReplaying()) {
0106         Q_ASSERT(m_viInputModeManager->macroRecorder()->isReplaying());
0107         // Post the completion back: it needs to be added to the "last change" list ...
0108         m_viInputModeManager->completionRecorder()->logCompletionEvent(completion);
0109         // ... but don't log the ctrl-space that led to this call to replayCompletion(), as
0110         // a synthetic ctrl-space was just added to the last change keypresses by logCompletionEvent(), and we don't
0111         // want to duplicate them!
0112         m_viInputModeManager->doNotLogCurrentKeypress();
0113     }
0114 }
0115 
0116 Completion CompletionReplayer::nextCompletion()
0117 {
0118     Q_ASSERT(m_viInputModeManager->lastChangeRecorder()->isReplaying() || m_viInputModeManager->macroRecorder()->isReplaying());
0119 
0120     if (m_nextCompletionIndex.top() >= m_CompletionsToReplay.top().length()) {
0121         qCDebug(LOG_KTE) << "Something wrong here: requesting more completions for macro than we actually have.  Returning dummy.";
0122         return Completion(QString(), false, Completion::PlainText);
0123     }
0124 
0125     return m_CompletionsToReplay.top()[m_nextCompletionIndex.top()++];
0126 }
0127 
0128 int CompletionReplayer::findNextMergeableBracketPos(const KTextEditor::Cursor startPos) const
0129 {
0130     KTextEditor::DocumentPrivate *doc = m_viInputModeManager->view()->doc();
0131     const QString lineAfterCursor = doc->text(KTextEditor::Range(startPos, KTextEditor::Cursor(startPos.line(), doc->lineLength(startPos.line()))));
0132     static const QRegularExpression whitespaceThenOpeningBracket(QStringLiteral("^\\s*(\\()"), QRegularExpression::UseUnicodePropertiesOption);
0133     QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(lineAfterCursor);
0134     int nextMergableBracketAfterCursorPos = -1;
0135     if (match.hasMatch()) {
0136         nextMergableBracketAfterCursorPos = match.capturedStart(1);
0137     }
0138     return nextMergableBracketAfterCursorPos;
0139 }