1/*
2 SPDX-FileCopyrightText: 2004, 2010 Joseph Wenninger <jowenn@kde.org>
3 SPDX-FileCopyrightText: 2009 Milian Wolff <mail@milianw.de>
4 SPDX-FileCopyrightText: 2014 Sven Brauch <svenbrauch@gmail.com>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include <QKeyEvent>
10#include <QQueue>
11#include <QRegularExpression>
12
13#include <ktexteditor/movingcursor.h>
14#include <ktexteditor/movingrange.h>
15
16#include "kateconfig.h"
17#include "katedocument.h"
18#include "kateglobal.h"
19#include "katepartdebug.h"
20#include "kateregexpsearch.h"
21#include "katetemplatehandler.h"
22#include "kateundomanager.h"
23#include "kateview.h"
24#include "script/katescriptmanager.h"
25
26using namespace KTextEditor;
27
28#define ifDebug(x)
29
30KateTemplateHandler::KateTemplateHandler(KTextEditor::ViewPrivate *view,
31 Cursor position,
32 const QString &templateString,
33 const QString &script,
34 KateUndoManager *undoManager)
35 : QObject(view)
36 , m_view(view)
37 , m_undoManager(undoManager)
38 , m_wholeTemplateRange()
39 , m_internalEdit(false)
40 , m_templateScript(script, KateScript::InputSCRIPT)
41{
42 Q_ASSERT(m_view);
43
44 m_templateScript.setView(m_view);
45
46 // remember selection, it will be lost when inserting the template
47 std::unique_ptr<MovingRange> selection(doc()->newMovingRange(range: m_view->selectionRange(), insertBehaviors: MovingRange::DoNotExpand));
48
49 m_undoManager->setAllowComplexMerge(true);
50
51 {
52 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: &KateTemplateHandler::slotTemplateInserted);
53 KTextEditor::Document::EditingTransaction t(doc());
54 // insert the raw template string
55 if (!doc()->insertText(position, s: templateString)) {
56 deleteLater();
57 return;
58 }
59 // now there must be a range, caught by the textInserted slot
60 Q_ASSERT(m_wholeTemplateRange);
61 doc()->align(view: m_view, range: *m_wholeTemplateRange);
62 }
63
64 // before initialization, restore selection (if any) so user scripts can retrieve it
65 m_view->setSelection(selection->toRange());
66 initializeTemplate();
67 // then delete the selected text (if any); it was replaced by the template
68 doc()->removeText(range: selection->toRange());
69
70 const bool have_editable_field = std::any_of(first: m_fields.constBegin(), last: m_fields.constEnd(), pred: [](const TemplateField &field) {
71 return (field.kind == TemplateField::Editable);
72 });
73 // only do complex stuff when required
74 if (have_editable_field) {
75 const auto views = doc()->views();
76 for (View *view : views) {
77 setupEventHandler(view);
78 }
79
80 // place the cursor at the first field and select stuff
81 jump(by: 1, initial: true);
82
83 connect(sender: doc(), signal: &KTextEditor::Document::viewCreated, context: this, slot: &KateTemplateHandler::slotViewCreated);
84 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: &KateTemplateHandler::updateDependentFields);
85 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textRemoved, context: this, slot: &KateTemplateHandler::updateDependentFields);
86 connect(sender: doc(), signal: &KTextEditor::Document::aboutToReload, context: this, slot: &KateTemplateHandler::deleteLater);
87
88 } else {
89 // when no interesting ranges got added, we can terminate directly
90 jumpToFinalCursorPosition();
91 deleteLater();
92 }
93}
94
95KateTemplateHandler::~KateTemplateHandler()
96{
97 m_undoManager->setAllowComplexMerge(false);
98}
99
100void KateTemplateHandler::sortFields()
101{
102 std::sort(first: m_fields.begin(), last: m_fields.end(), comp: [](const TemplateField &l, const TemplateField &r) {
103 // always sort the final cursor pos last
104 if (l.kind == TemplateField::FinalCursorPosition) {
105 return false;
106 }
107 if (r.kind == TemplateField::FinalCursorPosition) {
108 return true;
109 }
110 // sort by range
111 return l.range->toRange() < r.range->toRange();
112 });
113}
114
115void KateTemplateHandler::jumpToNextRange()
116{
117 jump(by: +1);
118}
119
120void KateTemplateHandler::jumpToPreviousRange()
121{
122 jump(by: -1);
123}
124
125void KateTemplateHandler::jump(int by, bool initial)
126{
127 Q_ASSERT(by == 1 || by == -1);
128 sortFields();
129
130 // find (editable) field index of current cursor position
131 int pos = -1;
132 auto cursor = view()->cursorPosition();
133 // if initial is not set, should start from the beginning (field -1)
134 if (!initial) {
135 pos = m_fields.indexOf(t: fieldForRange(range: KTextEditor::Range(cursor, cursor)));
136 }
137
138 // modulo field count and make positive
139 auto wrap = [this](int x) -> unsigned int {
140 x %= m_fields.size();
141 return x + (x < 0 ? m_fields.size() : 0);
142 };
143
144 pos = wrap(pos);
145 // choose field to jump to, including wrap-around
146 auto choose_next_field = [this, by, wrap](unsigned int from_field_index) {
147 for (int i = from_field_index + by;; i += by) {
148 auto wrapped_i = wrap(i);
149 auto kind = m_fields.at(i: wrapped_i).kind;
150 if (kind == TemplateField::Editable || kind == TemplateField::FinalCursorPosition) {
151 // found an editable field by walking into the desired direction
152 return wrapped_i;
153 }
154 if (wrapped_i == from_field_index) {
155 // nothing found, do nothing (i.e. keep cursor in current field)
156 break;
157 }
158 }
159 return from_field_index;
160 };
161
162 // jump
163 auto jump_to_field = m_fields.at(i: choose_next_field(pos));
164 view()->setCursorPosition(jump_to_field.range->toRange().start());
165 if (!jump_to_field.touched) {
166 // field was never edited by the user, so select its contents
167 view()->setSelection(jump_to_field.range->toRange());
168 }
169}
170
171void KateTemplateHandler::jumpToFinalCursorPosition()
172{
173 for (const auto &field : std::as_const(t&: m_fields)) {
174 if (field.kind == TemplateField::FinalCursorPosition) {
175 view()->setCursorPosition(field.range->toRange().start());
176 return;
177 }
178 }
179 view()->setCursorPosition(m_wholeTemplateRange->end());
180}
181
182void KateTemplateHandler::slotTemplateInserted(Document * /*document*/, Range range)
183{
184 m_wholeTemplateRange.reset(p: doc()->newMovingRange(range, insertBehaviors: MovingRange::ExpandLeft | MovingRange::ExpandRight));
185
186 disconnect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, receiver: this, slot: &KateTemplateHandler::slotTemplateInserted);
187}
188
189KTextEditor::DocumentPrivate *KateTemplateHandler::doc() const
190{
191 return m_view->doc();
192}
193
194void KateTemplateHandler::slotViewCreated(Document *document, View *view)
195{
196 Q_ASSERT(document == doc());
197 Q_UNUSED(document)
198 setupEventHandler(view);
199}
200
201void KateTemplateHandler::setupEventHandler(View *view)
202{
203 view->focusProxy()->installEventFilter(filterObj: this);
204}
205
206bool KateTemplateHandler::eventFilter(QObject *object, QEvent *event)
207{
208 // prevent indenting by eating the keypress event for TAB
209 if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
210 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
211 if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) {
212 if (!m_view->isCompletionActive()) {
213 return true;
214 }
215 }
216 }
217
218 // actually offer shortcuts for navigation
219 if (event->type() == QEvent::ShortcutOverride) {
220 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
221
222 if (keyEvent->key() == Qt::Key_Escape || (keyEvent->key() == Qt::Key_Return && keyEvent->modifiers() & Qt::AltModifier)) {
223 // terminate
224 jumpToFinalCursorPosition();
225 view()->clearSelection();
226 deleteLater();
227 keyEvent->accept();
228 return true;
229 } else if (keyEvent->key() == Qt::Key_Tab && !m_view->isCompletionActive()) {
230 if (keyEvent->modifiers() & Qt::ShiftModifier) {
231 jumpToPreviousRange();
232 } else {
233 jumpToNextRange();
234 }
235 keyEvent->accept();
236 return true;
237 } else if (keyEvent->key() == Qt::Key_Backtab && !m_view->isCompletionActive()) {
238 jumpToPreviousRange();
239 keyEvent->accept();
240 return true;
241 }
242 }
243
244 return QObject::eventFilter(watched: object, event);
245}
246
247/**
248 * Returns an attribute with \p color as background with @p alpha alpha value.
249 */
250Attribute::Ptr getAttribute(QColor color, int alpha = 230)
251{
252 Attribute::Ptr attribute(new Attribute());
253 color.setAlpha(alpha);
254 attribute->setBackground(QBrush(color));
255 return attribute;
256}
257
258void KateTemplateHandler::parseFields(const QString &templateText)
259{
260 // matches any field, i.e. the three forms ${foo}, ${foo=expr}, ${func()}
261 // this also captures escaped fields, i.e. \\${foo} etc.
262 static const QRegularExpression field(QStringLiteral("\\\\?\\${([^}]+)}"), QRegularExpression::UseUnicodePropertiesOption);
263 // matches the "foo=expr" form within a match of the above expression
264 static const QRegularExpression defaultField(QStringLiteral("\\w+=([^\\}]*)"), QRegularExpression::UseUnicodePropertiesOption);
265
266 // compute start cursor of a match
267 auto startOfMatch = [this, &templateText](const QRegularExpressionMatch &match) {
268 const auto offset = match.capturedStart(nth: 0);
269 const auto left = QStringView(templateText).left(n: offset);
270 const auto nl = QLatin1Char('\n');
271 const auto rel_lineno = left.count(c: nl);
272 const auto start = m_wholeTemplateRange->start().toCursor();
273 return Cursor(start.line(), rel_lineno == 0 ? start.column() : 0) + Cursor(rel_lineno, offset - left.lastIndexOf(c: nl) - 1);
274 };
275
276 // create a moving range spanning the given field
277 auto createMovingRangeForMatch = [this, startOfMatch](const QRegularExpressionMatch &match) {
278 auto matchStart = startOfMatch(match);
279 return doc()->newMovingRange(range: {matchStart, matchStart + Cursor(0, match.capturedLength(nth: 0))}, insertBehaviors: MovingRange::ExpandLeft | MovingRange::ExpandRight);
280 };
281
282 // list of escape backslashes to remove after parsing
283 QList<KTextEditor::Cursor> stripBackslashes;
284 auto fieldMatch = field.globalMatch(subject: templateText);
285 while (fieldMatch.hasNext()) {
286 const auto match = fieldMatch.next();
287 if (match.captured(nth: 0).startsWith(c: QLatin1Char('\\'))) {
288 // $ is escaped, not a field; mark the backslash for removal
289 // prepend it to the list so the characters are removed starting from the
290 // back and ranges do not move around
291 stripBackslashes.prepend(t: startOfMatch(match));
292 continue;
293 }
294 // a template field was found, instantiate a field object and populate it
295 auto defaultMatch = defaultField.match(subject: match.captured(nth: 0));
296 const QString contents = match.captured(nth: 1);
297 TemplateField f;
298 f.range.reset(p: createMovingRangeForMatch(match));
299 f.identifier = contents;
300 f.kind = TemplateField::Editable;
301 if (defaultMatch.hasMatch()) {
302 // the field has a default value, i.e. ${foo=3}
303 f.defaultValue = defaultMatch.captured(nth: 1);
304 f.identifier = QStringView(contents).left(n: contents.indexOf(c: QLatin1Char('='))).trimmed().toString();
305 } else if (f.identifier.contains(c: QLatin1Char('('))) {
306 // field is a function call when it contains an opening parenthesis
307 f.kind = TemplateField::FunctionCall;
308 } else if (f.identifier == QLatin1String("cursor")) {
309 // field marks the final cursor position
310 f.kind = TemplateField::FinalCursorPosition;
311 }
312 for (const auto &other : std::as_const(t&: m_fields)) {
313 if (other.kind == TemplateField::Editable && !(f == other) && other.identifier == f.identifier) {
314 // field is a mirror field
315 f.kind = TemplateField::Mirror;
316 break;
317 }
318 }
319 m_fields.append(t: f);
320 }
321
322 // remove escape characters
323 for (const auto &backslash : stripBackslashes) {
324 doc()->removeText(range: KTextEditor::Range(backslash, backslash + Cursor(0, 1)));
325 }
326}
327
328void KateTemplateHandler::setupFieldRanges()
329{
330 auto config = m_view->rendererConfig();
331 auto editableAttribute = getAttribute(color: config->templateEditablePlaceholderColor(), alpha: 255);
332 editableAttribute->setDynamicAttribute(type: Attribute::ActivateCaretIn, attribute: getAttribute(color: config->templateFocusedEditablePlaceholderColor(), alpha: 255));
333 auto notEditableAttribute = getAttribute(color: config->templateNotEditablePlaceholderColor(), alpha: 255);
334
335 // color the whole template
336 m_wholeTemplateRange->setAttribute(getAttribute(color: config->templateBackgroundColor(), alpha: 200));
337
338 // color all the template fields
339 for (const auto &field : std::as_const(t&: m_fields)) {
340 field.range->setAttribute(field.kind == TemplateField::Editable ? editableAttribute : notEditableAttribute);
341 }
342}
343
344void KateTemplateHandler::setupDefaultValues()
345{
346 for (const auto &field : std::as_const(t&: m_fields)) {
347 if (field.kind != TemplateField::Editable) {
348 continue;
349 }
350 QString value;
351 if (field.defaultValue.isEmpty()) {
352 // field has no default value specified; use its identifier
353 value = field.identifier;
354 } else {
355 // field has a default value; evaluate it with the JS engine
356 value = m_templateScript.evaluate(program: field.defaultValue).toString();
357 }
358 doc()->replaceText(range: field.range->toRange(), s: value);
359 }
360}
361
362void KateTemplateHandler::initializeTemplate()
363{
364 auto templateString = doc()->text(range: *m_wholeTemplateRange);
365 parseFields(templateText: templateString);
366 setupFieldRanges();
367 setupDefaultValues();
368
369 // call update for each field to set up the initial stuff
370 for (int i = 0; i < m_fields.size(); i++) {
371 auto &field = m_fields[i];
372 ifDebug(qCDebug(LOG_KTE) << "update field:" << field.range->toRange();) updateDependentFields(document: doc(), oldRange: field.range->toRange());
373 // remove "user edited field" mark set by the above call since it's not a real edit
374 field.touched = false;
375 }
376}
377
378const KateTemplateHandler::TemplateField KateTemplateHandler::fieldForRange(KTextEditor::Range range) const
379{
380 for (const auto &field : m_fields) {
381 if (field.range->contains(cursor: range.start()) || field.range->end() == range.start()) {
382 return field;
383 }
384 if (field.kind == TemplateField::FinalCursorPosition && range.end() == field.range->end().toCursor()) {
385 return field;
386 }
387 }
388 return {};
389}
390
391void KateTemplateHandler::updateDependentFields(Document *document, Range range)
392{
393 Q_ASSERT(document == doc());
394 Q_UNUSED(document);
395 if (!m_undoManager->isActive()) {
396 // currently undoing stuff; don't update fields
397 return;
398 }
399
400 bool in_range = m_wholeTemplateRange->toRange().contains(cursor: range.start());
401 bool at_end = m_wholeTemplateRange->toRange().end() == range.end() || m_wholeTemplateRange->toRange().end() == range.start();
402 if (m_wholeTemplateRange->toRange().isEmpty() || (!in_range && !at_end)) {
403 // edit outside template range, abort
404 ifDebug(qCDebug(LOG_KTE) << "edit outside template range, exiting";) deleteLater();
405 return;
406 }
407
408 if (m_internalEdit || range.isEmpty()) {
409 // internal or null edit; for internal edits, don't do anything
410 // to prevent unwanted recursion
411 return;
412 }
413
414 ifDebug(qCDebug(LOG_KTE) << "text changed" << document << range;)
415
416 // group all the changes into one undo transaction
417 KTextEditor::Document::EditingTransaction t(doc());
418
419 // find the field which was modified, if any
420 sortFields();
421 const auto changedField = fieldForRange(range);
422 if (changedField.kind == TemplateField::Invalid) {
423 // edit not within a field, nothing to do
424 ifDebug(qCDebug(LOG_KTE) << "edit not within a field:" << range;) return;
425 }
426 if (changedField.kind == TemplateField::FinalCursorPosition && doc()->text(range: changedField.range->toRange()).isEmpty()) {
427 // text changed at final cursor position: the user is done, so exit
428 // this is not executed when the field's range is not empty: in that case this call
429 // is for initial setup and we have to continue below
430 ifDebug(qCDebug(LOG_KTE) << "final cursor changed:" << range;) deleteLater();
431 return;
432 }
433
434 // turn off expanding left/right for all ranges except @p current;
435 // this prevents ranges from overlapping each other when they are adjacent
436 auto dontExpandOthers = [this](const TemplateField &current) {
437 for (qsizetype i = 0; i < m_fields.size(); i++) {
438 if (current.range != m_fields.at(i).range) {
439 m_fields.at(i).range->setInsertBehaviors(MovingRange::DoNotExpand);
440 } else {
441 m_fields.at(i).range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
442 }
443 }
444 };
445
446 // new contents of the changed template field
447 const auto &newText = doc()->text(range: changedField.range->toRange());
448 m_internalEdit = true;
449 // go through all fields and update the contents of the dependent ones
450 for (auto field = m_fields.begin(); field != m_fields.end(); field++) {
451 if (field->kind == TemplateField::FinalCursorPosition) {
452 // only relevant on first run
453 doc()->replaceText(range: field->range->toRange(), s: QString());
454 }
455
456 if (*field == changedField) {
457 // mark that the user changed this field
458 field->touched = true;
459 }
460
461 // If this is mirrored field with the same identifier as the
462 // changed one and the changed one is editable, mirror changes
463 // edits to non-editable mirror fields are ignored
464 if (field->kind == TemplateField::Mirror && changedField.kind == TemplateField::Editable && field->identifier == changedField.identifier) {
465 // editable field changed, mirror changes
466 dontExpandOthers(*field);
467 doc()->replaceText(range: field->range->toRange(), s: newText);
468 } else if (field->kind == TemplateField::FunctionCall) {
469 // replace field by result of function call
470 dontExpandOthers(*field);
471 // build map of objects in the scope to pass to the function
472 auto map = fieldMap();
473 const auto &callResult = m_templateScript.evaluate(program: field->identifier, env: map);
474 doc()->replaceText(range: field->range->toRange(), s: callResult.toString());
475 }
476 }
477 m_internalEdit = false;
478 updateRangeBehaviours();
479}
480
481void KateTemplateHandler::updateRangeBehaviours()
482{
483 KTextEditor::Cursor last = {-1, -1};
484 for (int i = 0; i < m_fields.size(); i++) {
485 auto field = m_fields.at(i);
486 auto end = field.range->end().toCursor();
487 auto start = field.range->start().toCursor();
488 if (field.kind == TemplateField::FinalCursorPosition) {
489 // final cursor position never grows
490 field.range->setInsertBehaviors(MovingRange::DoNotExpand);
491 } else if (start <= last) {
492 // ranges are adjacent, only expand to the right to prevent overlap
493 field.range->setInsertBehaviors(MovingRange::ExpandRight);
494 } else {
495 // ranges are not adjacent, can grow in both directions
496 field.range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
497 }
498 last = end;
499 }
500}
501
502KateScript::FieldMap KateTemplateHandler::fieldMap() const
503{
504 KateScript::FieldMap map;
505 for (const auto &field : m_fields) {
506 if (field.kind != TemplateField::Editable) {
507 // only editable fields are of interest to the scripts
508 continue;
509 }
510 map.insert(key: field.identifier, value: QJSValue(doc()->text(range: field.range->toRange())));
511 }
512 return map;
513}
514
515KTextEditor::ViewPrivate *KateTemplateHandler::view() const
516{
517 return m_view;
518}
519
520#include "moc_katetemplatehandler.cpp"
521

source code of ktexteditor/src/utils/katetemplatehandler.cpp