1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2003 Jesse Yurkovich <yurkjes@iit.edu>
4
5 KateVarIndent class:
6 SPDX-FileCopyrightText: 2004 Anders Lund <anders@alweb.dk>
7
8 Basic support for config page:
9 SPDX-FileCopyrightText: 2005 Dominik Haumann <dhdev@gmx.de>
10
11 SPDX-License-Identifier: LGPL-2.0-only
12*/
13
14#include "kateautoindent.h"
15
16#include "attribute.h"
17#include "katedocument.h"
18#include "kateglobal.h"
19#include "katehighlight.h"
20#include "kateindentscript.h"
21#include "katepartdebug.h"
22#include "katescriptmanager.h"
23
24#include <KLocalizedString>
25
26#include <QActionGroup>
27#include <QMenu>
28
29namespace
30{
31inline const QString MODE_NONE()
32{
33 return QStringLiteral("none");
34}
35inline const QString MODE_NORMAL()
36{
37 return QStringLiteral("normal");
38}
39}
40
41// BEGIN KateAutoIndent
42
43QStringList KateAutoIndent::listModes()
44{
45 QStringList l;
46 l.reserve(asize: modeCount());
47 for (int i = 0; i < modeCount(); ++i) {
48 l << modeDescription(mode: i);
49 }
50
51 return l;
52}
53
54QStringList KateAutoIndent::listIdentifiers()
55{
56 QStringList l;
57 l.reserve(asize: modeCount());
58 for (int i = 0; i < modeCount(); ++i) {
59 l << modeName(mode: i);
60 }
61
62 return l;
63}
64
65int KateAutoIndent::modeCount()
66{
67 // inbuild modes + scripts
68 return 2 + KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptCount();
69}
70
71QString KateAutoIndent::modeName(int mode)
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(index: mode - 2)->indentHeader().baseName();
82}
83
84QString KateAutoIndent::modeDescription(int mode)
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(index: mode - 2)->indentHeader().name();
95 return i18nc("Autoindent mode", name.toUtf8().constData());
96}
97
98QString KateAutoIndent::modeRequiredStyle(int mode)
99{
100 if (mode == 0 || mode == 1 || mode >= modeCount()) {
101 return QString();
102 }
103
104 return KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(index: mode - 2)->indentHeader().requiredStyle();
105}
106
107uint KateAutoIndent::modeNumber(const QString &name)
108{
109 for (int i = 0; i < modeCount(); ++i) {
110 if (modeName(mode: i) == name) {
111 return i;
112 }
113 }
114
115 return 0;
116}
117
118KateAutoIndent::KateAutoIndent(KTextEditor::DocumentPrivate *_doc)
119 : QObject(_doc)
120 , doc(_doc)
121 , m_script(nullptr)
122{
123 // don't call updateConfig() here, document might is not ready for that....
124
125 // on script reload, the script pointer is invalid -> force reload
126 connect(sender: KTextEditor::EditorPrivate::self()->scriptManager(), signal: &KateScriptManager::reloaded, context: this, slot: &KateAutoIndent::reloadScript);
127}
128
129KateAutoIndent::~KateAutoIndent() = default;
130
131QString KateAutoIndent::tabString(int length, int align) const
132{
133 QString s;
134 length = qMin(a: length, b: 256); // sanity check for large values of pos
135 int spaces = qBound(min: 0, val: align - length, max: 256);
136
137 if (!useSpaces) {
138 s.append(s: QString(length / tabWidth, QLatin1Char('\t')));
139 length = length % tabWidth;
140 }
141 // we use spaces to indent any left over length
142 s.append(s: QString(length + spaces, QLatin1Char(' ')));
143
144 return s;
145}
146
147bool KateAutoIndent::doIndent(int line, int indentDepth, int align)
148{
149 Kate::TextLine textline = doc->plainKateTextLine(i: line);
150
151 // sanity check
152 if (indentDepth < 0) {
153 indentDepth = 0;
154 }
155
156 const QString oldIndentation = textline.leadingWhitespace();
157
158 // Preserve existing "tabs then spaces" alignment if and only if:
159 // - no alignment was passed to doIndent and
160 // - we aren't using spaces for indentation and
161 // - we aren't rounding indentation up to the next multiple of the indentation width and
162 // - we aren't using a combination to tabs and spaces for alignment, or in other words
163 // the indent width is a multiple of the tab width.
164 bool preserveAlignment = !useSpaces && keepExtra && indentWidth % tabWidth == 0;
165 if (align == 0 && preserveAlignment) {
166 // Count the number of consecutive spaces at the end of the existing indentation
167 int i = oldIndentation.size() - 1;
168 while (i >= 0 && oldIndentation.at(i) == QLatin1Char(' ')) {
169 --i;
170 }
171 // Use the passed indentDepth as the alignment, and set the indentDepth to
172 // that value minus the number of spaces found (but don't let it get negative).
173 align = indentDepth;
174 indentDepth = qMax(a: 0, b: align - (oldIndentation.size() - 1 - i));
175 }
176
177 QString indentString = tabString(length: indentDepth, align);
178
179 // Modify the document *ONLY* if smth has really changed!
180 if (oldIndentation != indentString) {
181 // insert the required new indentation
182 // before removing the old indentation
183 // to prevent the selection to be shrink by the first removal
184 // (see bug329247)
185 doc->editStart();
186 doc->editInsertText(line, col: 0, s: indentString);
187 doc->editRemoveText(line, col: indentString.length(), len: oldIndentation.length());
188 doc->editEnd();
189 }
190
191 return true;
192}
193
194bool KateAutoIndent::doIndentRelative(int line, int change)
195{
196 Kate::TextLine textline = doc->plainKateTextLine(i: 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
218void 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(line: nonEmptyLine) > 0) {
229 break;
230 }
231 --nonEmptyLine;
232 }
233
234 // no line in front, no work...
235 if (nonEmptyLine < 0) {
236 return;
237 }
238
239 Kate::TextLine prevTextLine = doc->plainKateTextLine(i: nonEmptyLine);
240 Kate::TextLine textLine = doc->plainKateTextLine(i: line);
241
242 const QString previousWhitespace = prevTextLine.leadingWhitespace();
243
244 // remove leading whitespace, then insert the leading indentation
245 doc->editStart();
246
247 int indentDepth = textLine.indentDepth(tabWidth);
248 int extraSpaces = indentDepth % indentWidth;
249 doc->editRemoveText(line, col: 0, len: textLine.leadingWhitespace().size());
250 if (keepExtra && extraSpaces > 0)
251 doc->editInsertText(line, col: 0, s: QString(extraSpaces, QLatin1Char(' ')));
252
253 doc->editInsertText(line, col: 0, s: previousWhitespace);
254 doc->editEnd();
255}
256
257void 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
266void 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, typedCharacter: 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(line: position.line());
284 }
285
286 // get align
287 else {
288 // we got a positive or zero indent to use...
289 doIndent(line: position.line(), indentDepth: newIndentInChars, align: result.second);
290 }
291
292 // end edit in all cases
293 doc->editEnd();
294 doc->popEditState();
295}
296
297bool KateAutoIndent::isStyleProvided(const KateIndentScript *script, const KateHighlighting *highlight)
298{
299 QString requiredStyle = script->indentHeader().requiredStyle();
300 return (requiredStyle.isEmpty() || requiredStyle == highlight->style());
301}
302
303void KateAutoIndent::setMode(const QString &name)
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(scriptname: name);
326 if (script) {
327 if (isStyleProvided(script, highlight: 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"
333 << doc->highlight()->style() << "but script requires" << 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
343void KateAutoIndent::checkRequiredStyle()
344{
345 if (m_script) {
346 if (!isStyleProvided(script: m_script, highlight: doc->highlight())) {
347 qCDebug(LOG_KTE) << "mode" << m_mode << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style"
348 << doc->highlight()->style() << "but script requires" << m_script->indentHeader().requiredStyle();
349 doc->config()->setIndentationMode(MODE_NORMAL());
350 }
351 }
352}
353
354void KateAutoIndent::updateConfig()
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
364bool KateAutoIndent::changeIndent(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(a: range.end().line(), b: doc->lines() - 1); ++line) {
370 // don't indent empty lines
371 if (doc->line(line).isEmpty()) {
372 skippedLines.push_back(x: 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(x: line);
378 continue;
379 }
380
381 doIndentRelative(line, change: 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: change * indentWidth);
388 }
389 }
390
391 return true;
392}
393
394void KateAutoIndent::indent(KTextEditor::ViewPrivate *view, 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 bool prevKeepExtra = keepExtra;
405 keepExtra = false; // we are formatting a block of code, no extra spaces
406 // loop over all lines given...
407 for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(a: range.end().line(), b: doc->lines() - 1); ++line) {
408 // let the script indent for us...
409 scriptIndent(view, position: KTextEditor::Cursor(line, 0), typedChar: QChar());
410 }
411
412 keepExtra = prevKeepExtra;
413 // we want one undo action => END
414 doc->setUndoMergeAllEdits(false);
415}
416
417void KateAutoIndent::userTypedChar(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor position, QChar typedChar)
418{
419 // normal mode
420 if (m_mode == MODE_NORMAL()) {
421 // only indent on new line, per default
422 if (typedChar != QLatin1Char('\n')) {
423 return;
424 }
425
426 // keep indent of previous line
427 keepIndent(line: position.line());
428 return;
429 }
430
431 // no script, do nothing...
432 if (!m_script) {
433 return;
434 }
435
436 // does the script allow this char as trigger?
437 if (typedChar != QLatin1Char('\n') && !m_script->triggerCharacters().contains(c: typedChar)) {
438 return;
439 }
440
441 // let the script indent for us...
442 scriptIndent(view, position, typedChar);
443}
444// END KateAutoIndent
445
446// BEGIN KateViewIndentAction
447KateViewIndentationAction::KateViewIndentationAction(KTextEditor::DocumentPrivate *_doc, const QString &text, QObject *parent)
448 : KActionMenu(text, parent)
449 , doc(_doc)
450{
451 setPopupMode(QToolButton::InstantPopup);
452 connect(sender: menu(), signal: &QMenu::aboutToShow, context: this, slot: &KateViewIndentationAction::slotAboutToShow);
453 actionGroup = new QActionGroup(menu());
454}
455
456void KateViewIndentationAction::slotAboutToShow()
457{
458 const QStringList modes = KateAutoIndent::listModes();
459
460 menu()->clear();
461 const auto actions = actionGroup->actions();
462 for (QAction *action : actions) {
463 actionGroup->removeAction(a: action);
464 }
465 for (int z = 0; z < modes.size(); ++z) {
466 QAction *action = menu()->addAction(text: QLatin1Char('&') + KateAutoIndent::modeDescription(mode: z).replace(c: QLatin1Char('&'), after: QLatin1String("&&")));
467 actionGroup->addAction(a: action);
468 action->setCheckable(true);
469 action->setData(z);
470
471 QString requiredStyle = KateAutoIndent::modeRequiredStyle(mode: z);
472 action->setEnabled(requiredStyle.isEmpty() || requiredStyle == doc->highlight()->style());
473
474 if (doc->config()->indentationMode() == KateAutoIndent::modeName(mode: z)) {
475 action->setChecked(true);
476 }
477 }
478
479 disconnect(sender: menu(), signal: &QMenu::triggered, receiver: this, slot: &KateViewIndentationAction::setMode);
480 connect(sender: menu(), signal: &QMenu::triggered, context: this, slot: &KateViewIndentationAction::setMode);
481}
482
483void KateViewIndentationAction::setMode(QAction *action)
484{
485 // set new mode
486 doc->config()->setIndentationMode(KateAutoIndent::modeName(mode: action->data().toInt()));
487 doc->rememberUserDidSetIndentationMode();
488}
489// END KateViewIndentationAction
490
491#include "moc_kateautoindent.cpp"
492

source code of ktexteditor/src/utils/kateautoindent.cpp