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 | |