File indexing completed on 2024-04-28 15:30:55
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 "katecmd.h" 0012 #include "katedocument.h" 0013 #include "kateglobal.h" 0014 #include "katepartdebug.h" 0015 #include "kateview.h" 0016 0017 #include <KLocalizedString> 0018 0019 #include <QDir> 0020 #include <QRegularExpression> 0021 #include <QUrl> 0022 0023 KateCommands::SedReplace *KateCommands::SedReplace::m_instance = nullptr; 0024 0025 static int backslashString(const QString &haystack, const QString &needle, int index) 0026 { 0027 int len = haystack.length(); 0028 int searchlen = needle.length(); 0029 bool evenCount = true; 0030 while (index < len) { 0031 if (haystack[index] == QLatin1Char('\\')) { 0032 evenCount = !evenCount; 0033 } else { 0034 // isn't a slash 0035 if (!evenCount) { 0036 if (QStringView(haystack).mid(index, searchlen) == needle) { 0037 return index - 1; 0038 } 0039 } 0040 evenCount = true; 0041 } 0042 ++index; 0043 } 0044 0045 return -1; 0046 } 0047 0048 // exchange "\t" for the actual tab character, for example 0049 static void exchangeAbbrevs(QString &str) 0050 { 0051 // the format is (findreplace)*[nullzero] 0052 const char *magic = "a\x07t\tn\n"; 0053 0054 while (*magic) { 0055 int index = 0; 0056 char replace = magic[1]; 0057 while ((index = backslashString(str, QString(QChar::fromLatin1(*magic)), index)) != -1) { 0058 str.replace(index, 2, QChar::fromLatin1(replace)); 0059 ++index; 0060 } 0061 ++magic; 0062 ++magic; 0063 } 0064 } 0065 0066 bool KateCommands::SedReplace::exec(class KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &r) 0067 { 0068 qCDebug(LOG_KTE) << "SedReplace::execCmd( " << cmd << " )"; 0069 if (r.isValid()) { 0070 qCDebug(LOG_KTE) << "Range: " << r; 0071 } 0072 0073 int findBeginPos = -1; 0074 int findEndPos = -1; 0075 int replaceBeginPos = -1; 0076 int replaceEndPos = -1; 0077 QString delimiter; 0078 if (!parse(cmd, delimiter, findBeginPos, findEndPos, replaceBeginPos, replaceEndPos)) { 0079 return false; 0080 } 0081 0082 const QStringView searchParamsString = QStringView(cmd).mid(cmd.lastIndexOf(delimiter)); 0083 const bool noCase = searchParamsString.contains(QLatin1Char('i')); 0084 const bool repeat = searchParamsString.contains(QLatin1Char('g')); 0085 const bool interactive = searchParamsString.contains(QLatin1Char('c')); 0086 0087 QString find = cmd.mid(findBeginPos, findEndPos - findBeginPos + 1); 0088 qCDebug(LOG_KTE) << "SedReplace: find =" << find; 0089 0090 QString replace = cmd.mid(replaceBeginPos, replaceEndPos - replaceBeginPos + 1); 0091 exchangeAbbrevs(replace); 0092 qCDebug(LOG_KTE) << "SedReplace: replace =" << replace; 0093 0094 if (find.isEmpty()) { 0095 // Nothing to do. 0096 return true; 0097 } 0098 0099 KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view); 0100 KTextEditor::DocumentPrivate *doc = kateView->doc(); 0101 if (!doc) { 0102 return false; 0103 } 0104 // Only current line ... 0105 int startLine = kateView->cursorPosition().line(); 0106 int endLine = kateView->cursorPosition().line(); 0107 // ... unless a range was provided. 0108 if (r.isValid()) { 0109 startLine = r.start().line(); 0110 endLine = r.end().line(); 0111 } 0112 0113 QSharedPointer<InteractiveSedReplacer> interactiveSedReplacer(new InteractiveSedReplacer(doc, find, replace, !noCase, !repeat, startLine, endLine)); 0114 0115 if (interactive) { 0116 const bool hasInitialMatch = interactiveSedReplacer->currentMatch().isValid(); 0117 if (!hasInitialMatch) { 0118 // Can't start an interactive sed replace if there is no initial match! 0119 msg = interactiveSedReplacer->finalStatusReportMessage(); 0120 return false; 0121 } 0122 interactiveSedReplace(kateView, interactiveSedReplacer); 0123 return true; 0124 } 0125 0126 interactiveSedReplacer->replaceAllRemaining(); 0127 msg = interactiveSedReplacer->finalStatusReportMessage(); 0128 0129 return true; 0130 } 0131 0132 bool KateCommands::SedReplace::interactiveSedReplace(KTextEditor::ViewPrivate *, QSharedPointer<InteractiveSedReplacer>) 0133 { 0134 qCDebug(LOG_KTE) << "Interactive sedreplace is only currently supported with Vi mode plus Vi emulated command bar."; 0135 return false; 0136 } 0137 0138 bool KateCommands::SedReplace::parse(const QString &sedReplaceString, 0139 QString &destDelim, 0140 int &destFindBeginPos, 0141 int &destFindEndPos, 0142 int &destReplaceBeginPos, 0143 int &destReplaceEndPos) 0144 { 0145 // valid delimiters are all non-word, non-space characters plus '_' 0146 static const QRegularExpression delim(QStringLiteral("^s\\s*([^\\w\\s]|_)"), QRegularExpression::UseUnicodePropertiesOption); 0147 auto match = delim.match(sedReplaceString); 0148 if (!match.hasMatch()) { 0149 return false; 0150 } 0151 0152 const QString d = match.captured(1); 0153 qCDebug(LOG_KTE) << "SedReplace: delimiter is '" << d << "'"; 0154 0155 QRegularExpression splitter(QStringLiteral("^s\\s*") + d + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)\\") + d 0156 + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)(\\") + d + QLatin1String("[igc]{0,3})?$"), 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 { 0192 m_currentSearchPos = KTextEditor::Cursor(startLine, 0); 0193 } 0194 0195 KTextEditor::Range KateCommands::SedReplace::InteractiveSedReplacer::currentMatch() 0196 { 0197 QVector<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->editBegin(); 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->editBegin(); 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 QVector<KTextEditor::Range> KateCommands::SedReplace::InteractiveSedReplacer::fullCurrentMatch() 0281 { 0282 if (m_currentSearchPos > m_doc->documentEnd()) { 0283 return QVector<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 QVector<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 }