1 | /* |
2 | SPDX-FileCopyrightText: 2019 Dominik Haumann <dhaumann@kde.org> |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | |
7 | #include "katevariableexpansionmanager.h" |
8 | #include "katevariableexpansionhelpers.h" |
9 | |
10 | #include "katedocument.h" |
11 | #include "kateglobal.h" |
12 | |
13 | #include <KLocalizedString> |
14 | |
15 | #include <QAbstractItemModel> |
16 | #include <QListView> |
17 | #include <QVBoxLayout> |
18 | |
19 | #include <QDate> |
20 | #include <QDir> |
21 | #include <QFileInfo> |
22 | #include <QJSEngine> |
23 | #include <QLocale> |
24 | #include <QTime> |
25 | #include <QUuid> |
26 | |
27 | static void registerVariables(KateVariableExpansionManager &mng) |
28 | { |
29 | using KTextEditor::Variable; |
30 | |
31 | mng.addVariable(variable: Variable( |
32 | QStringLiteral("Document:FileBaseName" ), |
33 | i18n("File base name without path and suffix of the current document." ), |
34 | [](const QStringView &, KTextEditor::View *view) { |
35 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
36 | return QFileInfo(url).baseName(); |
37 | }, |
38 | false)); |
39 | mng.addVariable(variable: Variable( |
40 | QStringLiteral("Document:FileExtension" ), |
41 | i18n("File extension of the current document." ), |
42 | [](const QStringView &, KTextEditor::View *view) { |
43 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
44 | return QFileInfo(url).completeSuffix(); |
45 | }, |
46 | false)); |
47 | mng.addVariable(variable: Variable( |
48 | QStringLiteral("Document:FileName" ), |
49 | i18n("File name without path of the current document." ), |
50 | [](const QStringView &, KTextEditor::View *view) { |
51 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
52 | return QFileInfo(url).fileName(); |
53 | }, |
54 | false)); |
55 | mng.addVariable(variable: Variable( |
56 | QStringLiteral("Document:FilePath" ), |
57 | i18n("Full path of the current document including the file name." ), |
58 | [](const QStringView &, KTextEditor::View *view) { |
59 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
60 | return QFileInfo(url).absoluteFilePath(); |
61 | }, |
62 | false)); |
63 | mng.addVariable(variable: Variable( |
64 | QStringLiteral("Document:Text" ), |
65 | i18n("Contents of the current document." ), |
66 | [](const QStringView &, KTextEditor::View *view) { |
67 | return view ? view->document()->text() : QString(); |
68 | }, |
69 | false)); |
70 | mng.addVariable(variable: Variable( |
71 | QStringLiteral("Document:Path" ), |
72 | i18n("Full path of the current document excluding the file name." ), |
73 | [](const QStringView &, KTextEditor::View *view) { |
74 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
75 | return QFileInfo(url).absolutePath(); |
76 | }, |
77 | false)); |
78 | mng.addVariable(variable: Variable( |
79 | QStringLiteral("Document:NativeFilePath" ), |
80 | i18n("Full document path including file name, with native path separator (backslash on Windows)." ), |
81 | [](const QStringView &, KTextEditor::View *view) { |
82 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
83 | return url.isEmpty() ? QString() : QDir::toNativeSeparators(pathName: QFileInfo(url).absoluteFilePath()); |
84 | }, |
85 | false)); |
86 | mng.addVariable(variable: Variable( |
87 | QStringLiteral("Document:NativePath" ), |
88 | i18n("Full document path excluding file name, with native path separator (backslash on Windows)." ), |
89 | [](const QStringView &, KTextEditor::View *view) { |
90 | const auto url = view ? view->document()->url().toLocalFile() : QString(); |
91 | return url.isEmpty() ? QString() : QDir::toNativeSeparators(pathName: QFileInfo(url).absolutePath()); |
92 | }, |
93 | false)); |
94 | mng.addVariable(variable: Variable( |
95 | QStringLiteral("Document:Cursor:Line" ), |
96 | i18n("Line number of the text cursor position in current document (starts with 0)." ), |
97 | [](const QStringView &, KTextEditor::View *view) { |
98 | return view ? QString::number(view->cursorPosition().line()) : QString(); |
99 | }, |
100 | false)); |
101 | mng.addVariable(variable: Variable( |
102 | QStringLiteral("Document:Cursor:Column" ), |
103 | i18n("Column number of the text cursor position in current document (starts with 0)." ), |
104 | [](const QStringView &, KTextEditor::View *view) { |
105 | return view ? QString::number(view->cursorPosition().column()) : QString(); |
106 | }, |
107 | false)); |
108 | mng.addVariable(variable: Variable( |
109 | QStringLiteral("Document:Cursor:XPos" ), |
110 | i18n("X component in global screen coordinates of the cursor position." ), |
111 | [](const QStringView &, KTextEditor::View *view) { |
112 | return view ? QString::number(view->mapToGlobal(view->cursorPositionCoordinates()).x()) : QString(); |
113 | }, |
114 | false)); |
115 | mng.addVariable(variable: Variable( |
116 | QStringLiteral("Document:Cursor:YPos" ), |
117 | i18n("Y component in global screen coordinates of the cursor position." ), |
118 | [](const QStringView &, KTextEditor::View *view) { |
119 | return view ? QString::number(view->mapToGlobal(view->cursorPositionCoordinates()).y()) : QString(); |
120 | }, |
121 | false)); |
122 | mng.addVariable(variable: Variable( |
123 | QStringLiteral("Document:Selection:Text" ), |
124 | i18n("Text selection of the current document." ), |
125 | [](const QStringView &, KTextEditor::View *view) { |
126 | return (view && view->selection()) ? view->selectionText() : QString(); |
127 | }, |
128 | false)); |
129 | mng.addVariable(variable: Variable( |
130 | QStringLiteral("Document:Selection:StartLine" ), |
131 | i18n("Start line of selected text of the current document." ), |
132 | [](const QStringView &, KTextEditor::View *view) { |
133 | return (view && view->selection()) ? QString::number(view->selectionRange().start().line()) : QString(); |
134 | }, |
135 | false)); |
136 | mng.addVariable(variable: Variable( |
137 | QStringLiteral("Document:Selection:StartColumn" ), |
138 | i18n("Start column of selected text of the current document." ), |
139 | [](const QStringView &, KTextEditor::View *view) { |
140 | return (view && view->selection()) ? QString::number(view->selectionRange().start().column()) : QString(); |
141 | }, |
142 | false)); |
143 | mng.addVariable(variable: Variable( |
144 | QStringLiteral("Document:Selection:EndLine" ), |
145 | i18n("End line of selected text of the current document." ), |
146 | [](const QStringView &, KTextEditor::View *view) { |
147 | return (view && view->selection()) ? QString::number(view->selectionRange().end().line()) : QString(); |
148 | }, |
149 | false)); |
150 | mng.addVariable(variable: Variable( |
151 | QStringLiteral("Document:Selection:EndColumn" ), |
152 | i18n("End column of selected text of the current document." ), |
153 | [](const QStringView &, KTextEditor::View *view) { |
154 | return (view && view->selection()) ? QString::number(view->selectionRange().end().column()) : QString(); |
155 | }, |
156 | false)); |
157 | mng.addVariable(variable: Variable( |
158 | QStringLiteral("Document:RowCount" ), |
159 | i18n("Number of rows of the current document." ), |
160 | [](const QStringView &, KTextEditor::View *view) { |
161 | return view ? QString::number(view->document()->lines()) : QString(); |
162 | }, |
163 | false)); |
164 | mng.addVariable(variable: Variable( |
165 | QStringLiteral("Document:Variable:" ), |
166 | i18n("Read a document variable." ), |
167 | [](const QStringView &str, KTextEditor::View *view) { |
168 | return view ? qobject_cast<KTextEditor::DocumentPrivate *>(object: view->document())->variable(name: str.mid(pos: 18).toString()) : QString(); |
169 | }, |
170 | true)); |
171 | |
172 | mng.addVariable(variable: Variable( |
173 | QStringLiteral("Date:Locale" ), |
174 | i18n("The current date in current locale format." ), |
175 | [](const QStringView &, KTextEditor::View *) { |
176 | return QLocale().toString(date: QDate::currentDate(), format: QLocale::ShortFormat); |
177 | }, |
178 | false)); |
179 | mng.addVariable(variable: Variable( |
180 | QStringLiteral("Date:ISO" ), |
181 | i18n("The current date (ISO)." ), |
182 | [](const QStringView &, KTextEditor::View *) { |
183 | return QDate::currentDate().toString(format: Qt::ISODate); |
184 | }, |
185 | false)); |
186 | mng.addVariable(variable: Variable( |
187 | QStringLiteral("Date:" ), |
188 | i18n("The current date (QDate formatstring)." ), |
189 | [](const QStringView &str, KTextEditor::View *) { |
190 | return QDate::currentDate().toString(format: str.mid(pos: 5)); |
191 | }, |
192 | true)); |
193 | |
194 | mng.addVariable(variable: Variable( |
195 | QStringLiteral("Time:Locale" ), |
196 | i18n("The current time in current locale format." ), |
197 | [](const QStringView &, KTextEditor::View *) { |
198 | return QLocale().toString(time: QTime::currentTime(), format: QLocale::ShortFormat); |
199 | }, |
200 | false)); |
201 | mng.addVariable(variable: Variable( |
202 | QStringLiteral("Time:ISO" ), |
203 | i18n("The current time (ISO)." ), |
204 | [](const QStringView &, KTextEditor::View *) { |
205 | return QTime::currentTime().toString(f: Qt::ISODate); |
206 | }, |
207 | false)); |
208 | mng.addVariable(variable: Variable( |
209 | QStringLiteral("Time:" ), |
210 | i18n("The current time (QTime formatstring)." ), |
211 | [](const QStringView &str, KTextEditor::View *) { |
212 | return QTime::currentTime().toString(format: str.mid(pos: 5)); |
213 | }, |
214 | true)); |
215 | |
216 | mng.addVariable(variable: Variable( |
217 | QStringLiteral("ENV:" ), |
218 | i18n("Access to environment variables." ), |
219 | [](const QStringView &str, KTextEditor::View *) { |
220 | return QString::fromLocal8Bit(ba: qgetenv(varName: str.mid(pos: 4).toLocal8Bit().constData())); |
221 | }, |
222 | true)); |
223 | |
224 | mng.addVariable(variable: Variable( |
225 | QStringLiteral("JS:" ), |
226 | i18n("Evaluate simple JavaScript statements." ), |
227 | [](const QStringView &str, KTextEditor::View *) { |
228 | QJSEngine jsEngine; |
229 | const QJSValue out = jsEngine.evaluate(program: str.toString()); |
230 | return out.toString(); |
231 | }, |
232 | true)); |
233 | |
234 | mng.addVariable(variable: Variable( |
235 | QStringLiteral("PercentEncoded:" ), |
236 | i18n("Encode text to make it URL compatible." ), |
237 | [](const QStringView &str, KTextEditor::View *) { |
238 | return QString::fromUtf8(ba: QUrl::toPercentEncoding(str.mid(pos: 15).toString())); |
239 | }, |
240 | true)); |
241 | |
242 | mng.addVariable(variable: Variable( |
243 | QStringLiteral("UUID" ), |
244 | i18n("Generate a new UUID." ), |
245 | [](const QStringView &, KTextEditor::View *) { |
246 | return QUuid::createUuid().toString(mode: QUuid::WithoutBraces); |
247 | }, |
248 | false)); |
249 | } |
250 | |
251 | KateVariableExpansionManager::KateVariableExpansionManager(QObject *parent) |
252 | : QObject(parent) |
253 | { |
254 | // register default variables for expansion |
255 | registerVariables(mng&: *this); |
256 | } |
257 | |
258 | bool KateVariableExpansionManager::addVariable(const KTextEditor::Variable &var) |
259 | { |
260 | if (!var.isValid()) { |
261 | return false; |
262 | } |
263 | |
264 | // reject duplicates |
265 | const auto alreadyExists = std::any_of(first: m_variables.begin(), last: m_variables.end(), pred: [&var](const KTextEditor::Variable &v) { |
266 | return var.name() == v.name(); |
267 | }); |
268 | if (alreadyExists) { |
269 | return false; |
270 | } |
271 | |
272 | // require a ':' in prefix matches (aka %{JS:1+1}) |
273 | if (var.isPrefixMatch() && !var.name().contains(c: QLatin1Char(':'))) { |
274 | return false; |
275 | } |
276 | |
277 | m_variables.push_back(t: var); |
278 | return true; |
279 | } |
280 | |
281 | bool KateVariableExpansionManager::removeVariable(const QString &name) |
282 | { |
283 | auto it = std::find_if(first: m_variables.begin(), last: m_variables.end(), pred: [&name](const KTextEditor::Variable &var) { |
284 | return var.name() == name; |
285 | }); |
286 | if (it != m_variables.end()) { |
287 | m_variables.erase(pos: it); |
288 | return true; |
289 | } |
290 | return false; |
291 | } |
292 | |
293 | KTextEditor::Variable KateVariableExpansionManager::variable(const QString &name) const |
294 | { |
295 | auto it = std::find_if(first: m_variables.begin(), last: m_variables.end(), pred: [&name](const KTextEditor::Variable &var) { |
296 | return var.name() == name; |
297 | }); |
298 | if (it != m_variables.end()) { |
299 | return *it; |
300 | } |
301 | return {}; |
302 | } |
303 | |
304 | const QList<KTextEditor::Variable> &KateVariableExpansionManager::variables() const |
305 | { |
306 | return m_variables; |
307 | } |
308 | |
309 | bool KateVariableExpansionManager::expandVariable(const QString &name, KTextEditor::View *view, QString &output) const |
310 | { |
311 | // first try exact matches |
312 | auto var = variable(name); |
313 | if (!var.isValid()) { |
314 | // try prefix matching |
315 | for (auto &v : m_variables) { |
316 | if (v.isPrefixMatch() && name.startsWith(s: v.name())) { |
317 | var = v; |
318 | break; |
319 | } |
320 | } |
321 | } |
322 | |
323 | if (var.isValid()) { |
324 | output = var.evaluate(prefix: name, view); |
325 | return true; |
326 | } |
327 | |
328 | return false; |
329 | } |
330 | |
331 | QString KateVariableExpansionManager::expandText(const QString &text, KTextEditor::View *view) |
332 | { |
333 | return KateMacroExpander::expandMacro(input: text, view); |
334 | } |
335 | |
336 | void KateVariableExpansionManager::showDialog(const QList<QWidget *> &widgets, const QStringList &names) const |
337 | { |
338 | // avoid any work in case no widgets or only nullptrs were provided |
339 | if (widgets.isEmpty() || std::all_of(first: widgets.cbegin(), last: widgets.cend(), pred: [](const QWidget *w) { |
340 | return w == nullptr; |
341 | })) { |
342 | return; |
343 | } |
344 | |
345 | // collect variables |
346 | QList<KTextEditor::Variable> vars; |
347 | if (!names.isEmpty()) { |
348 | for (const auto &name : names) { |
349 | const auto var = variable(name); |
350 | if (var.isValid()) { |
351 | vars.push_back(t: var); |
352 | } |
353 | // else: Not found, silently ignore for now |
354 | // Maybe raise a qCWarning(LOG_KTE)? |
355 | } |
356 | } else { |
357 | vars = variables(); |
358 | } |
359 | |
360 | // if we have no vars at all, do nothing |
361 | if (vars.isEmpty()) { |
362 | return; |
363 | } |
364 | |
365 | // find parent dialog (for taskbar sharing, centering, ...) |
366 | QWidget *parentDialog = nullptr; |
367 | for (auto widget : widgets) { |
368 | if (widget) { |
369 | parentDialog = widget->window(); |
370 | break; |
371 | } |
372 | } |
373 | |
374 | // show dialog |
375 | auto dlg = new KateVariableExpansionDialog(parentDialog); |
376 | for (auto widget : widgets) { |
377 | if (widget) { |
378 | dlg->addWidget(widget); |
379 | } |
380 | } |
381 | |
382 | // add provided variables... |
383 | for (const auto &var : std::as_const(t&: vars)) { |
384 | if (var.isValid()) { |
385 | dlg->addVariable(variable: var); |
386 | } |
387 | } |
388 | } |
389 | |
390 | // kate: space-indent on; indent-width 4; replace-tabs on; |
391 | |