KTextEditor

kateautoindent.cpp
1 /*
2  This file is part of the KDE libraries
3  SPDX-FileCopyrightText: 2003 Jesse Yurkovich <[email protected]>
4 
5  KateVarIndent class:
6  SPDX-FileCopyrightText: 2004 Anders Lund <[email protected]>
7 
8  Basic support for config page:
9  SPDX-FileCopyrightText: 2005 Dominik Haumann <[email protected]>
10 
11  SPDX-License-Identifier: LGPL-2.0-only
12 */
13 
14 #include "kateautoindent.h"
15 
16 #include "katedocument.h"
17 #include "kateextendedattribute.h"
18 #include "kateglobal.h"
19 #include "katehighlight.h"
20 #include "kateindentscript.h"
21 #include "katepartdebug.h"
22 #include "katescriptmanager.h"
23 #include "kateview.h"
24 
25 #include <KLocalizedString>
26 
27 #include <cctype>
28 
29 namespace
30 {
31 inline const QString MODE_NONE()
32 {
33  return QStringLiteral("none");
34 }
35 inline const QString MODE_NORMAL()
36 {
37  return QStringLiteral("normal");
38 }
39 }
40 
41 // BEGIN KateAutoIndent
42 
44 {
45  QStringList l;
46  l.reserve(modeCount());
47  for (int i = 0; i < modeCount(); ++i) {
48  l << modeDescription(i);
49  }
50 
51  return l;
52 }
53 
55 {
56  QStringList l;
57  l.reserve(modeCount());
58  for (int i = 0; i < modeCount(); ++i) {
59  l << modeName(i);
60  }
61 
62  return l;
63 }
64 
66 {
67  // inbuild modes + scripts
68  return 2 + KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptCount();
69 }
70 
72 {
73  if (mode == 0 || mode >= modeCount()) {
74  return MODE_NONE();
75  }
76 
77  if (mode == 1) {
78  return MODE_NORMAL();
79  }
80 
81  return KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(mode - 2)->indentHeader().baseName();
82 }
83 
85 {
86  if (mode == 0 || mode >= modeCount()) {
87  return i18nc("Autoindent mode", "None");
88  }
89 
90  if (mode == 1) {
91  return i18nc("Autoindent mode", "Normal");
92  }
93 
94  const QString &name = KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(mode - 2)->indentHeader().name();
95  return i18nc("Autoindent mode", name.toUtf8().constData());
96 }
97 
99 {
100  if (mode == 0 || mode == 1 || mode >= modeCount()) {
101  return QString();
102  }
103 
104  return KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(mode - 2)->indentHeader().requiredStyle();
105 }
106 
108 {
109  for (int i = 0; i < modeCount(); ++i)
110  if (modeName(i) == name) {
111  return i;
112  }
113 
114  return 0;
115 }
116 
117 KateAutoIndent::KateAutoIndent(KTextEditor::DocumentPrivate *_doc)
118  : QObject(_doc)
119  , doc(_doc)
120  , m_script(nullptr)
121 {
122  // don't call updateConfig() here, document might is not ready for that....
123 
124  // on script reload, the script pointer is invalid -> force reload
125  connect(KTextEditor::EditorPrivate::self()->scriptManager(), &KateScriptManager::reloaded, this, &KateAutoIndent::reloadScript);
126 }
127 
129 
130 QString KateAutoIndent::tabString(int length, int align) const
131 {
132  QString s;
133  length = qMin(length, 256); // sanity check for large values of pos
134  int spaces = qBound(0, align - length, 256);
135 
136  if (!useSpaces) {
137  s.append(QString(length / tabWidth, QLatin1Char('\t')));
138  length = length % tabWidth;
139  }
140  s.append(QString(length + spaces, QLatin1Char(' ')));
141 
142  return s;
143 }
144 
145 bool KateAutoIndent::doIndent(int line, int indentDepth, int align)
146 {
147  Kate::TextLine textline = doc->plainKateTextLine(line);
148 
149  // textline not found, cu
150  if (!textline) {
151  return false;
152  }
153 
154  // sanity check
155  if (indentDepth < 0) {
156  indentDepth = 0;
157  }
158 
159  const QString oldIndentation = textline->leadingWhitespace();
160 
161  // Preserve existing "tabs then spaces" alignment if and only if:
162  // - no alignment was passed to doIndent and
163  // - we aren't using spaces for indentation and
164  // - we aren't rounding indentation up to the next multiple of the indentation width and
165  // - we aren't using a combination to tabs and spaces for alignment, or in other words
166  // the indent width is a multiple of the tab width.
167  bool preserveAlignment = !useSpaces && keepExtra && indentWidth % tabWidth == 0;
168  if (align == 0 && preserveAlignment) {
169  // Count the number of consecutive spaces at the end of the existing indentation
170  int i = oldIndentation.size() - 1;
171  while (i >= 0 && oldIndentation.at(i) == QLatin1Char(' ')) {
172  --i;
173  }
174  // Use the passed indentDepth as the alignment, and set the indentDepth to
175  // that value minus the number of spaces found (but don't let it get negative).
176  align = indentDepth;
177  indentDepth = qMax(0, align - (oldIndentation.size() - 1 - i));
178  }
179 
180  QString indentString = tabString(indentDepth, align);
181 
182  // Modify the document *ONLY* if smth has really changed!
183  if (oldIndentation != indentString) {
184  // remove leading whitespace, then insert the leading indentation
185  doc->editStart();
186  doc->editRemoveText(line, 0, oldIndentation.length());
187  doc->editInsertText(line, 0, indentString);
188  doc->editEnd();
189  }
190 
191  return true;
192 }
193 
194 bool KateAutoIndent::doIndentRelative(int line, int change)
195 {
196  Kate::TextLine textline = doc->plainKateTextLine(line);
197 
198  // get indent width of current line
199  int indentDepth = textline->indentDepth(tabWidth);
200  int extraSpaces = indentDepth % indentWidth;
201 
202  // add change
203  indentDepth += change;
204 
205  // if keepExtra is off, snap to a multiple of the indentWidth
206  if (!keepExtra && extraSpaces > 0) {
207  if (change < 0) {
208  indentDepth += indentWidth - extraSpaces;
209  } else {
210  indentDepth -= extraSpaces;
211  }
212  }
213 
214  // do indent
215  return doIndent(line, indentDepth);
216 }
217 
218 void KateAutoIndent::keepIndent(int line)
219 {
220  // no line in front, no work...
221  if (line <= 0) {
222  return;
223  }
224 
225  // keep indentation: find line with content
226  int nonEmptyLine = line - 1;
227  while (nonEmptyLine >= 0) {
228  if (doc->lineLength(nonEmptyLine) > 0) {
229  break;
230  }
231  --nonEmptyLine;
232  }
233  Kate::TextLine prevTextLine = doc->plainKateTextLine(nonEmptyLine);
234  Kate::TextLine textLine = doc->plainKateTextLine(line);
235 
236  // textline not found, cu
237  if (!prevTextLine || !textLine) {
238  return;
239  }
240 
241  const QString previousWhitespace = prevTextLine->leadingWhitespace();
242 
243  // remove leading whitespace, then insert the leading indentation
244  doc->editStart();
245 
246  if (!keepExtra) {
247  const QString currentWhitespace = textLine->leadingWhitespace();
248  doc->editRemoveText(line, 0, currentWhitespace.length());
249  }
250 
251  doc->editInsertText(line, 0, previousWhitespace);
252  doc->editEnd();
253 }
254 
255 void KateAutoIndent::reloadScript()
256 {
257  // small trick to force reload
258  m_script = nullptr; // prevent dangling pointer
259  QString currentMode = m_mode;
260  m_mode = QString();
261  setMode(currentMode);
262 }
263 
264 void KateAutoIndent::scriptIndent(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &position, QChar typedChar)
265 {
266  // start edit
267  doc->pushEditState();
268  doc->editStart();
269 
270  QPair<int, int> result = m_script->indent(view, position, typedChar, indentWidth);
271  int newIndentInChars = result.first;
272 
273  // handle negative values special
274  if (newIndentInChars < -1) {
275  // do nothing atm
276  }
277 
278  // reuse indentation of the previous line, just like the "normal" indenter
279  else if (newIndentInChars == -1) {
280  // keep indent of previous line
281  keepIndent(position.line());
282  }
283 
284  // get align
285  else {
286  // we got a positive or zero indent to use...
287  doIndent(position.line(), newIndentInChars, result.second);
288  }
289 
290  // end edit in all cases
291  doc->editEnd();
292  doc->popEditState();
293 }
294 
295 bool KateAutoIndent::isStyleProvided(const KateIndentScript *script, const KateHighlighting *highlight)
296 {
297  QString requiredStyle = script->indentHeader().requiredStyle();
298  return (requiredStyle.isEmpty() || requiredStyle == highlight->style());
299 }
300 
302 {
303  // bail out, already set correct mode...
304  if (m_mode == name) {
305  return;
306  }
307 
308  // cleanup
309  m_script = nullptr;
310 
311  // first, catch easy stuff... normal mode and none, easy...
312  if (name.isEmpty() || name == MODE_NONE()) {
313  m_mode = MODE_NONE();
314  return;
315  }
316 
317  if (name == MODE_NORMAL()) {
318  m_mode = MODE_NORMAL();
319  return;
320  }
321 
322  // handle script indenters, if any for this name...
323  KateIndentScript *script = KTextEditor::EditorPrivate::self()->scriptManager()->indentationScript(name);
324  if (script) {
325  if (isStyleProvided(script, doc->highlight())) {
326  m_script = script;
327  m_mode = name;
328  return;
329  } else {
330  qCWarning(LOG_KTE) << "mode" << name << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style"
331  << doc->highlight()->style() << "but script requires" << script->indentHeader().requiredStyle();
332  }
333  } else {
334  qCWarning(LOG_KTE) << "mode" << name << "does not exist";
335  }
336 
337  // Fall back to normal
338  m_mode = MODE_NORMAL();
339 }
340 
342 {
343  if (m_script) {
344  if (!isStyleProvided(m_script, doc->highlight())) {
345  qCDebug(LOG_KTE) << "mode" << m_mode << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style"
346  << doc->highlight()->style() << "but script requires" << m_script->indentHeader().requiredStyle();
347  doc->config()->setIndentationMode(MODE_NORMAL());
348  }
349  }
350 }
351 
353 {
354  KateDocumentConfig *config = doc->config();
355 
356  useSpaces = config->replaceTabsDyn();
357  keepExtra = config->keepExtraSpaces();
358  tabWidth = config->tabWidth();
359  indentWidth = config->indentationWidth();
360 }
361 
362 bool KateAutoIndent::changeIndent(const KTextEditor::Range &range, int change)
363 {
364  std::vector<int> skippedLines;
365 
366  // loop over all lines given...
367  for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(range.end().line(), doc->lines() - 1); ++line) {
368  // don't indent empty lines
369  if (doc->line(line).isEmpty()) {
370  skippedLines.push_back(line);
371  continue;
372  }
373  // don't indent the last line when the cursor is on the first column
374  if (line == range.end().line() && range.end().column() == 0) {
375  skippedLines.push_back(line);
376  continue;
377  }
378 
379  doIndentRelative(line, change * indentWidth);
380  }
381 
382  if (static_cast<int>(skippedLines.size()) > range.numberOfLines()) {
383  // all lines were empty, so indent them nevertheless
384  for (int line : skippedLines) {
385  doIndentRelative(line, change * indentWidth);
386  }
387  }
388 
389  return true;
390 }
391 
392 void KateAutoIndent::indent(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range)
393 {
394  // no script, do nothing...
395  if (!m_script) {
396  return;
397  }
398 
399  // we want one undo action >= START
400  doc->setUndoMergeAllEdits(true);
401 
402  // loop over all lines given...
403  for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(range.end().line(), doc->lines() - 1); ++line) {
404  // let the script indent for us...
405  scriptIndent(view, KTextEditor::Cursor(line, 0), QChar());
406  }
407 
408  // we want one undo action => END
409  doc->setUndoMergeAllEdits(false);
410 }
411 
412 void KateAutoIndent::userTypedChar(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &position, QChar typedChar)
413 {
414  // normal mode
415  if (m_mode == MODE_NORMAL()) {
416  // only indent on new line, per default
417  if (typedChar != QLatin1Char('\n')) {
418  return;
419  }
420 
421  // keep indent of previous line
422  keepIndent(position.line());
423  return;
424  }
425 
426  // no script, do nothing...
427  if (!m_script) {
428  return;
429  }
430 
431  // does the script allow this char as trigger?
432  if (typedChar != QLatin1Char('\n') && !m_script->triggerCharacters().contains(typedChar)) {
433  return;
434  }
435 
436  // let the script indent for us...
437  scriptIndent(view, position, typedChar);
438 }
439 // END KateAutoIndent
440 
441 // BEGIN KateViewIndentAction
442 KateViewIndentationAction::KateViewIndentationAction(KTextEditor::DocumentPrivate *_doc, const QString &text, QObject *parent)
443  : KActionMenu(text, parent)
444  , doc(_doc)
445 {
446  setPopupMode(QToolButton::InstantPopup);
447  connect(menu(), &QMenu::aboutToShow, this, &KateViewIndentationAction::slotAboutToShow);
448  actionGroup = new QActionGroup(menu());
449 }
450 
451 void KateViewIndentationAction::slotAboutToShow()
452 {
453  const QStringList modes = KateAutoIndent::listModes();
454 
455  menu()->clear();
456  const auto actions = actionGroup->actions();
457  for (QAction *action : actions) {
458  actionGroup->removeAction(action);
459  }
460  for (int z = 0; z < modes.size(); ++z) {
461  QAction *action = menu()->addAction(QLatin1Char('&') + KateAutoIndent::modeDescription(z).replace(QLatin1Char('&'), QLatin1String("&&")));
462  actionGroup->addAction(action);
463  action->setCheckable(true);
464  action->setData(z);
465 
466  QString requiredStyle = KateAutoIndent::modeRequiredStyle(z);
467  action->setEnabled(requiredStyle.isEmpty() || requiredStyle == doc->highlight()->style());
468 
469  if (doc->config()->indentationMode() == KateAutoIndent::modeName(z)) {
470  action->setChecked(true);
471  }
472  }
473 
474  disconnect(menu(), &QMenu::triggered, this, &KateViewIndentationAction::setMode);
475  connect(menu(), &QMenu::triggered, this, &KateViewIndentationAction::setMode);
476 }
477 
478 void KateViewIndentationAction::setMode(QAction *action)
479 {
480  // set new mode
481  doc->config()->setIndentationMode(KateAutoIndent::modeName(action->data().toInt()));
482  doc->rememberUserDidSetIndentationMode();
483 }
484 // END KateViewIndentationAction
QString & append(QChar ch)
static QString modeDescription(int mode)
Return the mode description.
void updateConfig()
Update indenter&#39;s configuration (indention width, etc.) Is called in the updateConfig() of the docume...
void reserve(int alloc)
constexpr int numberOfLines() const Q_DECL_NOEXCEPT
Returns the number of lines separating the start() and end() positions.
void setChecked(bool)
QVariant data() const const
void setMode(const QString &name)
Switch indenter Nop if already set to given mode Otherwise switch to given indenter or to "None" if n...
int size() const const
QPair< int, int > indent(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &position, QChar typedCharacter, int indentWidth)
Returns a pair where the first value is the indent amount, and the second value is the alignment...
void triggered(QAction *action)
void indent(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range)
The document requests the indenter to indent the given range of existing text.
static QString modeRequiredStyle(int mode)
Return the syntax highlighting style required to use this mode.
static QStringList listIdentifiers()
List all possible names, i.e.
The Cursor represents a position in a Document.
Definition: cursor.h:71
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
int size() const const
void aboutToShow()
int toInt(bool *ok) const const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
bool isEmpty() const const
const char * constData() const const
constexpr int column() const Q_DECL_NOEXCEPT
Retrieve the column on which this cursor is situated.
Definition: cursor.h:213
static uint modeNumber(const QString &name)
Maps name -> index.
constexpr Cursor start() const Q_DECL_NOEXCEPT
Get the start position of this range.
void userTypedChar(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &position, QChar typedChar)
The user typed some char, the indenter can react on this &#39; &#39; will be send as char if the user wraps a...
A specialized class for scripts that are of type ScriptType::Indentation.
An object representing a section of text, from one Cursor to another.
KateAutoIndent(KTextEditor::DocumentPrivate *doc)
Constructor, creates dummy indenter "None".
void setData(const QVariant &userData)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
static int modeCount()
count of modes
bool changeIndent(const KTextEditor::Range &range, int change)
Function to provide the common indent/unindent/clean indent functionality to the document This should...
void setCheckable(bool)
const QString & modeName() const
mode name
constexpr Cursor end() const Q_DECL_NOEXCEPT
Get the end position of this range.
const QChar at(int position) const const
KateScriptManager * scriptManager()
Global script collection.
Definition: kateglobal.h:275
constexpr int line() const Q_DECL_NOEXCEPT
Retrieve the line on which this cursor is situated.
Definition: cursor.h:195
int length() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
static KTextEditor::EditorPrivate * self()
Kate Part Internal stuff ;)
Definition: kateglobal.cpp:349
QObject * parent() const const
void reloaded()
this signal is emitted when all scripts are deleted and reloaded again.
~KateAutoIndent()
Destructor.
void setEnabled(bool)
static QStringList listModes()
List all possible modes by name, i.e.
void checkRequiredStyle()
Check if the current highlighting mode provides the style required by the current indenter...
QByteArray toUtf8() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Thu Jun 17 2021 22:57:20 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.