| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2008-2009 Erlend Hamberg <ehamberg@gmail.com> |
| 3 | SPDX-FileCopyrightText: 2011 Svyatoslav Kuzmich <svatoslav1@gmail.com> |
| 4 | SPDX-FileCopyrightText: 2012 Vegard Øye |
| 5 | SPDX-FileCopyrightText: 2013 Simon St James <kdedevel@etotheipiplusone.com> |
| 6 | |
| 7 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 8 | */ |
| 9 | |
| 10 | #include "commandrangeexpressionparser.h" |
| 11 | |
| 12 | #include "katedocument.h" |
| 13 | #include "kateview.h" |
| 14 | #include "marks.h" |
| 15 | #include <vimode/inputmodemanager.h> |
| 16 | |
| 17 | #include <QRegularExpression> |
| 18 | #include <QStringList> |
| 19 | |
| 20 | using namespace KateVi; |
| 21 | |
| 22 | #define RegExp(name, pattern) \ |
| 23 | inline const QRegularExpression &name() \ |
| 24 | { \ |
| 25 | static const QRegularExpression regex(QStringLiteral(pattern), QRegularExpression::UseUnicodePropertiesOption); \ |
| 26 | return regex; \ |
| 27 | } |
| 28 | |
| 29 | namespace |
| 30 | { |
| 31 | #define RE_MARK "\\'[0-9a-z><\\+\\*\\_]" |
| 32 | #define RE_THISLINE "\\." |
| 33 | #define RE_LASTLINE "\\$" |
| 34 | #define RE_LINE "\\d+" |
| 35 | #define RE_FORWARDSEARCH "/[^/]*/?" |
| 36 | #define RE_BACKWARDSEARCH "\\?[^?]*\\??" |
| 37 | #define RE_BASE "(?:" RE_MARK ")|(?:" RE_LINE ")|(?:" RE_THISLINE ")|(?:" RE_LASTLINE ")|(?:" RE_FORWARDSEARCH ")|(?:" RE_BACKWARDSEARCH ")" |
| 38 | #define RE_OFFSET "[+-](?:" RE_BASE ")?" |
| 39 | #define RE_POSITION "(" RE_BASE ")((?:" RE_OFFSET ")*)" |
| 40 | |
| 41 | RegExp(RE_Line, RE_LINE) RegExp(RE_LastLine, RE_LASTLINE) RegExp(RE_ThisLine, RE_THISLINE) RegExp(RE_Mark, RE_MARK) RegExp(RE_ForwardSearch, "^/([^/]*)/?$" ) |
| 42 | RegExp(RE_BackwardSearch, "^\\?([^?]*)\\??$" ) RegExp(RE_CalculatePositionSplit, "[-+](?!([+-]|$))" ) |
| 43 | // The range regexp contains seven groups: the first is the start position, the second is |
| 44 | // the base of the start position, the third is the offset of the start position, the |
| 45 | // fourth is the end position including a leading comma, the fifth is end position |
| 46 | // without the comma, the sixth is the base of the end position, and the seventh is the |
| 47 | // offset of the end position. The third and fourth groups may be empty, and the |
| 48 | // fifth, sixth and seventh groups are contingent on the fourth group. |
| 49 | inline const QRegularExpression &RE_CmdRange() |
| 50 | { |
| 51 | static const QRegularExpression regex(QStringLiteral("^(" RE_POSITION ")((?:,(" RE_POSITION "))?)" )); |
| 52 | return regex; |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | CommandRangeExpressionParser::CommandRangeExpressionParser(InputModeManager *vimanager) |
| 57 | : m_viInputModeManager(vimanager) |
| 58 | { |
| 59 | } |
| 60 | |
| 61 | QString CommandRangeExpressionParser::parseRangeString(const QString &command) |
| 62 | { |
| 63 | if (command.isEmpty()) { |
| 64 | return QString(); |
| 65 | } |
| 66 | |
| 67 | if (command.at(i: 0) == QLatin1Char('%')) { |
| 68 | return QStringLiteral("%" ); |
| 69 | } |
| 70 | |
| 71 | QRegularExpressionMatch rangeMatch = RE_CmdRange().match(subject: command); |
| 72 | |
| 73 | return rangeMatch.hasMatch() ? rangeMatch.captured() : QString(); |
| 74 | } |
| 75 | |
| 76 | KTextEditor::Range CommandRangeExpressionParser::parseRange(const QString &command, QString &destTransformedCommand) const |
| 77 | { |
| 78 | if (command.isEmpty()) { |
| 79 | return KTextEditor::Range::invalid(); |
| 80 | } |
| 81 | |
| 82 | QString commandTmp = command; |
| 83 | |
| 84 | // expand '%' to '1,$' ("all lines") if at the start of the line |
| 85 | if (commandTmp.at(i: 0) == QLatin1Char('%')) { |
| 86 | commandTmp.replace(i: 0, len: 1, QStringLiteral("1,$" )); |
| 87 | } |
| 88 | |
| 89 | QRegularExpressionMatch rangeMatch = RE_CmdRange().match(subject: commandTmp); |
| 90 | |
| 91 | if (!rangeMatch.hasMatch()) { |
| 92 | return KTextEditor::Range::invalid(); |
| 93 | } |
| 94 | |
| 95 | QString position_string1 = rangeMatch.captured(nth: 1); |
| 96 | QString position_string2 = rangeMatch.captured(nth: 4); |
| 97 | int position1 = calculatePosition(string: position_string1); |
| 98 | int position2 = (position_string2.isEmpty()) ? position1 : calculatePosition(string: rangeMatch.captured(nth: 5)); |
| 99 | |
| 100 | commandTmp.remove(re: RE_CmdRange()); |
| 101 | |
| 102 | // Vi indexes lines starting from 1; however, it does accept 0 as a valid line index |
| 103 | // and treats it as 1 |
| 104 | position1 = (position1 == 0) ? 1 : position1; |
| 105 | position2 = (position2 == 0) ? 1 : position2; |
| 106 | |
| 107 | // special case: if the command is just a number with an optional +/- prefix, rewrite to "goto" |
| 108 | if (commandTmp.isEmpty()) { |
| 109 | destTransformedCommand = QStringLiteral("goto %1" ).arg(a: position1); |
| 110 | return KTextEditor::Range::invalid(); |
| 111 | } else { |
| 112 | destTransformedCommand = commandTmp; |
| 113 | return KTextEditor::Range(KTextEditor::Range(position1 - 1, 0, position2 - 1, 0)); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | int CommandRangeExpressionParser::calculatePosition(const QString &string) const |
| 118 | { |
| 119 | int pos = 0; |
| 120 | QList<bool> operators_list; |
| 121 | const QStringList split = string.split(sep: RE_CalculatePositionSplit()); |
| 122 | QList<int> values; |
| 123 | |
| 124 | for (const QString &line : split) { |
| 125 | pos += line.size(); |
| 126 | |
| 127 | if (pos < string.size()) { |
| 128 | if (string.at(i: pos) == QLatin1Char('+')) { |
| 129 | operators_list.push_back(t: true); |
| 130 | } else if (string.at(i: pos) == QLatin1Char('-')) { |
| 131 | operators_list.push_back(t: false); |
| 132 | } else { |
| 133 | Q_ASSERT(false); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | ++pos; |
| 138 | |
| 139 | matchLineNumber(line, values) || matchLastLine(line, values) || matchThisLine(line, values) || matchMark(line, values) |
| 140 | || matchForwardSearch(line, values) || matchBackwardSearch(line, values); |
| 141 | } |
| 142 | |
| 143 | if (values.isEmpty()) { |
| 144 | return -1; |
| 145 | } |
| 146 | |
| 147 | int result = values.at(i: 0); |
| 148 | for (int i = 0; i < operators_list.size(); ++i) { |
| 149 | if (operators_list.at(i)) { |
| 150 | result += values.at(i: i + 1); |
| 151 | } else { |
| 152 | result -= values.at(i: i + 1); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | return result; |
| 157 | } |
| 158 | |
| 159 | bool CommandRangeExpressionParser::matchLineNumber(const QString &line, QList<int> &values) |
| 160 | { |
| 161 | QRegularExpressionMatch match = RE_Line().match(subject: line); |
| 162 | |
| 163 | if (!match.hasMatch() || match.capturedLength() != line.length()) { |
| 164 | return false; |
| 165 | } |
| 166 | |
| 167 | values.push_back(t: line.toInt()); |
| 168 | return true; |
| 169 | } |
| 170 | |
| 171 | bool CommandRangeExpressionParser::matchLastLine(const QString &line, QList<int> &values) const |
| 172 | { |
| 173 | QRegularExpressionMatch match = RE_LastLine().match(subject: line); |
| 174 | |
| 175 | if (!match.hasMatch() || match.capturedLength() != line.length()) { |
| 176 | return false; |
| 177 | } |
| 178 | |
| 179 | values.push_back(t: m_viInputModeManager->view()->doc()->lines()); |
| 180 | return true; |
| 181 | } |
| 182 | |
| 183 | bool CommandRangeExpressionParser::matchThisLine(const QString &line, QList<int> &values) const |
| 184 | { |
| 185 | QRegularExpressionMatch match = RE_ThisLine().match(subject: line); |
| 186 | |
| 187 | if (!match.hasMatch() || match.capturedLength() != line.length()) { |
| 188 | return false; |
| 189 | } |
| 190 | |
| 191 | values.push_back(t: m_viInputModeManager->view()->cursorPosition().line() + 1); |
| 192 | return true; |
| 193 | } |
| 194 | |
| 195 | bool CommandRangeExpressionParser::matchMark(const QString &line, QList<int> &values) const |
| 196 | { |
| 197 | QRegularExpressionMatch match = RE_Mark().match(subject: line); |
| 198 | |
| 199 | if (!match.hasMatch() || match.capturedLength() != line.length()) { |
| 200 | return false; |
| 201 | } |
| 202 | |
| 203 | values.push_back(t: m_viInputModeManager->marks()->getMarkPosition(mark: line.at(i: 1)).line() + 1); |
| 204 | return true; |
| 205 | } |
| 206 | |
| 207 | bool CommandRangeExpressionParser::matchForwardSearch(const QString &line, QList<int> &values) const |
| 208 | { |
| 209 | QRegularExpressionMatch match = RE_ForwardSearch().match(subject: line); |
| 210 | |
| 211 | if (!match.hasMatch()) { |
| 212 | return false; |
| 213 | } |
| 214 | |
| 215 | QString pattern = match.captured(nth: 1); |
| 216 | KTextEditor::Range range(m_viInputModeManager->view()->cursorPosition(), m_viInputModeManager->view()->doc()->documentEnd()); |
| 217 | QList<KTextEditor::Range> matchingLines = m_viInputModeManager->view()->doc()->searchText(range, pattern, options: KTextEditor::Regex); |
| 218 | |
| 219 | if (matchingLines.isEmpty()) { |
| 220 | return true; |
| 221 | } |
| 222 | |
| 223 | int lineNumber = matchingLines.first().start().line(); |
| 224 | |
| 225 | values.push_back(t: lineNumber + 1); |
| 226 | return true; |
| 227 | } |
| 228 | |
| 229 | bool CommandRangeExpressionParser::matchBackwardSearch(const QString &line, QList<int> &values) const |
| 230 | { |
| 231 | QRegularExpressionMatch match = RE_BackwardSearch().match(subject: line); |
| 232 | |
| 233 | if (!match.hasMatch()) { |
| 234 | return false; |
| 235 | } |
| 236 | |
| 237 | QString pattern = match.captured(nth: 1); |
| 238 | KTextEditor::Range range(KTextEditor::Cursor(0, 0), m_viInputModeManager->view()->cursorPosition()); |
| 239 | QList<KTextEditor::Range> matchingLines = m_viInputModeManager->view()->doc()->searchText(range, pattern, options: KTextEditor::Regex); |
| 240 | |
| 241 | if (matchingLines.isEmpty()) { |
| 242 | return true; |
| 243 | } |
| 244 | |
| 245 | int lineNumber = matchingLines.first().start().line(); |
| 246 | |
| 247 | values.push_back(t: lineNumber + 1); |
| 248 | return true; |
| 249 | } |
| 250 | |