KTextEditor

katetemplatehandler.cpp
1 /*
2  SPDX-FileCopyrightText: 2004, 2010 Joseph Wenninger <[email protected]>
3  SPDX-FileCopyrightText: 2009 Milian Wolff <[email protected]>
4  SPDX-FileCopyrightText: 2014 Sven Brauch <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include <QQueue>
10 #include <QRegularExpression>
11 
12 #include <ktexteditor/movingcursor.h>
13 #include <ktexteditor/movingrange.h>
14 
15 #include "kateconfig.h"
16 #include "katedocument.h"
17 #include "kateglobal.h"
18 #include "katepartdebug.h"
19 #include "kateregexpsearch.h"
20 #include "katerenderer.h"
21 #include "katetemplatehandler.h"
22 #include "kateundomanager.h"
23 #include "kateview.h"
24 #include "script/katescriptmanager.h"
25 
26 using namespace KTextEditor;
27 
28 #define ifDebug(x)
29 
30 KateTemplateHandler::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)) {
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(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();
91  deleteLater();
92  }
93 }
94 
95 KateTemplateHandler::~KateTemplateHandler()
96 {
97  m_undoManager->setAllowComplexMerge(false);
98 }
99 
100 void 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 
115 void KateTemplateHandler::jumpToNextRange()
116 {
117  jump(+1);
118 }
119 
120 void KateTemplateHandler::jumpToPreviousRange()
121 {
122  jump(-1);
123 }
124 
125 void 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 
171 void 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 
182 void 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 
189 KTextEditor::DocumentPrivate *KateTemplateHandler::doc() const
190 {
191  return m_view->doc();
192 }
193 
194 void KateTemplateHandler::slotViewCreated(Document *document, View *view)
195 {
196  Q_ASSERT(document == doc());
197  Q_UNUSED(document)
198  setupEventHandler(view);
199 }
200 
201 void 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  */
250 Attribute::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 
258 void 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  QVector<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 
328 void KateTemplateHandler::setupFieldRanges()
329 {
330  auto config = m_view->renderer()->config();
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 
344 void 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 
362 void 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 
378 const 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 
391 void 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 
481 void 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 
502 KateScript::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 
515 KTextEditor::ViewPrivate *KateTemplateHandler::view() const
516 {
517  return m_view;
518 }
void prepend(T &&value)
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
QVector::iterator begin()
bool setView(KTextEditor::ViewPrivate *view)
set view for this script for the execution will trigger load!
Definition: katescript.cpp:224
QTextStream & left(QTextStream &stream)
void append(const T &value)
Q_SCRIPTABLE Q_NOREPLY void start()
int indexOf(const T &value, int from) const const
bool eventFilter(QObject *object, QEvent *event) override
Provide keyboard interaction for the template handler.
QVector::const_iterator constEnd() const const
An object representing a section of text, from one Cursor to another.
QStringView left(qsizetype length) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
virtual bool eventFilter(QObject *watched, QEvent *event)
QString toString() const const
void deleteLater()
virtual bool event(QEvent *e)
The Cursor represents a position in a Document.
Definition: cursor.h:71
void setAllowComplexMerge(bool allow)
Allows or disallows merging of "complex" undo groups.
const T & at(int i) const const
void installEventFilter(QObject *filterObj)
QJSValue evaluate(const QString &program, const FieldMap &env=FieldMap())
Execute a piece of code.
Definition: katescript.cpp:184
KateUndoManager implements a document's history.
void viewCreated(KTextEditor::Document *document, KTextEditor::View *view)
This signal is emitted whenever the document creates a new view.
void setAlpha(int alpha)
QStringView trimmed() const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
KSharedConfigPtr config()
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
Definition: katetextblock.h:22
Inserts a template and offers advanced snippet features, like navigation and mirroring.
KateTemplateHandler(KTextEditor::ViewPrivate *view, KTextEditor::Cursor position, const QString &templateString, const QString &script, KateUndoManager *undoManager)
Setup the template handler, insert the template string.
QVector::iterator end()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
Editing transaction support.
Definition: document.h:483
int size() const const
void aboutToReload(KTextEditor::Document *document)
Warn anyone listening that the current document is about to reload.
QVector::const_iterator constBegin() const const
AltModifier
QFuture< void > map(Sequence &sequence, MapFunctor function)
const QList< QKeySequence > & end()
QString toString() const const
KateScript objects represent a script that can be executed and inspected.
Definition: katescript.h:106
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 03:50:22 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.