File indexing completed on 2024-04-21 03:57:46

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