File indexing completed on 2024-04-28 07:46:18

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