File indexing completed on 2024-04-28 15:30:51

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2003 Jesse Yurkovich <yurkjes@iit.edu>
0004 
0005     KateVarIndent class:
0006     SPDX-FileCopyrightText: 2004 Anders Lund <anders@alweb.dk>
0007 
0008     Basic support for config page:
0009     SPDX-FileCopyrightText: 2005 Dominik Haumann <dhdev@gmx.de>
0010 
0011     SPDX-License-Identifier: LGPL-2.0-only
0012 */
0013 
0014 #include "kateautoindent.h"
0015 
0016 #include "katedocument.h"
0017 #include "kateextendedattribute.h"
0018 #include "kateglobal.h"
0019 #include "katehighlight.h"
0020 #include "kateindentscript.h"
0021 #include "katepartdebug.h"
0022 #include "katescriptmanager.h"
0023 #include "kateview.h"
0024 
0025 #include <KLocalizedString>
0026 
0027 #include <QActionGroup>
0028 
0029 #include <cctype>
0030 
0031 namespace
0032 {
0033 inline const QString MODE_NONE()
0034 {
0035     return QStringLiteral("none");
0036 }
0037 inline const QString MODE_NORMAL()
0038 {
0039     return QStringLiteral("normal");
0040 }
0041 }
0042 
0043 // BEGIN KateAutoIndent
0044 
0045 QStringList KateAutoIndent::listModes()
0046 {
0047     QStringList l;
0048     l.reserve(modeCount());
0049     for (int i = 0; i < modeCount(); ++i) {
0050         l << modeDescription(i);
0051     }
0052 
0053     return l;
0054 }
0055 
0056 QStringList KateAutoIndent::listIdentifiers()
0057 {
0058     QStringList l;
0059     l.reserve(modeCount());
0060     for (int i = 0; i < modeCount(); ++i) {
0061         l << modeName(i);
0062     }
0063 
0064     return l;
0065 }
0066 
0067 int KateAutoIndent::modeCount()
0068 {
0069     // inbuild modes + scripts
0070     return 2 + KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptCount();
0071 }
0072 
0073 QString KateAutoIndent::modeName(int mode)
0074 {
0075     if (mode == 0 || mode >= modeCount()) {
0076         return MODE_NONE();
0077     }
0078 
0079     if (mode == 1) {
0080         return MODE_NORMAL();
0081     }
0082 
0083     return KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(mode - 2)->indentHeader().baseName();
0084 }
0085 
0086 QString KateAutoIndent::modeDescription(int mode)
0087 {
0088     if (mode == 0 || mode >= modeCount()) {
0089         return i18nc("Autoindent mode", "None");
0090     }
0091 
0092     if (mode == 1) {
0093         return i18nc("Autoindent mode", "Normal");
0094     }
0095 
0096     const QString &name = KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(mode - 2)->indentHeader().name();
0097     return i18nc("Autoindent mode", name.toUtf8().constData());
0098 }
0099 
0100 QString KateAutoIndent::modeRequiredStyle(int mode)
0101 {
0102     if (mode == 0 || mode == 1 || mode >= modeCount()) {
0103         return QString();
0104     }
0105 
0106     return KTextEditor::EditorPrivate::self()->scriptManager()->indentationScriptByIndex(mode - 2)->indentHeader().requiredStyle();
0107 }
0108 
0109 uint KateAutoIndent::modeNumber(const QString &name)
0110 {
0111     for (int i = 0; i < modeCount(); ++i) {
0112         if (modeName(i) == name) {
0113             return i;
0114         }
0115     }
0116 
0117     return 0;
0118 }
0119 
0120 KateAutoIndent::KateAutoIndent(KTextEditor::DocumentPrivate *_doc)
0121     : QObject(_doc)
0122     , doc(_doc)
0123     , m_script(nullptr)
0124 {
0125     // don't call updateConfig() here, document might is not ready for that....
0126 
0127     // on script reload, the script pointer is invalid -> force reload
0128     connect(KTextEditor::EditorPrivate::self()->scriptManager(), &KateScriptManager::reloaded, this, &KateAutoIndent::reloadScript);
0129 }
0130 
0131 KateAutoIndent::~KateAutoIndent() = default;
0132 
0133 QString KateAutoIndent::tabString(int length, int align) const
0134 {
0135     QString s;
0136     length = qMin(length, 256); // sanity check for large values of pos
0137     int spaces = qBound(0, align - length, 256);
0138 
0139     if (!useSpaces) {
0140         s.append(QString(length / tabWidth, QLatin1Char('\t')));
0141         length = length % tabWidth;
0142     }
0143     // we use spaces to indent any left over length
0144     s.append(QString(length + spaces, QLatin1Char(' ')));
0145 
0146     return s;
0147 }
0148 
0149 bool KateAutoIndent::doIndent(int line, int indentDepth, int align)
0150 {
0151     Kate::TextLine textline = doc->plainKateTextLine(line);
0152 
0153     // textline not found, cu
0154     if (!textline) {
0155         return false;
0156     }
0157 
0158     // sanity check
0159     if (indentDepth < 0) {
0160         indentDepth = 0;
0161     }
0162 
0163     const QString oldIndentation = textline->leadingWhitespace();
0164 
0165     // Preserve existing "tabs then spaces" alignment if and only if:
0166     //  - no alignment was passed to doIndent and
0167     //  - we aren't using spaces for indentation and
0168     //  - we aren't rounding indentation up to the next multiple of the indentation width and
0169     //  - we aren't using a combination to tabs and spaces for alignment, or in other words
0170     //    the indent width is a multiple of the tab width.
0171     bool preserveAlignment = !useSpaces && keepExtra && indentWidth % tabWidth == 0;
0172     if (align == 0 && preserveAlignment) {
0173         // Count the number of consecutive spaces at the end of the existing indentation
0174         int i = oldIndentation.size() - 1;
0175         while (i >= 0 && oldIndentation.at(i) == QLatin1Char(' ')) {
0176             --i;
0177         }
0178         // Use the passed indentDepth as the alignment, and set the indentDepth to
0179         // that value minus the number of spaces found (but don't let it get negative).
0180         align = indentDepth;
0181         indentDepth = qMax(0, align - (oldIndentation.size() - 1 - i));
0182     }
0183 
0184     QString indentString = tabString(indentDepth, align);
0185 
0186     // Modify the document *ONLY* if smth has really changed!
0187     if (oldIndentation != indentString) {
0188         // insert the required new indentation
0189         // before removing the old indentation
0190         // to prevent the selection to be shrink by the first removal
0191         // (see bug329247)
0192         doc->editStart();
0193         doc->editInsertText(line, 0, indentString);
0194         doc->editRemoveText(line, indentString.length(), oldIndentation.length());
0195         doc->editEnd();
0196     }
0197 
0198     return true;
0199 }
0200 
0201 bool KateAutoIndent::doIndentRelative(int line, int change)
0202 {
0203     Kate::TextLine textline = doc->plainKateTextLine(line);
0204 
0205     // get indent width of current line
0206     int indentDepth = textline->indentDepth(tabWidth);
0207     int extraSpaces = indentDepth % indentWidth;
0208 
0209     // add change
0210     indentDepth += change;
0211 
0212     // if keepExtra is off, snap to a multiple of the indentWidth
0213     if (!keepExtra && extraSpaces > 0) {
0214         if (change < 0) {
0215             indentDepth += indentWidth - extraSpaces;
0216         } else {
0217             indentDepth -= extraSpaces;
0218         }
0219     }
0220 
0221     // do indent
0222     return doIndent(line, indentDepth);
0223 }
0224 
0225 void KateAutoIndent::keepIndent(int line)
0226 {
0227     // no line in front, no work...
0228     if (line <= 0) {
0229         return;
0230     }
0231 
0232     // keep indentation: find line with content
0233     int nonEmptyLine = line - 1;
0234     while (nonEmptyLine >= 0) {
0235         if (doc->lineLength(nonEmptyLine) > 0) {
0236             break;
0237         }
0238         --nonEmptyLine;
0239     }
0240     Kate::TextLine prevTextLine = doc->plainKateTextLine(nonEmptyLine);
0241     Kate::TextLine textLine = doc->plainKateTextLine(line);
0242 
0243     // textline not found, cu
0244     if (!prevTextLine || !textLine) {
0245         return;
0246     }
0247 
0248     const QString previousWhitespace = prevTextLine->leadingWhitespace();
0249 
0250     // remove leading whitespace, then insert the leading indentation
0251     doc->editStart();
0252 
0253     int indentDepth = textLine->indentDepth(tabWidth);
0254     int extraSpaces = indentDepth % indentWidth;
0255     doc->editRemoveText(line, 0, textLine->leadingWhitespace().size());
0256     if (keepExtra && extraSpaces > 0)
0257         doc->editInsertText(line, 0, QString(extraSpaces, QLatin1Char(' ')));
0258 
0259     doc->editInsertText(line, 0, previousWhitespace);
0260     doc->editEnd();
0261 }
0262 
0263 void KateAutoIndent::reloadScript()
0264 {
0265     // small trick to force reload
0266     m_script = nullptr; // prevent dangling pointer
0267     QString currentMode = m_mode;
0268     m_mode = QString();
0269     setMode(currentMode);
0270 }
0271 
0272 void KateAutoIndent::scriptIndent(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor position, QChar typedChar)
0273 {
0274     // start edit
0275     doc->pushEditState();
0276     doc->editStart();
0277 
0278     QPair<int, int> result = m_script->indent(view, position, typedChar, indentWidth);
0279     int newIndentInChars = result.first;
0280 
0281     // handle negative values special
0282     if (newIndentInChars < -1) {
0283         // do nothing atm
0284     }
0285 
0286     // reuse indentation of the previous line, just like the "normal" indenter
0287     else if (newIndentInChars == -1) {
0288         // keep indent of previous line
0289         keepIndent(position.line());
0290     }
0291 
0292     // get align
0293     else {
0294         // we got a positive or zero indent to use...
0295         doIndent(position.line(), newIndentInChars, result.second);
0296     }
0297 
0298     // end edit in all cases
0299     doc->editEnd();
0300     doc->popEditState();
0301 }
0302 
0303 bool KateAutoIndent::isStyleProvided(const KateIndentScript *script, const KateHighlighting *highlight)
0304 {
0305     QString requiredStyle = script->indentHeader().requiredStyle();
0306     return (requiredStyle.isEmpty() || requiredStyle == highlight->style());
0307 }
0308 
0309 void KateAutoIndent::setMode(const QString &name)
0310 {
0311     // bail out, already set correct mode...
0312     if (m_mode == name) {
0313         return;
0314     }
0315 
0316     // cleanup
0317     m_script = nullptr;
0318 
0319     // first, catch easy stuff... normal mode and none, easy...
0320     if (name.isEmpty() || name == MODE_NONE()) {
0321         m_mode = MODE_NONE();
0322         return;
0323     }
0324 
0325     if (name == MODE_NORMAL()) {
0326         m_mode = MODE_NORMAL();
0327         return;
0328     }
0329 
0330     // handle script indenters, if any for this name...
0331     KateIndentScript *script = KTextEditor::EditorPrivate::self()->scriptManager()->indentationScript(name);
0332     if (script) {
0333         if (isStyleProvided(script, doc->highlight())) {
0334             m_script = script;
0335             m_mode = name;
0336             return;
0337         } else {
0338             qCWarning(LOG_KTE) << "mode" << name << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style"
0339                                << doc->highlight()->style() << "but script requires" << script->indentHeader().requiredStyle();
0340         }
0341     } else {
0342         qCWarning(LOG_KTE) << "mode" << name << "does not exist";
0343     }
0344 
0345     // Fall back to normal
0346     m_mode = MODE_NORMAL();
0347 }
0348 
0349 void KateAutoIndent::checkRequiredStyle()
0350 {
0351     if (m_script) {
0352         if (!isStyleProvided(m_script, doc->highlight())) {
0353             qCDebug(LOG_KTE) << "mode" << m_mode << "requires a different highlight style: highlighting" << doc->highlight()->name() << "with style"
0354                              << doc->highlight()->style() << "but script requires" << m_script->indentHeader().requiredStyle();
0355             doc->config()->setIndentationMode(MODE_NORMAL());
0356         }
0357     }
0358 }
0359 
0360 void KateAutoIndent::updateConfig()
0361 {
0362     KateDocumentConfig *config = doc->config();
0363 
0364     useSpaces = config->replaceTabsDyn();
0365     keepExtra = config->keepExtraSpaces();
0366     tabWidth = config->tabWidth();
0367     indentWidth = config->indentationWidth();
0368 }
0369 
0370 bool KateAutoIndent::changeIndent(KTextEditor::Range range, int change)
0371 {
0372     std::vector<int> skippedLines;
0373 
0374     // loop over all lines given...
0375     for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(range.end().line(), doc->lines() - 1); ++line) {
0376         // don't indent empty lines
0377         if (doc->line(line).isEmpty()) {
0378             skippedLines.push_back(line);
0379             continue;
0380         }
0381         // don't indent the last line when the cursor is on the first column
0382         if (line == range.end().line() && range.end().column() == 0) {
0383             skippedLines.push_back(line);
0384             continue;
0385         }
0386 
0387         doIndentRelative(line, change * indentWidth);
0388     }
0389 
0390     if (static_cast<int>(skippedLines.size()) > range.numberOfLines()) {
0391         // all lines were empty, so indent them nevertheless
0392         for (int line : skippedLines) {
0393             doIndentRelative(line, change * indentWidth);
0394         }
0395     }
0396 
0397     return true;
0398 }
0399 
0400 void KateAutoIndent::indent(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
0401 {
0402     // no script, do nothing...
0403     if (!m_script) {
0404         return;
0405     }
0406 
0407     // we want one undo action >= START
0408     doc->setUndoMergeAllEdits(true);
0409 
0410     bool prevKeepExtra = keepExtra;
0411     keepExtra = false; // we are formatting a block of code, no extra spaces
0412     // loop over all lines given...
0413     for (int line = range.start().line() < 0 ? 0 : range.start().line(); line <= qMin(range.end().line(), doc->lines() - 1); ++line) {
0414         // let the script indent for us...
0415         scriptIndent(view, KTextEditor::Cursor(line, 0), QChar());
0416     }
0417 
0418     keepExtra = prevKeepExtra;
0419     // we want one undo action => END
0420     doc->setUndoMergeAllEdits(false);
0421 }
0422 
0423 void KateAutoIndent::userTypedChar(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor position, QChar typedChar)
0424 {
0425     // normal mode
0426     if (m_mode == MODE_NORMAL()) {
0427         // only indent on new line, per default
0428         if (typedChar != QLatin1Char('\n')) {
0429             return;
0430         }
0431 
0432         // keep indent of previous line
0433         keepIndent(position.line());
0434         return;
0435     }
0436 
0437     // no script, do nothing...
0438     if (!m_script) {
0439         return;
0440     }
0441 
0442     // does the script allow this char as trigger?
0443     if (typedChar != QLatin1Char('\n') && !m_script->triggerCharacters().contains(typedChar)) {
0444         return;
0445     }
0446 
0447     // let the script indent for us...
0448     scriptIndent(view, position, typedChar);
0449 }
0450 // END KateAutoIndent
0451 
0452 // BEGIN KateViewIndentAction
0453 KateViewIndentationAction::KateViewIndentationAction(KTextEditor::DocumentPrivate *_doc, const QString &text, QObject *parent)
0454     : KActionMenu(text, parent)
0455     , doc(_doc)
0456 {
0457     setPopupMode(QToolButton::InstantPopup);
0458     connect(menu(), &QMenu::aboutToShow, this, &KateViewIndentationAction::slotAboutToShow);
0459     actionGroup = new QActionGroup(menu());
0460 }
0461 
0462 void KateViewIndentationAction::slotAboutToShow()
0463 {
0464     const QStringList modes = KateAutoIndent::listModes();
0465 
0466     menu()->clear();
0467     const auto actions = actionGroup->actions();
0468     for (QAction *action : actions) {
0469         actionGroup->removeAction(action);
0470     }
0471     for (int z = 0; z < modes.size(); ++z) {
0472         QAction *action = menu()->addAction(QLatin1Char('&') + KateAutoIndent::modeDescription(z).replace(QLatin1Char('&'), QLatin1String("&&")));
0473         actionGroup->addAction(action);
0474         action->setCheckable(true);
0475         action->setData(z);
0476 
0477         QString requiredStyle = KateAutoIndent::modeRequiredStyle(z);
0478         action->setEnabled(requiredStyle.isEmpty() || requiredStyle == doc->highlight()->style());
0479 
0480         if (doc->config()->indentationMode() == KateAutoIndent::modeName(z)) {
0481             action->setChecked(true);
0482         }
0483     }
0484 
0485     disconnect(menu(), &QMenu::triggered, this, &KateViewIndentationAction::setMode);
0486     connect(menu(), &QMenu::triggered, this, &KateViewIndentationAction::setMode);
0487 }
0488 
0489 void KateViewIndentationAction::setMode(QAction *action)
0490 {
0491     // set new mode
0492     doc->config()->setIndentationMode(KateAutoIndent::modeName(action->data().toInt()));
0493     doc->rememberUserDidSetIndentationMode();
0494 }
0495 // END KateViewIndentationAction
0496 
0497 #include "moc_kateautoindent.cpp"