KTextEditor

katetemplatehandler.cpp
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(m_view->selectionRange(), MovingRange::DoNotExpand));
48
49 m_undoManager->setAllowComplexMerge(true);
50
51 {
52 connect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateTemplateHandler::slotTemplateInserted);
54 // insert the raw template string
55 if (!doc()->insertText(position, templateString)) {
57 return;
58 }
59 // now there must be a range, caught by the textInserted slot
60 Q_ASSERT(m_wholeTemplateRange);
61 doc()->align(m_view, *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(selection->toRange());
69
70 const bool have_editable_field = std::any_of(m_fields.constBegin(), m_fields.constEnd(), [](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(1, true);
82
83 connect(doc(), &KTextEditor::Document::viewCreated, this, &KateTemplateHandler::slotViewCreated);
84 connect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateTemplateHandler::updateDependentFields);
85 connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &KateTemplateHandler::updateDependentFields);
87
88 } else {
89 // when no interesting ranges got added, we can terminate directly
90 jumpToFinalCursorPosition();
92 }
93}
94
95KateTemplateHandler::~KateTemplateHandler()
96{
97 m_undoManager->setAllowComplexMerge(false);
98}
99
100void KateTemplateHandler::sortFields()
101{
102 std::sort(m_fields.begin(), m_fields.end(), [](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(+1);
118}
119
120void KateTemplateHandler::jumpToPreviousRange()
121{
122 jump(-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(fieldForRange(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(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(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(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(doc()->newMovingRange(range, MovingRange::ExpandLeft | MovingRange::ExpandRight));
185
186 disconnect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &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(this);
204}
205
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(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(0);
269 const auto left = QStringView(templateText).left(offset);
270 const auto nl = QLatin1Char('\n');
271 const auto rel_lineno = left.count(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(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({matchStart, matchStart + Cursor(0, match.capturedLength(0))}, 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(templateText);
285 while (fieldMatch.hasNext()) {
286 const auto match = fieldMatch.next();
287 if (match.captured(0).startsWith(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(startOfMatch(match));
292 continue;
293 }
294 // a template field was found, instantiate a field object and populate it
295 auto defaultMatch = defaultField.match(match.captured(0));
296 const QString contents = match.captured(1);
297 TemplateField f;
298 f.range.reset(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(1);
304 f.identifier = QStringView(contents).left(contents.indexOf(QLatin1Char('='))).trimmed().toString();
305 } else if (f.identifier.contains(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(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(f);
320 }
321
322 // remove escape characters
323 for (const auto &backslash : stripBackslashes) {
324 doc()->removeText(KTextEditor::Range(backslash, backslash + Cursor(0, 1)));
325 }
326}
327
328void KateTemplateHandler::setupFieldRanges()
329{
330 auto config = m_view->rendererConfig();
331 auto editableAttribute = getAttribute(config->templateEditablePlaceholderColor(), 255);
332 editableAttribute->setDynamicAttribute(Attribute::ActivateCaretIn, getAttribute(config->templateFocusedEditablePlaceholderColor(), 255));
333 auto notEditableAttribute = getAttribute(config->templateNotEditablePlaceholderColor(), 255);
334
335 // color the whole template
336 m_wholeTemplateRange->setAttribute(getAttribute(config->templateBackgroundColor(), 200));
337
338 // color all the template fields
339 for (const auto &field : std::as_const(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(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(field.defaultValue).toString();
357 }
358 doc()->replaceText(field.range->toRange(), value);
359 }
360}
361
362void KateTemplateHandler::initializeTemplate()
363{
364 auto templateString = doc()->text(*m_wholeTemplateRange);
365 parseFields(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(doc(), 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(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(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
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(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(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(field->range->toRange(), 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(field->range->toRange(), 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(field->identifier, map);
474 doc()->replaceText(field->range->toRange(), 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{
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(field.identifier, QJSValue(doc()->text(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"
A class which provides customized text decorations.
Definition attribute.h:51
@ ActivateCaretIn
Activate attribute on caret in.
Definition attribute.h:248
The Cursor represents a position in a Document.
Definition cursor.h:75
Backend of KTextEditor::Document related public KTextEditor interfaces.
KTextEditor::MovingRange * newMovingRange(KTextEditor::Range range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors=KTextEditor::MovingRange::DoNotExpand, KTextEditor::MovingRange::EmptyBehavior emptyBehavior=KTextEditor::MovingRange::AllowEmpty) override
Create a new moving range for this document.
QString text(KTextEditor::Range range, bool blockwise=false) const override
Get the document content within the given range.
void textRemoved(KTextEditor::Document *document, KTextEditor::Range range, const QString &oldText)
The document emits this signal whenever range was removed, i.e.
void textInsertedRange(KTextEditor::Document *document, KTextEditor::Range range)
The document emits this signal whenever text was inserted.
QList< KTextEditor::View * > views() const override
Returns the views pre-casted to KTextEditor::Views.
Editing transaction support.
Definition document.h:574
void viewCreated(KTextEditor::Document *document, KTextEditor::View *view)
This signal is emitted whenever the document creates a new view.
void aboutToReload(KTextEditor::Document *document)
Warn anyone listening that the current document is about to reload.
@ DoNotExpand
Don't expand to encapsulate new characters in either direction. This is the default.
@ ExpandRight
Expand to encapsulate new characters to the right of the range.
@ ExpandLeft
Expand to encapsulate new characters to the left of the range.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
KateScript objects represent a script that can be executed and inspected.
Definition katescript.h:107
bool setView(KTextEditor::ViewPrivate *view)
set view for this script for the execution will trigger load!
QJSValue evaluate(const QString &program, const FieldMap &env=FieldMap())
Execute a piece of code.
Inserts a template and offers advanced snippet features, like navigation and mirroring.
bool eventFilter(QObject *object, QEvent *event) override
Provide keyboard interaction for the template handler.
KateTemplateHandler(KTextEditor::ViewPrivate *view, KTextEditor::Cursor position, const QString &templateString, const QString &script, KateUndoManager *undoManager)
Setup the template handler, insert the template string.
KateUndoManager implements a document's history.
void setAllowComplexMerge(bool allow)
Allows or disallows merging of "complex" undo groups.
Q_SCRIPTABLE Q_NOREPLY void start()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
const QList< QKeySequence > & end()
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
void setAlpha(int alpha)
QString toString() const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator end()
qsizetype indexOf(const AT &value, qsizetype from) const const
void prepend(parameter_type value)
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QStringView left(qsizetype length) const const
QString toString() const const
QStringView trimmed() const const
AltModifier
QTextStream & left(QTextStream &stream)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 4 2024 12:03:01 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.