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"