KTextEditor

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

KDE's Doxygen guidelines are available online.