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
20using 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
29namespace
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
41RegExp(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
56CommandRangeExpressionParser::CommandRangeExpressionParser(InputModeManager *vimanager)
57 : m_viInputModeManager(vimanager)
58{
59}
60
61QString 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
76KTextEditor::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
117int 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
159bool 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
171bool 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
183bool 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
195bool 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
207bool 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
229bool 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

source code of ktexteditor/src/vimode/commandrangeexpressionparser.cpp