File indexing completed on 2024-04-28 11:45:01

0001 /*
0002     SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
0003     SPDX-FileCopyrightText: 2002-2004 Christoph Cullmann <cullmann@kde.org>
0004     SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "katebuffer.h"
0010 #include "kateautoindent.h"
0011 #include "kateconfig.h"
0012 #include "katedocument.h"
0013 #include "kateglobal.h"
0014 #include "katehighlight.h"
0015 #include "katepartdebug.h"
0016 #include "katesyntaxmanager.h"
0017 
0018 #include <KLocalizedString>
0019 
0020 #include <QDate>
0021 #include <QFile>
0022 #include <QFileInfo>
0023 #include <QTextCodec>
0024 #include <QTextStream>
0025 #include <QTimer>
0026 
0027 /**
0028  * Initial value for m_maxDynamicContexts
0029  */
0030 static const int KATE_MAX_DYNAMIC_CONTEXTS = 512;
0031 
0032 /**
0033  * Create an empty buffer. (with one block with one empty line)
0034  */
0035 KateBuffer::KateBuffer(KTextEditor::DocumentPrivate *doc)
0036     : Kate::TextBuffer(doc)
0037     , m_doc(doc)
0038     , m_brokenEncoding(false)
0039     , m_tooLongLinesWrapped(false)
0040     , m_longestLineLoaded(0)
0041     , m_highlight(nullptr)
0042     , m_tabWidth(8)
0043     , m_lineHighlighted(0)
0044     , m_maxDynamicContexts(KATE_MAX_DYNAMIC_CONTEXTS)
0045 {
0046 }
0047 
0048 /**
0049  * Cleanup on destruction
0050  */
0051 KateBuffer::~KateBuffer() = default;
0052 
0053 void KateBuffer::editStart()
0054 {
0055     if (!startEditing()) {
0056         return;
0057     }
0058 }
0059 
0060 void KateBuffer::editEnd()
0061 {
0062     // not finished, do nothing
0063     if (!finishEditing()) {
0064         return;
0065     }
0066 
0067     // nothing change, OK
0068     if (!editingChangedBuffer()) {
0069         return;
0070     }
0071 
0072     // if we arrive here, line changed should be OK
0073     Q_ASSERT(editingMinimalLineChanged() != -1);
0074     Q_ASSERT(editingMaximalLineChanged() != -1);
0075     Q_ASSERT(editingMinimalLineChanged() <= editingMaximalLineChanged());
0076 
0077     updateHighlighting();
0078 }
0079 
0080 void KateBuffer::updateHighlighting()
0081 {
0082     // no highlighting, nothing to do
0083     if (!m_highlight) {
0084         return;
0085     }
0086 
0087     // if we don't touch the highlighted area => fine
0088     if (editingMinimalLineChanged() > m_lineHighlighted) {
0089         return;
0090     }
0091 
0092     // really update highlighting
0093     // look one line too far, needed for linecontinue stuff
0094     doHighlight(editingMinimalLineChanged(), editingMaximalLineChanged() + 1, true);
0095 }
0096 
0097 void KateBuffer::clear()
0098 {
0099     // call original clear function
0100     Kate::TextBuffer::clear();
0101 
0102     // reset the state
0103     m_brokenEncoding = false;
0104     m_tooLongLinesWrapped = false;
0105     m_longestLineLoaded = 0;
0106 
0107     // back to line 0 with hl
0108     m_lineHighlighted = 0;
0109 }
0110 
0111 bool KateBuffer::openFile(const QString &m_file, bool enforceTextCodec)
0112 {
0113     // first: setup fallback and normal encoding
0114     setEncodingProberType(KateGlobalConfig::global()->proberType());
0115     setFallbackTextCodec(KateGlobalConfig::global()->fallbackCodec());
0116     setTextCodec(m_doc->config()->codec());
0117 
0118     // setup eol
0119     setEndOfLineMode((EndOfLineMode)m_doc->config()->eol());
0120 
0121     // NOTE: we do not remove trailing spaces on load. This was discussed
0122     //       over the years again and again. bugs: 306926, 239077, ...
0123 
0124     // line length limit
0125     setLineLengthLimit(m_doc->lineLengthLimit());
0126 
0127     // then, try to load the file
0128     m_brokenEncoding = false;
0129     m_tooLongLinesWrapped = false;
0130     m_longestLineLoaded = 0;
0131 
0132     // allow non-existent files without error, if local file!
0133     // will allow to do "kate newfile.txt" without error messages but still fail if e.g. you mistype a url
0134     // and it can't be fetched via fish:// or other strange things in kio happen...
0135     // just clear() + exit with success!
0136 
0137     QFileInfo fileInfo(m_file);
0138     if (m_doc->url().isLocalFile() && !fileInfo.exists()) {
0139         clear();
0140         KTextEditor::Message *message = new KTextEditor::Message(i18nc("short translation, user created new file", "New file"), KTextEditor::Message::Warning);
0141         message->setPosition(KTextEditor::Message::TopInView);
0142         message->setAutoHide(1000);
0143         m_doc->postMessage(message);
0144 
0145         // remember error
0146         m_doc->m_openingError = true;
0147         m_doc->m_openingErrorMessage = i18n("The file %1 does not exist.", m_doc->url().toString());
0148         return true;
0149     }
0150 
0151     // check if this is a normal file or not, avoids to open char devices or directories!
0152     // else clear buffer and break out with error
0153     if (!fileInfo.isFile()) {
0154         clear();
0155         return false;
0156     }
0157 
0158     // try to load
0159     if (!load(m_file, m_brokenEncoding, m_tooLongLinesWrapped, m_longestLineLoaded, enforceTextCodec)) {
0160         return false;
0161     }
0162 
0163     // save back encoding
0164     m_doc->config()->setEncoding(QString::fromLatin1(textCodec()->name()));
0165 
0166     // set eol mode, if a eol char was found
0167     if (m_doc->config()->allowEolDetection()) {
0168         m_doc->config()->setEol(endOfLineMode());
0169     }
0170 
0171     // generate a bom?
0172     if (generateByteOrderMark()) {
0173         m_doc->config()->setBom(true);
0174     }
0175 
0176     // okay, loading did work
0177     return true;
0178 }
0179 
0180 bool KateBuffer::canEncode()
0181 {
0182     QTextCodec *codec = m_doc->config()->codec();
0183 
0184     // hardcode some Unicode encodings which can encode all chars
0185     if ((QString::fromLatin1(codec->name()) == QLatin1String("UTF-8")) || (QString::fromLatin1(codec->name()) == QLatin1String("ISO-10646-UCS-2"))) {
0186         return true;
0187     }
0188 
0189     for (int i = 0; i < lines(); i++) {
0190         if (!codec->canEncode(line(i)->text())) {
0191             qCDebug(LOG_KTE) << QLatin1String("ENC NAME: ") << codec->name();
0192             qCDebug(LOG_KTE) << QLatin1String("STRING LINE: ") << line(i)->text();
0193             qCDebug(LOG_KTE) << QLatin1String("ENC WORKING: FALSE");
0194 
0195             return false;
0196         }
0197     }
0198 
0199     return true;
0200 }
0201 
0202 bool KateBuffer::saveFile(const QString &m_file)
0203 {
0204     // first: setup fallback and normal encoding
0205     setEncodingProberType(KateGlobalConfig::global()->proberType());
0206     setFallbackTextCodec(KateGlobalConfig::global()->fallbackCodec());
0207     setTextCodec(m_doc->config()->codec());
0208 
0209     // setup eol
0210     setEndOfLineMode((EndOfLineMode)m_doc->config()->eol());
0211 
0212     // generate bom?
0213     setGenerateByteOrderMark(m_doc->config()->bom());
0214 
0215     // try to save
0216     if (!save(m_file)) {
0217         return false;
0218     }
0219 
0220     // no longer broken encoding, or we don't care
0221     m_brokenEncoding = false;
0222     m_tooLongLinesWrapped = false;
0223     m_longestLineLoaded = 0;
0224 
0225     // okay
0226     return true;
0227 }
0228 
0229 void KateBuffer::ensureHighlighted(int line, int lookAhead)
0230 {
0231     // valid line at all?
0232     if (line < 0 || line >= lines()) {
0233         return;
0234     }
0235 
0236     // already hl up-to-date for this line?
0237     if (line < m_lineHighlighted) {
0238         return;
0239     }
0240 
0241     // update hl until this line + max lookAhead
0242     int end = qMin(line + lookAhead, lines() - 1);
0243 
0244     // ensure we have enough highlighted
0245     doHighlight(m_lineHighlighted, end, false);
0246 }
0247 
0248 void KateBuffer::wrapLine(const KTextEditor::Cursor position)
0249 {
0250     // call original
0251     Kate::TextBuffer::wrapLine(position);
0252 
0253     if (m_lineHighlighted > position.line() + 1) {
0254         m_lineHighlighted++;
0255     }
0256 }
0257 
0258 void KateBuffer::unwrapLine(int line)
0259 {
0260     // reimplemented, so first call original
0261     Kate::TextBuffer::unwrapLine(line);
0262 
0263     if (m_lineHighlighted > line) {
0264         --m_lineHighlighted;
0265     }
0266 }
0267 
0268 void KateBuffer::setTabWidth(int w)
0269 {
0270     if ((m_tabWidth != w) && (m_tabWidth > 0)) {
0271         m_tabWidth = w;
0272 
0273         if (m_highlight && m_highlight->foldingIndentationSensitive()) {
0274             invalidateHighlighting();
0275         }
0276     }
0277 }
0278 
0279 void KateBuffer::setHighlight(int hlMode)
0280 {
0281     KateHighlighting *h = KateHlManager::self()->getHl(hlMode);
0282 
0283     // aha, hl will change
0284     if (h != m_highlight) {
0285         bool invalidate = !h->noHighlighting();
0286 
0287         if (m_highlight) {
0288             invalidate = true;
0289         }
0290 
0291         m_highlight = h;
0292 
0293         if (invalidate) {
0294             invalidateHighlighting();
0295         }
0296 
0297         // inform the document that the hl was really changed
0298         // needed to update attributes and more ;)
0299         m_doc->bufferHlChanged();
0300 
0301         // try to set indentation
0302         if (!h->indentation().isEmpty()) {
0303             m_doc->config()->setIndentationMode(h->indentation());
0304         }
0305     }
0306 }
0307 
0308 void KateBuffer::invalidateHighlighting()
0309 {
0310     m_lineHighlighted = 0;
0311 }
0312 
0313 void KateBuffer::doHighlight(int startLine, int endLine, bool invalidate)
0314 {
0315     // no hl around, no stuff to do
0316     if (!m_highlight || m_highlight->noHighlighting()) {
0317         return;
0318     }
0319 
0320 #ifdef BUFFER_DEBUGGING
0321     QTime t;
0322     t.start();
0323     qCDebug(LOG_KTE) << "HIGHLIGHTED START --- NEED HL, LINESTART: " << startLine << " LINEEND: " << endLine;
0324     qCDebug(LOG_KTE) << "HL UNTIL LINE: " << m_lineHighlighted;
0325 #endif
0326 
0327     // if possible get previous line, otherwise create 0 line.
0328     Kate::TextLine prevLine = (startLine >= 1) ? plainLine(startLine - 1) : Kate::TextLine();
0329 
0330     // here we are atm, start at start line in the block
0331     int current_line = startLine;
0332     int start_spellchecking = -1;
0333     int last_line_spellchecking = -1;
0334     bool ctxChanged = false;
0335     // loop over the lines of the block, from startline to endline or end of block
0336     // if stillcontinue forces us to do so
0337     for (; current_line < qMin(endLine + 1, lines()); ++current_line) {
0338         // handle one line
0339         ctxChanged = false;
0340         Kate::TextLine textLine = plainLine(current_line);
0341         m_highlight->doHighlight(prevLine.get(), textLine.get(), ctxChanged);
0342         prevLine = textLine;
0343 
0344 #ifdef BUFFER_DEBUGGING
0345         // debug stuff
0346         qCDebug(LOG_KTE) << "current line to hl: " << current_line;
0347         qCDebug(LOG_KTE) << "text length: " << textLine->length() << " attribute list size: " << textLine->attributesList().size();
0348 
0349         const QVector<int> &ml(textLine->attributesList());
0350         for (int i = 2; i < ml.size(); i += 3) {
0351             qCDebug(LOG_KTE) << "start: " << ml.at(i - 2) << " len: " << ml.at(i - 1) << " at: " << ml.at(i) << " ";
0352         }
0353         qCDebug(LOG_KTE);
0354 #endif
0355 
0356         // need we to continue ?
0357         bool stillcontinue = ctxChanged;
0358         if (stillcontinue && start_spellchecking < 0) {
0359             start_spellchecking = current_line;
0360         } else if (!stillcontinue && start_spellchecking >= 0) {
0361             last_line_spellchecking = current_line;
0362         }
0363     }
0364 
0365     // perhaps we need to adjust the maximal highlighted line
0366     int oldHighlighted = m_lineHighlighted;
0367     if (ctxChanged || current_line > m_lineHighlighted) {
0368         m_lineHighlighted = current_line;
0369     }
0370 
0371     // tag the changed lines !
0372     if (invalidate) {
0373 #ifdef BUFFER_DEBUGGING
0374         qCDebug(LOG_KTE) << "HIGHLIGHTED TAG LINES: " << startLine << current_line;
0375 #endif
0376 
0377         Q_EMIT tagLines({startLine, qMax(current_line, oldHighlighted)});
0378 
0379         if (start_spellchecking >= 0 && lines() > 0) {
0380             Q_EMIT respellCheckBlock(start_spellchecking,
0381                                      qMin(lines() - 1, (last_line_spellchecking == -1) ? qMax(current_line, oldHighlighted) : last_line_spellchecking));
0382         }
0383     }
0384 
0385 #ifdef BUFFER_DEBUGGING
0386     qCDebug(LOG_KTE) << "HIGHLIGHTED END --- NEED HL, LINESTART: " << startLine << " LINEEND: " << endLine;
0387     qCDebug(LOG_KTE) << "HL UNTIL LINE: " << m_lineHighlighted;
0388     qCDebug(LOG_KTE) << "HL DYN COUNT: " << KateHlManager::self()->countDynamicCtxs() << " MAX: " << m_maxDynamicContexts;
0389     qCDebug(LOG_KTE) << "TIME TAKEN: " << t.elapsed();
0390 #endif
0391 }
0392 
0393 std::pair<bool, bool> KateBuffer::isFoldingStartingOnLine(int startLine)
0394 {
0395     // ensure valid input
0396     if (startLine < 0 || startLine >= lines()) {
0397         return {false, false};
0398     }
0399 
0400     // no highlighting, no folding, ATM
0401     if (!m_highlight || m_highlight->noHighlighting()) {
0402         return {false, false};
0403     }
0404 
0405     // first: get the wanted start line highlighted
0406     ensureHighlighted(startLine);
0407     const auto startTextLine = plainLine(startLine);
0408 
0409     // we prefer token based folding
0410     if (startTextLine->markedAsFoldingStartAttribute()) {
0411         return {true, false};
0412     }
0413 
0414     // check for indentation based folding
0415     if (m_highlight->foldingIndentationSensitive() && (tabWidth() > 0) && startTextLine->highlightingState().indentationBasedFoldingEnabled()
0416         && !m_highlight->isEmptyLine(startTextLine.get())) {
0417         // do some look ahead if this line might be a folding start
0418         // we limit this to avoid runtime disaster
0419         int linesVisited = 0;
0420         while (const auto nextLine = plainLine(++startLine)) {
0421             if (!m_highlight->isEmptyLine(nextLine.get())) {
0422                 const bool foldingStart = startTextLine->indentDepth(tabWidth()) < nextLine->indentDepth(tabWidth());
0423                 return {foldingStart, foldingStart};
0424             }
0425 
0426             // ensure some sensible limit of look ahead
0427             constexpr int lookAheadLimit = 64;
0428             if (++linesVisited > lookAheadLimit) {
0429                 break;
0430             }
0431         }
0432     }
0433 
0434     // no folding start of any kind
0435     return {false, false};
0436 }
0437 
0438 KTextEditor::Range KateBuffer::computeFoldingRangeForStartLine(int startLine)
0439 {
0440     // check for start, will trigger highlighting, too, and rule out bad lines
0441     const auto [foldingStart, foldingIndentationSensitive] = isFoldingStartingOnLine(startLine);
0442     if (!foldingStart) {
0443         return KTextEditor::Range::invalid();
0444     }
0445     const auto startTextLine = plainLine(startLine);
0446 
0447     // now: decided if indentation based folding or not!
0448     if (foldingIndentationSensitive) {
0449         // get our start indentation level
0450         const int startIndentation = startTextLine->indentDepth(tabWidth());
0451 
0452         // search next line with indentation level <= our one
0453         int lastLine = startLine + 1;
0454         for (; lastLine < lines(); ++lastLine) {
0455             // get line
0456             Kate::TextLine textLine = plainLine(lastLine);
0457 
0458             // indentation higher than our start line? continue
0459             if (startIndentation < textLine->indentDepth(tabWidth())) {
0460                 continue;
0461             }
0462 
0463             // empty line? continue
0464             if (m_highlight->isEmptyLine(textLine.get())) {
0465                 continue;
0466             }
0467 
0468             // else, break
0469             break;
0470         }
0471 
0472         // lastLine is always one too much
0473         --lastLine;
0474 
0475         // backtrack all empty lines, we don't want to add them to the fold!
0476         while (lastLine > startLine) {
0477             if (m_highlight->isEmptyLine(plainLine(lastLine).get())) {
0478                 --lastLine;
0479             } else {
0480                 break;
0481             }
0482         }
0483 
0484         // we shall not fold one-liners
0485         if (lastLine == startLine) {
0486             return KTextEditor::Range::invalid();
0487         }
0488 
0489         // be done now
0490         return KTextEditor::Range(KTextEditor::Cursor(startLine, 0), KTextEditor::Cursor(lastLine, plainLine(lastLine)->length()));
0491     }
0492 
0493     // 'normal' attribute based folding, aka token based like '{' BLUB '}'
0494 
0495     // first step: search the first region type, that stays open for the start line
0496     short openedRegionType = 0;
0497     int openedRegionOffset = -1;
0498     {
0499         // mapping of type to "first" offset of it and current number of not matched openings
0500         QHash<short, QPair<int, int>> foldingStartToOffsetAndCount;
0501 
0502         // walk over all attributes of the line and compute the matchings
0503         const auto &startLineAttributes = startTextLine->foldings();
0504         for (size_t i = 0; i < startLineAttributes.size(); ++i) {
0505             // folding close?
0506             if (startLineAttributes[i].foldingValue < 0) {
0507                 // search for this type, try to decrement counter, perhaps erase element!
0508                 QHash<short, QPair<int, int>>::iterator end = foldingStartToOffsetAndCount.find(-startLineAttributes[i].foldingValue);
0509                 if (end != foldingStartToOffsetAndCount.end()) {
0510                     if (end.value().second > 1) {
0511                         --(end.value().second);
0512                     } else {
0513                         foldingStartToOffsetAndCount.erase(end);
0514                     }
0515                 }
0516             }
0517 
0518             // folding open?
0519             if (startLineAttributes[i].foldingValue > 0) {
0520                 // search for this type, either insert it, with current offset or increment counter!
0521                 QHash<short, QPair<int, int>>::iterator start = foldingStartToOffsetAndCount.find(startLineAttributes[i].foldingValue);
0522                 if (start != foldingStartToOffsetAndCount.end()) {
0523                     ++(start.value().second);
0524                 } else {
0525                     foldingStartToOffsetAndCount.insert(startLineAttributes[i].foldingValue, qMakePair(startLineAttributes[i].offset, 1));
0526                 }
0527             }
0528         }
0529 
0530         // compute first type with offset
0531         QHashIterator<short, QPair<int, int>> hashIt(foldingStartToOffsetAndCount);
0532         while (hashIt.hasNext()) {
0533             hashIt.next();
0534             if (openedRegionOffset == -1 || hashIt.value().first < openedRegionOffset) {
0535                 openedRegionType = hashIt.key();
0536                 openedRegionOffset = hashIt.value().first;
0537             }
0538         }
0539     }
0540 
0541     // no opening region found, bad, nothing to do
0542     if (openedRegionType == 0) {
0543         return KTextEditor::Range::invalid();
0544     }
0545 
0546     // second step: search for matching end region marker!
0547     int countOfOpenRegions = 1;
0548     for (int line = startLine + 1; line < lines(); ++line) {
0549         // ensure line is highlighted
0550         ensureHighlighted(line);
0551         Kate::TextLine textLine = plainLine(line);
0552 
0553         // search for matching end marker
0554         const auto &lineAttributes = textLine->foldings();
0555         for (size_t i = 0; i < lineAttributes.size(); ++i) {
0556             // matching folding close?
0557             if (lineAttributes[i].foldingValue == -openedRegionType) {
0558                 --countOfOpenRegions;
0559 
0560                 // end reached?
0561                 // compute resulting range!
0562                 if (countOfOpenRegions == 0) {
0563                     // Don't return a valid range without content!
0564                     if (line - startLine == 1) {
0565                         return KTextEditor::Range::invalid();
0566                     }
0567 
0568                     // return computed range
0569                     return KTextEditor::Range(KTextEditor::Cursor(startLine, openedRegionOffset), KTextEditor::Cursor(line, lineAttributes[i].offset));
0570                 }
0571             }
0572 
0573             // matching folding open?
0574             if (lineAttributes[i].foldingValue == openedRegionType) {
0575                 ++countOfOpenRegions;
0576             }
0577         }
0578     }
0579 
0580     // if we arrive here, the opened range spans to the end of the document!
0581     return KTextEditor::Range(KTextEditor::Cursor(startLine, openedRegionOffset), KTextEditor::Cursor(lines() - 1, plainLine(lines() - 1)->length()));
0582 }
0583 
0584 #include "moc_katebuffer.cpp"