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 }