File indexing completed on 2024-04-14 03:55:30

0001 /*
0002     SPDX-FileCopyrightText: 2003-2005 Anders Lund <anders@alweb.dk>
0003     SPDX-FileCopyrightText: 2001-2010 Christoph Cullmann <cullmann@kde.org>
0004     SPDX-FileCopyrightText: 2001 Charles Samuels <charles@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "katesedcmd.h"
0010 
0011 #include "katedocument.h"
0012 #include "kateglobal.h"
0013 #include "katepartdebug.h"
0014 #include "kateview.h"
0015 
0016 #include <KLocalizedString>
0017 
0018 #include <QDir>
0019 #include <QRegularExpression>
0020 #include <QUrl>
0021 
0022 KateCommands::SedReplace *KateCommands::SedReplace::m_instance = nullptr;
0023 
0024 static int backslashString(const QString &haystack, const QString &needle, int index)
0025 {
0026     int len = haystack.length();
0027     int searchlen = needle.length();
0028     bool evenCount = true;
0029     while (index < len) {
0030         if (haystack[index] == QLatin1Char('\\')) {
0031             evenCount = !evenCount;
0032         } else {
0033             // isn't a slash
0034             if (!evenCount) {
0035                 if (QStringView(haystack).mid(index, searchlen) == needle) {
0036                     return index - 1;
0037                 }
0038             }
0039             evenCount = true;
0040         }
0041         ++index;
0042     }
0043 
0044     return -1;
0045 }
0046 
0047 // exchange "\t" for the actual tab character, for example
0048 static void exchangeAbbrevs(QString &str)
0049 {
0050     // the format is (findreplace)*[nullzero]
0051     const char *magic = "a\x07t\tn\n";
0052 
0053     while (*magic) {
0054         int index = 0;
0055         char replace = magic[1];
0056         while ((index = backslashString(str, QString(QChar::fromLatin1(*magic)), index)) != -1) {
0057             str.replace(index, 2, QChar::fromLatin1(replace));
0058             ++index;
0059         }
0060         ++magic;
0061         ++magic;
0062     }
0063 }
0064 
0065 bool KateCommands::SedReplace::exec(class KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &r)
0066 {
0067     qCDebug(LOG_KTE) << "SedReplace::execCmd( " << cmd << " )";
0068     if (r.isValid()) {
0069         qCDebug(LOG_KTE) << "Range: " << r;
0070     }
0071 
0072     int findBeginPos = -1;
0073     int findEndPos = -1;
0074     int replaceBeginPos = -1;
0075     int replaceEndPos = -1;
0076     QString delimiter;
0077     if (!parse(cmd, delimiter, findBeginPos, findEndPos, replaceBeginPos, replaceEndPos)) {
0078         return false;
0079     }
0080 
0081     const QStringView searchParamsString = QStringView(cmd).mid(cmd.lastIndexOf(delimiter));
0082     const bool noCase = searchParamsString.contains(QLatin1Char('i'));
0083     const bool repeat = searchParamsString.contains(QLatin1Char('g'));
0084     const bool interactive = searchParamsString.contains(QLatin1Char('c'));
0085 
0086     QString find = cmd.mid(findBeginPos, findEndPos - findBeginPos + 1);
0087     qCDebug(LOG_KTE) << "SedReplace: find =" << find;
0088 
0089     QString replace = cmd.mid(replaceBeginPos, replaceEndPos - replaceBeginPos + 1);
0090     exchangeAbbrevs(replace);
0091     qCDebug(LOG_KTE) << "SedReplace: replace =" << replace;
0092 
0093     if (find.isEmpty()) {
0094         // Nothing to do.
0095         return true;
0096     }
0097 
0098     KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view);
0099     KTextEditor::DocumentPrivate *doc = kateView->doc();
0100     if (!doc) {
0101         return false;
0102     }
0103     // Only current line ...
0104     int startLine = kateView->cursorPosition().line();
0105     int endLine = kateView->cursorPosition().line();
0106     // ... unless a range was provided.
0107     if (r.isValid()) {
0108         startLine = r.start().line();
0109         endLine = r.end().line();
0110     }
0111 
0112     std::shared_ptr<InteractiveSedReplacer> interactiveSedReplacer(new InteractiveSedReplacer(doc, find, replace, !noCase, !repeat, startLine, endLine));
0113 
0114     if (interactive) {
0115         const bool hasInitialMatch = interactiveSedReplacer->currentMatch().isValid();
0116         if (!hasInitialMatch) {
0117             // Can't start an interactive sed replace if there is no initial match!
0118             msg = interactiveSedReplacer->finalStatusReportMessage();
0119             return false;
0120         }
0121         interactiveSedReplace(kateView, interactiveSedReplacer);
0122         return true;
0123     }
0124 
0125     interactiveSedReplacer->replaceAllRemaining();
0126     msg = interactiveSedReplacer->finalStatusReportMessage();
0127 
0128     return true;
0129 }
0130 
0131 bool KateCommands::SedReplace::interactiveSedReplace(KTextEditor::ViewPrivate *, std::shared_ptr<InteractiveSedReplacer>)
0132 {
0133     qCDebug(LOG_KTE) << "Interactive sedreplace is only currently supported with Vi mode plus Vi emulated command bar.";
0134     return false;
0135 }
0136 
0137 bool KateCommands::SedReplace::parse(const QString &sedReplaceString,
0138                                      QString &destDelim,
0139                                      int &destFindBeginPos,
0140                                      int &destFindEndPos,
0141                                      int &destReplaceBeginPos,
0142                                      int &destReplaceEndPos)
0143 {
0144     // valid delimiters are all non-word, non-space characters plus '_'
0145     static const QRegularExpression delim(QStringLiteral("^s\\s*([^\\w\\s]|_)"), QRegularExpression::UseUnicodePropertiesOption);
0146     auto match = delim.match(sedReplaceString);
0147     if (!match.hasMatch()) {
0148         return false;
0149     }
0150 
0151     const QString d = match.captured(1);
0152     qCDebug(LOG_KTE) << "SedReplace: delimiter is '" << d << "'";
0153 
0154     QRegularExpression splitter(QStringLiteral("^s\\s*") + d + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)\\") + d
0155                                     + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)(\\") + d + QLatin1String("[igc]{0,3})?$"),
0156                                 QRegularExpression::UseUnicodePropertiesOption);
0157     match = splitter.match(sedReplaceString);
0158     if (!match.hasMatch()) {
0159         return false;
0160     }
0161 
0162     const QString find = match.captured(1);
0163     const QString replace = match.captured(2);
0164 
0165     destDelim = d;
0166     destFindBeginPos = match.capturedStart(1);
0167     destFindEndPos = match.capturedStart(1) + find.length() - 1;
0168     destReplaceBeginPos = match.capturedStart(2);
0169     destReplaceEndPos = match.capturedStart(2) + replace.length() - 1;
0170 
0171     return true;
0172 }
0173 
0174 KateCommands::SedReplace::InteractiveSedReplacer::InteractiveSedReplacer(KTextEditor::DocumentPrivate *doc,
0175                                                                          const QString &findPattern,
0176                                                                          const QString &replacePattern,
0177                                                                          bool caseSensitive,
0178                                                                          bool onlyOnePerLine,
0179                                                                          int startLine,
0180                                                                          int endLine)
0181     : m_findPattern(findPattern)
0182     , m_replacePattern(replacePattern)
0183     , m_onlyOnePerLine(onlyOnePerLine)
0184     , m_endLine(endLine)
0185     , m_doc(doc)
0186     , m_regExpSearch(doc)
0187     , m_caseSensitive(caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive)
0188     , m_numReplacementsDone(0)
0189     , m_numLinesTouched(0)
0190     , m_lastChangedLineNum(-1)
0191     , m_currentSearchPos(KTextEditor::Cursor(startLine, 0))
0192 {
0193 }
0194 
0195 KTextEditor::Range KateCommands::SedReplace::InteractiveSedReplacer::currentMatch()
0196 {
0197     QList<KTextEditor::Range> matches = fullCurrentMatch();
0198 
0199     if (matches.isEmpty()) {
0200         return KTextEditor::Range::invalid();
0201     }
0202 
0203     if (matches.first().start().line() > m_endLine) {
0204         return KTextEditor::Range::invalid();
0205     }
0206 
0207     return matches.first();
0208 }
0209 
0210 void KateCommands::SedReplace::InteractiveSedReplacer::skipCurrentMatch()
0211 {
0212     const KTextEditor::Range currentMatch = this->currentMatch();
0213     m_currentSearchPos = currentMatch.end();
0214     if (m_onlyOnePerLine && currentMatch.start().line() == m_currentSearchPos.line()) {
0215         m_currentSearchPos = KTextEditor::Cursor(m_currentSearchPos.line() + 1, 0);
0216     }
0217 }
0218 
0219 void KateCommands::SedReplace::InteractiveSedReplacer::replaceCurrentMatch()
0220 {
0221     const KTextEditor::Range currentMatch = this->currentMatch();
0222     const QString currentMatchText = m_doc->text(currentMatch);
0223     const QString replacementText = replacementTextForCurrentMatch();
0224 
0225     m_doc->editStart();
0226     m_doc->removeText(currentMatch);
0227     m_doc->insertText(currentMatch.start(), replacementText);
0228     m_doc->editEnd();
0229 
0230     // Begin next search from directly after replacement.
0231     if (!replacementText.contains(QLatin1Char('\n'))) {
0232         const int moveChar = currentMatch.isEmpty() ? 1 : 0; // if the search was for \s*, make sure we advance a char
0233         const int col = currentMatch.start().column() + replacementText.length() + moveChar;
0234 
0235         m_currentSearchPos = KTextEditor::Cursor(currentMatch.start().line(), col);
0236     } else {
0237         m_currentSearchPos = KTextEditor::Cursor(currentMatch.start().line() + replacementText.count(QLatin1Char('\n')),
0238                                                  replacementText.length() - replacementText.lastIndexOf(QLatin1Char('\n')) - 1);
0239     }
0240     if (m_onlyOnePerLine) {
0241         // Drop down to next line.
0242         m_currentSearchPos = KTextEditor::Cursor(m_currentSearchPos.line() + 1, 0);
0243     }
0244 
0245     // Adjust end line down by the number of new newlines just added, minus the number taken away.
0246     m_endLine += replacementText.count(QLatin1Char('\n'));
0247     m_endLine -= currentMatchText.count(QLatin1Char('\n'));
0248 
0249     m_numReplacementsDone++;
0250     if (m_lastChangedLineNum != currentMatch.start().line()) {
0251         // Counting "swallowed" lines as being "touched".
0252         m_numLinesTouched += currentMatchText.count(QLatin1Char('\n')) + 1;
0253     }
0254     m_lastChangedLineNum = m_currentSearchPos.line();
0255 }
0256 
0257 void KateCommands::SedReplace::InteractiveSedReplacer::replaceAllRemaining()
0258 {
0259     m_doc->editStart();
0260     while (currentMatch().isValid()) {
0261         replaceCurrentMatch();
0262     }
0263     m_doc->editEnd();
0264 }
0265 
0266 QString KateCommands::SedReplace::InteractiveSedReplacer::currentMatchReplacementConfirmationMessage()
0267 {
0268     return i18n("replace with %1?", replacementTextForCurrentMatch().replace(QLatin1Char('\n'), QLatin1String("\\n")));
0269 }
0270 
0271 QString KateCommands::SedReplace::InteractiveSedReplacer::finalStatusReportMessage() const
0272 {
0273     return i18ncp("%2 is the translation of the next message",
0274                   "1 replacement done on %2",
0275                   "%1 replacements done on %2",
0276                   m_numReplacementsDone,
0277                   i18ncp("substituted into the previous message", "1 line", "%1 lines", m_numLinesTouched));
0278 }
0279 
0280 const QList<KTextEditor::Range> KateCommands::SedReplace::InteractiveSedReplacer::fullCurrentMatch()
0281 {
0282     if (m_currentSearchPos > m_doc->documentEnd()) {
0283         return QList<KTextEditor::Range>();
0284     }
0285 
0286     QRegularExpression::PatternOptions options;
0287     if (m_caseSensitive == Qt::CaseInsensitive) {
0288         options |= (QRegularExpression::CaseInsensitiveOption);
0289     }
0290     return m_regExpSearch.search(m_findPattern, KTextEditor::Range(m_currentSearchPos, m_doc->documentEnd()), false /* search backwards */, options);
0291 }
0292 
0293 QString KateCommands::SedReplace::InteractiveSedReplacer::replacementTextForCurrentMatch()
0294 {
0295     const QList<KTextEditor::Range> captureRanges = fullCurrentMatch();
0296     QStringList captureTexts;
0297     captureTexts.reserve(captureRanges.size());
0298     for (KTextEditor::Range captureRange : captureRanges) {
0299         captureTexts << m_doc->text(captureRange);
0300     }
0301     const QString replacementText = m_regExpSearch.buildReplacement(m_replacePattern, captureTexts, 0);
0302     return replacementText;
0303 }