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"