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 
47  for (int i = 0; i < modeCount(); ++i) {
48  l << modeDescription(i);
49  }
50 
51  return l;
52 }
53 
55 {
56  QStringList l;
57 
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(), SIGNAL(reloaded()), this, SLOT(reloadScript()));
126 }
127 
129 {
130 }
131 
132 QString KateAutoIndent::tabString(int length, int align) const
133 {
134  QString s;
135  length = qMin(length, 256); // sanity check for large values of pos
136  int spaces = qBound(0, align - length, 256);
137 
138  if (!useSpaces) {
139  s.append(QString(length / tabWidth, QLatin1Char('\t')));
140  length = length % tabWidth;
141  }
142  s.append(QString(length + spaces, QLatin1Char(' ')));
143 
144  return s;
145 }
146 
147 bool KateAutoIndent::doIndent(int line, int indentDepth, int align)
148 {
149  Kate::TextLine textline = doc->plainKateTextLine(line);
150 
151  // textline not found, cu
152  if (!textline) {
153  return false;
154  }
155 
156  // sanity check
157  if (indentDepth < 0) {
158  indentDepth = 0;
159  }
160 
161  const QString oldIndentation = textline->leadingWhitespace();
162 
163  // Preserve existing "tabs then spaces" alignment if and only if:
164  // - no alignment was passed to doIndent and
165  // - we aren't using spaces for indentation and
166  // - we aren't rounding indentation up to the next multiple of the indentation width and
167  // - we aren't using a combination to tabs and spaces for alignment, or in other words
168  // the indent width is a multiple of the tab width.
169  bool preserveAlignment = !useSpaces && keepExtra && indentWidth % tabWidth == 0;
170  if (align == 0 && preserveAlignment) {
171  // Count the number of consecutive spaces at the end of the existing indentation
172  int i = oldIndentation.size() - 1;
173  while (i >= 0 && oldIndentation.at(i) == QLatin1Char(' ')) {
174  --i;
175  }
176  // Use the passed indentDepth as the alignment, and set the indentDepth to
177  // that value minus the number of spaces found (but don't let it get negative).
178  align = indentDepth;
179  indentDepth = qMax(0, align - (oldIndentation.size() - 1 - i));
180  }
181 
182  QString indentString = tabString(indentDepth, align);
183 
184  // Modify the document *ONLY* if smth has really changed!
185  if (oldIndentation != indentString) {
186  // remove leading whitespace, then insert the leading indentation
187  doc->editStart();
188  doc->editRemoveText(line, 0, oldIndentation.length());
189  doc->editInsertText(line, 0, indentString);
190  doc->editEnd();
191  }
192 
193  return true;
194 }
195 
196 bool KateAutoIndent::doIndentRelative(int line, int change)
197 {
198  Kate::TextLine textline = doc->plainKateTextLine(line);
199 
200  // get indent width of current line
201  int indentDepth = textline->indentDepth(tabWidth);
202  int extraSpaces = indentDepth % indentWidth;
203 
204  // add change
205  indentDepth += change;
206 
207  // if keepExtra is off, snap to a multiple of the indentWidth
208  if (!keepExtra && extraSpaces > 0) {
209  if (change < 0) {
210  indentDepth += indentWidth - extraSpaces;
211  } else {
212  indentDepth -= extraSpaces;
213  }
214  }
215 
216  // do indent
217  return doIndent(line, indentDepth);
218 }
219 
220 void KateAutoIndent::keepIndent(int line)
221 {
222  // no line in front, no work...
223  if (line <= 0) {
224  return;
225  }
226 
227  // keep indentation: find line with content
228  int nonEmptyLine = line - 1;
229  while (nonEmptyLine >= 0) {
230  if (doc->lineLength(nonEmptyLine) > 0) {
231  break;
232  }
233  --nonEmptyLine;
234  }
235  Kate::TextLine prevTextLine = doc->plainKateTextLine(nonEmptyLine);
236  Kate::TextLine textLine = doc->plainKateTextLine(line);
237 
238  // textline not found, cu
239  if (!prevTextLine || !textLine) {
240  return;
241  }
242 
243  const QString previousWhitespace = prevTextLine->leadingWhitespace();
244 
245  // remove leading whitespace, then insert the leading indentation
246  doc->editStart();
247 
248  if (!keepExtra) {
249  const QString currentWhitespace = textLine->leadingWhitespace();
250  doc->editRemoveText(line, 0, currentWhitespace.length());
251  }
252 
253  doc->editInsertText(line, 0, previousWhitespace);
254  doc->editEnd();
255 }
256 
257 void KateAutoIndent::reloadScript()
258 {
259  // small trick to force reload
260  m_script = nullptr; // prevent dangling pointer
261  QString currentMode = m_mode;
262  m_mode = QString();
263  setMode(currentMode);
264 }
265 
266 void KateAutoIndent::scriptIndent(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &position, QChar typedChar)
267 {
268  // start edit
269  doc->pushEditState();
270  doc->editStart();
271 
272  QPair<int, int> result = m_script->indent(view, position, typedChar, indentWidth);
273  int newIndentInChars = result.first;
274 
275  // handle negative values special
276  if (newIndentInChars < -1) {
277  // do nothing atm
278  }
279 
280  // reuse indentation of the previous line, just like the "normal" indenter
281  else if (newIndentInChars == -1) {
282  // keep indent of previous line
283  keepIndent(position.line());
284  }
285 
286  // get align
287  else {
288  // we got a positive or zero indent to use...
289  doIndent(position.line(), newIndentInChars, result.second);
290  }
291 
292  // end edit in all cases
293  doc->editEnd();
294  doc->popEditState();
295 }
296 
297 bool KateAutoIndent::isStyleProvided(const KateIndentScript *script, const KateHighlighting *highlight)
298 {
299  QString requiredStyle = script->indentHeader().requiredStyle();
300  return (requiredStyle.isEmpty() || requiredStyle == highlight->style());
301 }
302 
304 {
305  // bail out, already set correct mode...
306  if (m_mode == name) {
307  return;
308  }
309 
310  // cleanup
311  m_script = nullptr;
312 
313  // first, catch easy stuff... normal mode and none, easy...
314  if (name.isEmpty() || name == MODE_NONE()) {
315  m_mode = MODE_NONE();
316  return;
317  }
318 
319  if (name == MODE_NORMAL()) {
320  m_mode = MODE_NORMAL();
321  return;
322  }
323 
324  // handle script indenters, if any for this name...
325  KateIndentScript *script = KTextEditor::EditorPrivate::self()->scriptManager()->indentationScript(name);
326  if (script) {
327  if (isStyleProvided(script, doc->highlight())) {
328  m_script = script;
329  m_mode = name;
330  return;
331  } else {
332  qCWarning(LOG_KTE) << "mode" << name << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style" << doc->highlight()->style() << "but script requires"
333  << script->indentHeader().requiredStyle();
334  }
335  } else {
336  qCWarning(LOG_KTE) << "mode" << name << "does not exist";
337  }
338 
339  // Fall back to normal
340  m_mode = MODE_NORMAL();
341 }
342 
344 {
345  if (m_script) {
346  if (!isStyleProvided(m_script, doc->highlight())) {
347  qCDebug(LOG_KTE) << "mode" << m_mode << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style" << doc->highlight()->style() << "but script requires"
348  << m_script->indentHeader().requiredStyle();
349  doc->config()->setIndentationMode(MODE_NORMAL());
350  }
351  }
352 }
353 
355 {
356  KateDocumentConfig *config = doc->config();
357 
358  useSpaces = config->replaceTabsDyn();
359  keepExtra = config->keepExtraSpaces();
360  tabWidth = config->tabWidth();
361  indentWidth = config->indentationWidth();
362 }
363 
364 bool KateAutoIndent::changeIndent(const KTextEditor::Range &range, int change)
365 {
366  std::vector<int> skippedLines;
367 
368  // loop over all lines given...
369  for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(range.end().line(), doc->lines() - 1); ++line) {
370  // don't indent empty lines
371  if (doc->line(line).isEmpty()) {
372  skippedLines.push_back(line);
373  continue;
374  }
375  // don't indent the last line when the cursor is on the first column
376  if (line == range.end().line() && range.end().column() == 0) {
377  skippedLines.push_back(line);
378  continue;
379  }
380 
381  doIndentRelative(line, change * indentWidth);
382  }
383 
384  if (static_cast<int>(skippedLines.size()) > range.numberOfLines()) {
385  // all lines were empty, so indent them nevertheless
386  for (int line : skippedLines) {
387  doIndentRelative(line, change * indentWidth);
388  }
389  }
390 
391  return true;
392 }
393 
394 void KateAutoIndent::indent(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range)
395 {
396  // no script, do nothing...
397  if (!m_script) {
398  return;
399  }
400 
401  // we want one undo action >= START
402  doc->setUndoMergeAllEdits(true);
403 
404  // loop over all lines given...
405  for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(range.end().line(), doc->lines() - 1); ++line) {
406  // let the script indent for us...
407  scriptIndent(view, KTextEditor::Cursor(line, 0), QChar());
408  }
409 
410  // we want one undo action => END
411  doc->setUndoMergeAllEdits(false);
412 }
413 
414 void KateAutoIndent::userTypedChar(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &position, QChar typedChar)
415 {
416  // normal mode
417  if (m_mode == MODE_NORMAL()) {
418  // only indent on new line, per default
419  if (typedChar != QLatin1Char('\n')) {
420  return;
421  }
422 
423  // keep indent of previous line
424  keepIndent(position.line());
425  return;
426  }
427 
428  // no script, do nothing...
429  if (!m_script) {
430  return;
431  }
432 
433  // does the script allow this char as trigger?
434  if (typedChar != QLatin1Char('\n') && !m_script->triggerCharacters().contains(typedChar)) {
435  return;
436  }
437 
438  // let the script indent for us...
439  scriptIndent(view, position, typedChar);
440 }
441 // END KateAutoIndent
442 
443 // BEGIN KateViewIndentAction
444 KateViewIndentationAction::KateViewIndentationAction(KTextEditor::DocumentPrivate *_doc, const QString &text, QObject *parent)
445  : KActionMenu(text, parent)
446  , doc(_doc)
447 {
448  setDelayed(false);
449  connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow()));
450  actionGroup = new QActionGroup(menu());
451 }
452 
453 void KateViewIndentationAction::slotAboutToShow()
454 {
455  const QStringList modes = KateAutoIndent::listModes();
456 
457  menu()->clear();
458  const auto actions = actionGroup->actions();
459  for (QAction *action : actions) {
460  actionGroup->removeAction(action);
461  }
462  for (int z = 0; z < modes.size(); ++z) {
463  QAction *action = menu()->addAction(QLatin1Char('&') + KateAutoIndent::modeDescription(z).replace(QLatin1Char('&'), QLatin1String("&&")));
464  actionGroup->addAction(action);
465  action->setCheckable(true);
466  action->setData(z);
467 
468  QString requiredStyle = KateAutoIndent::modeRequiredStyle(z);
469  action->setEnabled(requiredStyle.isEmpty() || requiredStyle == doc->highlight()->style());
470 
471  if (doc->config()->indentationMode() == KateAutoIndent::modeName(z)) {
472  action->setChecked(true);
473  }
474  }
475 
476  disconnect(menu(), SIGNAL(triggered(QAction *)), this, SLOT(setMode(QAction *)));
477  connect(menu(), SIGNAL(triggered(QAction *)), this, SLOT(setMode(QAction *)));
478 }
479 
480 void KateViewIndentationAction::setMode(QAction *action)
481 {
482  // set new mode
483  doc->config()->setIndentationMode(KateAutoIndent::modeName(action->data().toInt()));
484  doc->rememberUserDidSetIndentationMode();
485 }
486 // 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...
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 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
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:203
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:276
constexpr int line() const Q_DECL_NOEXCEPT
Retrieve the line on which this cursor is situated.
Definition: cursor.h:185
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:343
QObject * parent() const const
~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-2020 The KDE developers.
Generated on Sat Oct 24 2020 23:02:53 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.