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 SPDX-FileCopyrightText: 2025 Mirian Margiani <mixosaurus+kde@pm.me>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include <QKeyEvent>
11#include <QQueue>
12#include <QRegularExpression>
13#include <algorithm>
14
15#include <ktexteditor/movingcursor.h>
16#include <ktexteditor/movingrange.h>
17
18#include "cursor.h"
19#include "kateconfig.h"
20#include "katedocument.h"
21#include "kateglobal.h"
22#include "katepartdebug.h"
23#include "kateregexpsearch.h"
24#include "katetemplatehandler.h"
25#include "kateundomanager.h"
26#include "kateview.h"
27#include "script/katescriptmanager.h"
28
29using namespace KTextEditor;
30
31#define ifDebug(x)
32
33KateTemplateHandler::KateTemplateHandler(KTextEditor::ViewPrivate *view,
34 Cursor position,
35 const QString &templateString,
36 const QString &script,
37 KateUndoManager *undoManager)
38 : QObject(view)
39 , m_view(view)
40 , m_undoManager(undoManager)
41 , m_wholeTemplateRange()
42 , m_internalEdit(false)
43 , m_templateScript(script, KateScript::InputSCRIPT)
44{
45 Q_ASSERT(m_view);
46
47 m_templateScript.setView(m_view);
48
49 // remember selection, it will be lost when inserting the template
50 std::unique_ptr<MovingRange> selection(doc()->newMovingRange(range: m_view->selectionRange(), insertBehaviors: MovingRange::DoNotExpand));
51
52 m_undoManager->setAllowComplexMerge(true);
53
54 {
55 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: &KateTemplateHandler::slotTemplateInserted);
56 KTextEditor::Document::EditingTransaction t(doc());
57 // insert the raw template string
58 if (!doc()->insertText(position, s: templateString)) {
59 // insertText() fails if the document is read only. That is already
60 // being checked before the template handler is created, so this
61 // code should be unreachable.
62 deleteLater();
63 return;
64 }
65 // now there must be a range, caught by the textInserted slot
66 Q_ASSERT(m_wholeTemplateRange);
67 doc()->align(view: m_view, range: *m_wholeTemplateRange);
68 }
69
70 // before initialization, restore selection (if any) so user scripts can retrieve it
71 m_view->setSelection(selection->toRange());
72 initializeTemplate();
73 // then delete the selected text (if any); it was replaced by the template
74 doc()->removeText(range: selection->toRange());
75
76 const bool have_editable_field = std::any_of(first: m_fields.constBegin(), last: m_fields.constEnd(), pred: [](const TemplateField &field) {
77 return (field.kind == TemplateField::Editable);
78 });
79 // only do complex stuff when required
80 if (have_editable_field) {
81 const auto views = doc()->views();
82 for (View *view : views) {
83 setupEventHandler(view);
84 }
85
86 // place the cursor at the first field and select stuff
87 jump(by: 1, initial: true);
88
89 connect(sender: doc(), signal: &KTextEditor::Document::viewCreated, context: this, slot: &KateTemplateHandler::slotViewCreated);
90 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: [this](KTextEditor::Document *doc, KTextEditor::Range range) {
91 updateDependentFields(document: doc, oldRange: range, textRemoved: false);
92 });
93 connect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textRemoved, context: this, slot: [this](KTextEditor::Document *doc, KTextEditor::Range range, const QString &) {
94 updateDependentFields(document: doc, oldRange: range, textRemoved: true);
95 });
96 connect(sender: doc(), signal: &KTextEditor::Document::aboutToReload, context: this, slot: &KateTemplateHandler::deleteLater);
97
98 } else {
99 // when no interesting ranges got added, we can terminate directly
100 jumpToFinalCursorPosition();
101 deleteLater();
102 }
103}
104
105KateTemplateHandler::~KateTemplateHandler()
106{
107 m_undoManager->setAllowComplexMerge(false);
108}
109
110void KateTemplateHandler::jumpToNextRange()
111{
112 jump(by: +1);
113}
114
115void KateTemplateHandler::jumpToPreviousRange()
116{
117 jump(by: -1);
118}
119
120void KateTemplateHandler::jump(int by, bool initial)
121{
122 Q_ASSERT(by == 1 || by == -1);
123
124 Cursor start;
125
126 if (initial) {
127 start = Cursor{-1, -1};
128 } else {
129 start = view()->cursorPosition();
130 }
131
132 QMap<Cursor, bool> starts;
133 QList<TemplateField *> fields;
134 QList<TemplateField *> finalCursors;
135 bool startAtFinalCursor = false;
136
137 std::sort(first: m_fields.begin(), last: m_fields.end());
138 for (auto &field : m_fields) {
139 if (field.removed) {
140 continue;
141 }
142
143 if (field.kind == TemplateField::Editable) {
144 if (!starts.contains(key: field.range->start()) || !field.range->isEmpty()) {
145 starts.insert(key: field.range->start(), value: true);
146 fields.append(t: &field);
147 }
148 } else if (field.kind == TemplateField::FinalCursorPosition) {
149 finalCursors.append(t: &field);
150
151 if (field.range->start() == start) {
152 startAtFinalCursor = true;
153 }
154 }
155 }
156
157 auto findNext = [this, &fields, &by](const Cursor &cursor) -> TemplateField * {
158 for (const auto &field : fields) {
159 if (by > 0) {
160 if (field->range->start() > cursor
161 || (field->range->start() == cursor && this->view()->selection() && this->view()->selectionRange() != field->range->toRange())
162 || (field->range->start() == cursor && !this->view()->selection() && !field->range->isEmpty())) {
163 return field;
164 }
165 } else {
166 if ((field->range->end() == cursor && !field->touched && !field->range->isEmpty() && this->view()->selectionRange() != field->range->toRange())
167 || field->range->end() < cursor) {
168 return field;
169 }
170 }
171 }
172
173 return nullptr;
174 };
175
176 auto findNextFinalCursor = [&finalCursors, &by](const Cursor &cursor) -> TemplateField * {
177 for (const auto &field : finalCursors) {
178 if ((by > 0 && field->range->start() > cursor) || (by < 0 && field->range->start() < cursor)) {
179 return field;
180 }
181 }
182
183 return nullptr;
184 };
185
186 auto updateView = [this](const TemplateField *field) {
187 if (!field->touched && !field->range->isEmpty()) {
188 view()->setSelection(*field->range);
189 } else {
190 view()->clearSelection();
191 }
192
193 view()->setCursorPosition(field->range->toRange().end());
194 };
195
196 Cursor wrap;
197 if (by > 0) {
198 wrap = Cursor{-1, -1};
199 } else {
200 wrap = Cursor{std::numeric_limits<int>::max(), std::numeric_limits<int>::max()};
201 std::sort(first: finalCursors.rbegin(), last: finalCursors.rend());
202 std::sort(first: fields.rbegin(), last: fields.rend());
203 }
204
205 if (!startAtFinalCursor) {
206 if (auto found = findNext(start)) {
207 updateView(found);
208 return;
209 }
210
211 if (auto found = findNextFinalCursor(wrap)) {
212 updateView(found);
213 return;
214 }
215 } else {
216 if (auto found = findNextFinalCursor(start)) {
217 updateView(found);
218 return;
219 }
220 }
221
222 if (auto found = findNext(wrap)) {
223 updateView(found);
224 return;
225 }
226}
227
228void KateTemplateHandler::jumpToFinalCursorPosition()
229{
230 for (const auto &field : std::as_const(t&: m_fields)) {
231 if (field.kind == TemplateField::FinalCursorPosition) {
232 view()->setCursorPosition(field.range->toRange().start());
233 return;
234 }
235 }
236 view()->setCursorPosition(m_wholeTemplateRange->end());
237}
238
239void KateTemplateHandler::slotTemplateInserted(Document * /*document*/, Range range)
240{
241 m_wholeTemplateRange.reset(p: doc()->newMovingRange(range, insertBehaviors: MovingRange::ExpandLeft | MovingRange::ExpandRight));
242
243 disconnect(sender: doc(), signal: &KTextEditor::DocumentPrivate::textInsertedRange, receiver: this, slot: &KateTemplateHandler::slotTemplateInserted);
244}
245
246KTextEditor::DocumentPrivate *KateTemplateHandler::doc() const
247{
248 return m_view->doc();
249}
250
251void KateTemplateHandler::slotViewCreated(Document *document, View *view)
252{
253 Q_ASSERT(document == doc());
254 Q_UNUSED(document)
255 setupEventHandler(view);
256}
257
258void KateTemplateHandler::setupEventHandler(View *view)
259{
260 view->focusProxy()->installEventFilter(filterObj: this);
261}
262
263bool KateTemplateHandler::eventFilter(QObject *object, QEvent *event)
264{
265 // prevent indenting by eating the keypress event for TAB
266 if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
267 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
268 if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) {
269 if (!m_view->isCompletionActive()) {
270 return true;
271 }
272 }
273 }
274
275 // actually offer shortcuts for navigation
276 if (event->type() == QEvent::ShortcutOverride) {
277 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
278
279 if (keyEvent->key() == Qt::Key_Escape || (keyEvent->key() == Qt::Key_Return && keyEvent->modifiers() & Qt::AltModifier)) {
280 // terminate
281 jumpToFinalCursorPosition();
282 view()->clearSelection();
283 deleteLater();
284 keyEvent->accept();
285 return true;
286 } else if (keyEvent->key() == Qt::Key_Tab && !m_view->isCompletionActive()) {
287 if (keyEvent->modifiers() & Qt::ShiftModifier) {
288 jumpToPreviousRange();
289 } else {
290 jumpToNextRange();
291 }
292 keyEvent->accept();
293 return true;
294 } else if (keyEvent->key() == Qt::Key_Backtab && !m_view->isCompletionActive()) {
295 jumpToPreviousRange();
296 keyEvent->accept();
297 return true;
298 }
299 }
300
301 return QObject::eventFilter(watched: object, event);
302}
303
304/**
305 * Returns an attribute with \p color as background with @p alpha alpha value.
306 */
307Attribute::Ptr getAttribute(QColor color, int alpha = 230)
308{
309 Attribute::Ptr attribute(new Attribute());
310 color.setAlpha(alpha);
311 attribute->setBackground(QBrush(color));
312 return attribute;
313}
314
315void KateTemplateHandler::parseFields(const QString &templateText)
316{
317 // matches any field, i.e. the three forms ${foo}, ${foo=expr}, ${func()}
318 static const QRegularExpression field(QStringLiteral(R"((?<!\\)(?<slash>(?:\\\\)*)\${(?<body>[^}]+)})"), QRegularExpression::UseUnicodePropertiesOption);
319 // matches the "foo=expr" form within a match of the above expression
320 static const QRegularExpression defaultField(QStringLiteral(R"(\w+=([^}]*))"), QRegularExpression::UseUnicodePropertiesOption);
321 // this only captures escaped fields, i.e. \\${foo} etc.
322 static const QRegularExpression escapedField(QStringLiteral(R"((?<!\\)(?<slash>\\(?:\\\\)*)\${[^}]+})"), QRegularExpression::UseUnicodePropertiesOption);
323
324 static const QString slash = QStringLiteral("slash");
325 static const QString body = QStringLiteral("body");
326
327 // compute start cursor of a match
328 auto startOfMatch = [this, &templateText](const QRegularExpressionMatch &match) {
329 const auto offset = match.capturedStart(nth: 0);
330 const auto left = QStringView(templateText).left(n: offset);
331 const auto nl = QLatin1Char('\n');
332 const auto rel_lineno = left.count(c: nl);
333 const auto start = m_wholeTemplateRange->start().toCursor();
334
335 return Cursor(start.line(), rel_lineno == 0 ? start.column() : 0) + Cursor(rel_lineno, offset - left.lastIndexOf(c: nl) - 1);
336 };
337
338 // create a moving range spanning the given field
339 auto createMovingRangeForMatch = [this, startOfMatch](const QRegularExpressionMatch &match) {
340 auto matchStart = startOfMatch(match);
341 auto slashOffset = Cursor{0, static_cast<int>(match.capturedLength(name: slash))};
342 return doc()->newMovingRange(range: {matchStart + slashOffset, matchStart + Cursor(0, match.capturedLength(nth: 0))},
343 insertBehaviors: MovingRange::ExpandLeft | MovingRange::ExpandRight);
344 };
345
346 // list of escape backslashes to remove after parsing
347 QList<KTextEditor::Range> stripBackslashes;
348
349 // process fields
350 auto fieldMatch = field.globalMatch(subject: templateText);
351 QMap<QString, qsizetype> mainFields;
352 qsizetype nextId = 0;
353
354 while (fieldMatch.hasNext()) {
355 const auto match = fieldMatch.next();
356
357 // collect leading escaped backslashes
358 if (match.hasCaptured(name: slash) && match.capturedLength(name: slash) > 0) {
359 auto slashes = match.captured(name: slash);
360 auto start = startOfMatch(match);
361 int count = std::floor(x: match.capturedLength(name: slash) / 2);
362 stripBackslashes.append(t: {start, start + Cursor{0, count}});
363 }
364
365 // a template field was found, instantiate a field object and populate it
366 auto defaultMatch = defaultField.match(subject: match.captured(nth: 0));
367 const QString contents = match.captured(name: body);
368
369 TemplateField f;
370 f.id = nextId;
371 nextId++;
372 f.range.reset(p: createMovingRangeForMatch(match));
373 f.identifier = contents;
374 f.kind = TemplateField::Editable;
375
376 if (defaultMatch.hasMatch()) {
377 // the field has a default value, i.e. ${foo=3}
378 f.defaultValue = defaultMatch.captured(nth: 1);
379 f.identifier = QStringView(contents).left(n: contents.indexOf(ch: QLatin1Char('='))).trimmed().toString();
380 } else if (f.identifier.contains(c: QLatin1Char('('))) {
381 // field is a function call when it contains an opening parenthesis
382 f.kind = TemplateField::FunctionCall;
383 } else if (f.identifier == QLatin1String("cursor")) {
384 // field marks the final cursor position
385 f.kind = TemplateField::FinalCursorPosition;
386 }
387
388 m_fields.append(t: f);
389 auto &storedField = m_fields.last();
390 auto index = m_fields.count() - 1;
391
392 if (f.kind != TemplateField::FinalCursorPosition && f.kind != TemplateField::FunctionCall) {
393 if (mainFields.contains(key: f.identifier)) {
394 auto &other = m_fields[mainFields[f.identifier]];
395
396 if (defaultMatch.hasMatch() && other.defaultValue.isEmpty()) {
397 other.kind = TemplateField::Mirror;
398 mainFields[storedField.identifier] = index;
399 } else {
400 storedField.kind = TemplateField::Mirror;
401 }
402 } else {
403 mainFields[storedField.identifier] = index;
404 }
405 }
406 }
407
408 for (const auto &match : escapedField.globalMatch(subject: templateText)) {
409 // $ is escaped, not a field; mark the backslash for removal
410 auto start = startOfMatch(match);
411 int count = std::floor(x: match.captured(name: slash).length() / 2) + 1;
412 stripBackslashes.append(t: {start, start + Cursor{0, count}});
413 }
414
415 // remove escape characters
416 // sort the list so the characters are removed starting from the
417 // back and ranges do not move around
418 std::stable_sort(first: stripBackslashes.begin(), last: stripBackslashes.end(), comp: [](const Range l, const Range r) {
419 return l > r;
420 });
421
422 for (const auto &backslash : stripBackslashes) {
423 doc()->removeText(range: backslash);
424 }
425}
426
427void KateTemplateHandler::setupFieldRanges()
428{
429 auto config = m_view->rendererConfig();
430 auto editableAttribute = getAttribute(color: config->templateEditablePlaceholderColor(), alpha: 255);
431 editableAttribute->setDynamicAttribute(type: Attribute::ActivateCaretIn, attribute: getAttribute(color: config->templateFocusedEditablePlaceholderColor(), alpha: 255));
432 auto notEditableAttribute = getAttribute(color: config->templateNotEditablePlaceholderColor(), alpha: 255);
433
434 // color the whole template
435 m_wholeTemplateRange->setAttribute(getAttribute(color: config->templateBackgroundColor(), alpha: 200));
436
437 // color all the template fields
438 for (const auto &field : std::as_const(t&: m_fields)) {
439 field.range->setAttribute(field.kind == TemplateField::Editable ? editableAttribute : notEditableAttribute);
440
441 // initially set all ranges to be static, as required by setupDefaultValues()
442 field.range->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
443 }
444}
445
446void KateTemplateHandler::setupDefaultValues()
447{
448 // Evaluate default values and apply them to the field that defined them:
449 // ${x='foo'}, ${x=func()}, etc.
450
451 KateScript::FieldMap defaultsMap;
452
453 for (auto &field : m_fields) {
454 if (field.kind != TemplateField::Editable) {
455 continue;
456 }
457
458 if (field.defaultValue.isEmpty() && field.kind != TemplateField::FinalCursorPosition) {
459 // field has no default value specified; use its identifier
460 field.defaultValue = field.identifier;
461 } else {
462 // field has a default value; evaluate it with the JS engine
463 //
464 // The default value may only reference other fields that are defined
465 // before the current field.
466 //
467 // Examples: ${a} ${b=a} -> a a
468 // ${a=b} ${b} -> ReferenceError: b is not defined b
469 // ${a=fields.b} ${b} -> undefined b
470
471 // Make sure a field that depends on itself does not cause a reference
472 // error. It uses its own name as value instead.
473 defaultsMap[field.identifier] = field.identifier;
474
475 field.defaultValue = m_templateScript.evaluate(program: field.defaultValue, env: defaultsMap).toString();
476 }
477
478 defaultsMap[field.identifier] = field.defaultValue;
479 }
480
481 // Evaluate function calls and mirror fields, and store the results in
482 // their defaultValue property.
483
484 for (auto &field : m_fields) {
485 if (field.kind == TemplateField::FunctionCall) {
486 field.defaultValue = m_templateScript.evaluate(program: field.identifier, env: defaultsMap).toString();
487 } else if (field.kind == TemplateField::Mirror) {
488 field.defaultValue = defaultsMap[field.identifier].toString();
489 }
490 }
491
492 // group all changes into one undo transaction
493 KTextEditor::Document::EditingTransaction t(doc());
494
495 for (auto &field : m_fields) {
496 // All ranges are static by default, as prepared in setupFieldRanges().
497 // Dynamic behaviors are set in updateRangeBehaviours() after initialization is finished.
498 field.range->setInsertBehaviors(KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
499 doc()->replaceText(range: field.range->toRange(), s: field.defaultValue);
500 field.range->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
501 field.staticRange = field.range->toRange();
502 }
503
504 reorderEmptyAdjacentFields(changedFields: m_fields);
505
506 // initialize static ranges
507 for (auto &field : m_fields) {
508 field.staticRange = field.range->toRange();
509 }
510}
511
512void KateTemplateHandler::initializeTemplate()
513{
514 auto templateString = doc()->text(range: *m_wholeTemplateRange);
515 parseFields(templateText: templateString);
516 setupFieldRanges();
517 setupDefaultValues();
518 updateRangeBehaviours();
519}
520
521const QList<KateTemplateHandler::TemplateField> KateTemplateHandler::fieldsForRange(KTextEditor::Range range, bool compareStaticRanges) const
522{
523 QList<KateTemplateHandler::TemplateField> collected;
524
525 for (const auto &field : m_fields) {
526 if (field.removed) {
527 continue;
528 }
529
530 const auto &fieldRange = (compareStaticRanges ? field.staticRange : field.range->toRange());
531
532 if (range.contains(range: fieldRange) || fieldRange.contains(cursor: range.start()) || fieldRange.contains(cursor: range.end()) || fieldRange.end() == range.start()
533 || fieldRange.end() == range.end()) {
534 collected << field;
535 }
536 }
537
538 return collected;
539}
540
541void KateTemplateHandler::reorderEmptyAdjacentFields(const QList<TemplateField> &changedFields)
542{
543 // This function is an elaborate workaround for adjacent (mirror) fields losing order
544 // when their contents are replaced.
545 //
546 // Consider this:
547 // ${foo}${foo=100}${foo} => 100100100
548 // IDs: 0 1 2
549 //
550 // Field #1 is the only editable field; it is selected when the template
551 // is inserted. Its contents will be replaced as soon as you type:
552 //
553 // 1. contents are deleted: all fields collapse and become empty, sitting at
554 // the same spot. Order: 0, 1, 2
555 // 2. new contents are inserted: fields #1 receives new content and pushes
556 // fields #0 and #2 to the end. Order: 1 (non-empty), 0 (empty), 2 (empty)
557 //
558 // This function resets the original order to be 0, 1, 2 again.
559
560 // find groups of previously empty fields at the same position
561 QList<qsizetype> lastGroup;
562 Cursor lastPosition{Cursor::invalid()};
563
564 QMap<qsizetype, TemplateField *> fieldsLookup;
565 for (auto &field : m_fields) {
566 fieldsLookup.insert(key: field.id, value: &field);
567 }
568
569 auto processGroup = [&lastPosition, &lastGroup, &fieldsLookup]() {
570 auto start = lastPosition;
571
572 for (auto &fieldId : lastGroup) {
573 auto &field = *fieldsLookup[fieldId];
574
575 field.range->setRange(start, end: start + field.range->end() - field.range->start());
576 start = field.range->end();
577 }
578
579 lastGroup.clear();
580 };
581
582 for (qsizetype i = 0; i < changedFields.size(); ++i) {
583 auto &field = changedFields[i];
584
585 if (field.staticRange.isEmpty() && field.staticRange.start() == lastPosition) {
586 lastGroup.push_back(t: field.id);
587 } else {
588 processGroup();
589 lastPosition = field.staticRange.start();
590
591 if (field.staticRange.isEmpty()) {
592 lastGroup.push_back(t: field.id);
593 }
594 }
595 }
596
597 processGroup();
598}
599
600void KateTemplateHandler::updateDependentFields(Document *document, Range range, bool textRemoved)
601{
602 Q_ASSERT(document == doc());
603 Q_UNUSED(document);
604 if (!m_undoManager->isActive()) {
605 // currently undoing stuff; don't update fields
606 return;
607 }
608
609 if (m_internalEdit || range.isEmpty()) {
610 // internal or null edit; for internal edits, don't do anything
611 // to prevent unwanted recursion
612 return;
613 }
614
615 bool in_range = m_wholeTemplateRange->toRange().contains(cursor: range.start());
616 bool at_end = m_wholeTemplateRange->toRange().end() == range.end() || m_wholeTemplateRange->toRange().end() == range.start();
617 if (m_wholeTemplateRange->toRange().isEmpty() || (!in_range && !at_end)) {
618 // edit outside template range, abort
619 ifDebug(qCDebug(LOG_KTE) << "edit outside template range, exiting";) deleteLater();
620 return;
621 }
622
623 ifDebug(qCDebug(LOG_KTE) << "text changed" << document << range;)
624
625 // find the fields which were modified, if any
626 const auto changedFields = fieldsForRange(range, compareStaticRanges: textRemoved);
627
628 if (changedFields.isEmpty()) {
629 ifDebug(qCDebug(LOG_KTE) << "edit did not touch any field:" << range;) return;
630 } else if (changedFields.length() == 1 && changedFields[0].kind == TemplateField::FinalCursorPosition) {
631 // text changed at final cursor position: the user is done, so exit
632 ifDebug(qCDebug(LOG_KTE) << "final cursor changed:" << range;) deleteLater();
633 }
634
635 // group all changes into one undo transaction
636 KTextEditor::Document::EditingTransaction t(doc());
637 m_internalEdit = true; // prevent unwanted recursion
638
639 if (textRemoved) {
640 // If text was removed, mark all affected fields that are now empty as removed
641 for (auto &field : m_fields) {
642 if (field.removed) {
643 continue;
644 }
645
646 if ((range.start() < field.staticRange.start() && range.end() >= field.staticRange.end())
647 || (range.start() <= field.staticRange.start() && range.end().line() > field.staticRange.end().line()) || !field.staticRange.isValid()) {
648 field.removed = true;
649 }
650 }
651 } else {
652 // If no text was removed (i.e. if text was inserted), make sure empty fields
653 // are sorted correctly before continuing.
654 reorderEmptyAdjacentFields(changedFields);
655 }
656
657 // Collect new values of changed editable fields
658 QMap<QString, QString> mainFieldValues;
659 for (const auto &field : changedFields) {
660 if (field.kind == TemplateField::Editable) {
661 mainFieldValues[field.identifier] = QStringLiteral("");
662
663 if (field.range->toRange().isValid()) {
664 mainFieldValues[field.identifier] = doc()->text(range: field.range->toRange());
665 }
666 }
667 }
668
669 // Mark all field ranges as static until we are finished with editing
670 for (const auto &field : m_fields) {
671 field.range->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
672 }
673
674 // - Apply changed main values to mirror fields
675 // - Mark changed main fields as edited
676 // - Re-run all function fields with new values
677 auto jsFields = fieldMap();
678 for (auto &field : m_fields) {
679 Cursor reset = {Cursor::invalid()};
680
681 if (field.range->start() == view()->cursorPosition()) {
682 reset = view()->cursorPosition();
683 }
684
685 if (mainFieldValues.contains(key: field.identifier)) {
686 if (field.kind == TemplateField::Editable && mainFieldValues[field.identifier] != field.defaultValue) {
687 field.touched = true;
688 } else if (field.kind == TemplateField::Mirror) {
689 field.range->setInsertBehaviors(KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
690 doc()->replaceText(range: field.range->toRange(), s: mainFieldValues[field.identifier]);
691 field.range->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
692 }
693 } else if (field.kind == TemplateField::FunctionCall) {
694 const auto &callResult = m_templateScript.evaluate(program: field.identifier, env: jsFields);
695 field.range->setInsertBehaviors(KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
696 doc()->replaceText(range: field.range->toRange(), s: callResult.toString());
697 field.range->setInsertBehaviors(KTextEditor::MovingRange::DoNotExpand);
698 }
699
700 if (reset.isValid()) {
701 view()->setCursorPosition(reset);
702 }
703 }
704
705 // Re-apply dynamic range behaviors now that we are done editing
706 updateRangeBehaviours();
707
708 // Update saved static ranges
709 for (auto &field : m_fields) {
710 field.staticRange = field.range->toRange();
711 }
712
713 m_internalEdit = false; // enable this slot again
714}
715
716void KateTemplateHandler::updateRangeBehaviours()
717{
718 std::stable_sort(first: m_fields.begin(), last: m_fields.end(), comp: [](const auto &l, const auto &r) {
719 return l.range->toRange().start() < r.range->toRange().start();
720 });
721
722 TemplateField *lastField = nullptr;
723
724 for (auto &field : m_fields) {
725 auto last = lastField != nullptr ? *(lastField->range) : Range::invalid();
726
727 if (field.removed) {
728 // field is removed: it should not receive any changes and should not
729 // be considered in relation to other fields
730 field.range->setInsertBehaviors(MovingRange::DoNotExpand);
731 continue;
732 }
733
734 if (field.kind == TemplateField::FinalCursorPosition) {
735 // final cursor position never grows
736 field.range->setInsertBehaviors(MovingRange::DoNotExpand);
737 } else if (field.range->start() <= last.end()) {
738 // ranges are adjacent...
739 if (field.range->isEmpty() && lastField != nullptr) {
740 if (field.kind != TemplateField::Editable && field.kind != TemplateField::FinalCursorPosition
741 && (lastField->kind == TemplateField::Editable || lastField->kind == TemplateField::FinalCursorPosition)) {
742 // ...do not expand the current field to let the previous, more important field expand instead
743 field.range->setInsertBehaviors(MovingRange::DoNotExpand);
744
745 // ...let the previous field expand to the right
746 lastField->range->setInsertBehaviors(lastField->range->insertBehaviors() | MovingRange::ExpandRight);
747 } else {
748 // ...do not expand the previous field to let the empty field expand instead
749 lastField->range->setInsertBehaviors(MovingRange::DoNotExpand);
750
751 // ...expand to both sides to catch new input while the field is empty
752 field.range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
753 }
754 } else if (field.kind == TemplateField::Editable && lastField != nullptr && lastField->kind != TemplateField::Editable) {
755 // ...expand to both sides as the current, editable field is more important than the previous field
756 field.range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
757
758 // ...do not expand the previous field to the right
759 lastField->range->setInsertBehaviors(lastField->range->insertBehaviors() & ~MovingRange::ExpandRight);
760 } else {
761 // ...only expand to the right to prevent overlap
762 field.range->setInsertBehaviors(MovingRange::ExpandRight);
763 }
764 } else {
765 // ranges are not adjacent, can grow in both directions
766 field.range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
767 }
768
769 lastField = &field;
770 }
771}
772
773KateScript::FieldMap KateTemplateHandler::fieldMap() const
774{
775 KateScript::FieldMap map;
776 for (const auto &field : m_fields) {
777 if (field.kind != TemplateField::Editable) {
778 // only editable fields are of interest to the scripts
779 continue;
780 }
781 map.insert(key: field.identifier, value: QJSValue(doc()->text(range: field.range->toRange())));
782 }
783 return map;
784}
785
786KTextEditor::ViewPrivate *KateTemplateHandler::view() const
787{
788 return m_view;
789}
790
791#include "moc_katetemplatehandler.cpp"
792

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