KTextEditor

kateautoindent.cpp
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
44{
46 l.reserve(modeCount());
47 for (int i = 0; i < modeCount(); ++i) {
48 l << modeDescription(i);
49 }
50
51 return l;
52}
53
55{
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
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(KTextEditor::EditorPrivate::self()->scriptManager(), &KateScriptManager::reloaded, this, &KateAutoIndent::reloadScript);
127}
128
130
131QString KateAutoIndent::tabString(int length, int align) const
132{
133 QString s;
134 length = qMin(length, 256); // sanity check for large values of pos
135 int spaces = qBound(0, align - length, 256);
136
137 if (!useSpaces) {
138 s.append(QString(length / tabWidth, QLatin1Char('\t')));
139 length = length % tabWidth;
140 }
141 // we use spaces to indent any left over length
142 s.append(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(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(0, align - (oldIndentation.size() - 1 - i));
175 }
176
177 QString indentString = tabString(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, 0, indentString);
187 doc->editRemoveText(line, indentString.length(), 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(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(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(nonEmptyLine);
240 Kate::TextLine textLine = doc->plainKateTextLine(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, 0, textLine.leadingWhitespace().size());
250 if (keepExtra && extraSpaces > 0)
251 doc->editInsertText(line, 0, QString(extraSpaces, QLatin1Char(' ')));
252
253 doc->editInsertText(line, 0, 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, 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
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
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"
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
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"
348 << doc->highlight()->style() << "but script requires" << 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
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
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(range.end().line(), doc->lines() - 1); ++line) {
408 // let the script indent for us...
409 scriptIndent(view, KTextEditor::Cursor(line, 0), 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(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(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(menu(), &QMenu::aboutToShow, this, &KateViewIndentationAction::slotAboutToShow);
453 actionGroup = new QActionGroup(menu());
454}
455
456void KateViewIndentationAction::slotAboutToShow()
457{
459
460 menu()->clear();
461 const auto actions = actionGroup->actions();
462 for (QAction *action : actions) {
463 actionGroup->removeAction(action);
464 }
465 for (int z = 0; z < modes.size(); ++z) {
467 actionGroup->addAction(action);
468 action->setCheckable(true);
469 action->setData(z);
470
472 action->setEnabled(requiredStyle.isEmpty() || requiredStyle == doc->highlight()->style());
473
474 if (doc->config()->indentationMode() == KateAutoIndent::modeName(z)) {
475 action->setChecked(true);
476 }
477 }
478
479 disconnect(menu(), &QMenu::triggered, this, &KateViewIndentationAction::setMode);
480 connect(menu(), &QMenu::triggered, this, &KateViewIndentationAction::setMode);
481}
482
483void KateViewIndentationAction::setMode(QAction *action)
484{
485 // set new mode
486 doc->config()->setIndentationMode(KateAutoIndent::modeName(action->data().toInt()));
487 doc->rememberUserDidSetIndentationMode();
488}
489// END KateViewIndentationAction
490
491#include "moc_kateautoindent.cpp"
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
KateScriptManager * scriptManager()
Global script collection.
Definition kateglobal.h:277
static KTextEditor::EditorPrivate * self()
Kate Part Internal stuff ;)
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr int numberOfLines() const noexcept
Returns the number of lines separating the start() and end() positions.
void indent(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
The document requests the indenter to indent the given range of existing text.
KateAutoIndent(KTextEditor::DocumentPrivate *doc)
Constructor, creates dummy indenter "None".
bool changeIndent(KTextEditor::Range range, int change)
Function to provide the common indent/unindent/clean indent functionality to the document This should...
static uint modeNumber(const QString &name)
Maps name -> index.
~KateAutoIndent() override
Destructor.
const QString & modeName() const
mode name
static int modeCount()
count of modes
void checkRequiredStyle()
Check if the current highlighting mode provides the style required by the current indenter.
void updateConfig()
Update indenter's configuration (indention width, etc.) Is called in the updateConfig() of the docume...
void setMode(const QString &name)
Switch indenter Nop if already set to given mode Otherwise switch to given indenter or to "None" if n...
static QString modeRequiredStyle(int mode)
Return the syntax highlighting style required to use this mode.
void userTypedChar(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor position, QChar typedChar)
The user typed some char, the indenter can react on this ' ' will be send as char if the user wraps a...
static QStringList listModes()
List all possible modes by name, i.e.
static QString modeDescription(int mode)
Return the mode description.
static QStringList listIdentifiers()
List all possible names, i.e.
A specialized class for scripts that are of type ScriptType::Indentation.
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 reloaded()
this signal is emitted when all scripts are deleted and reloaded again.
Class representing a single text line.
QString leadingWhitespace() const
Leading whitespace of this line.
int indentDepth(int tabWidth) const
Returns the indentation depth with each tab expanded into tabWidth characters.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
const QList< QKeySequence > & replace()
void setCheckable(bool)
void setChecked(bool)
QVariant data() const const
void setEnabled(bool)
QMenu * menu() const const
void setData(const QVariant &data)
QList< QAction * > actions() const const
QAction * addAction(QAction *action)
void removeAction(QAction *action)
const char * constData() const const
void reserve(qsizetype size)
qsizetype size() const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
void aboutToShow()
void clear()
void triggered(QAction *action)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QString & append(QChar ch)
const QChar at(qsizetype position) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
qsizetype size() const const
QByteArray toUtf8() const const
int toInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:15:44 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.