1 /*
2     SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
3     SPDX-FileCopyrightText: 2002-2004 Christoph Cullmann <cullmann@kde.org>
4     SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "katebuffer.h"
10 #include "kateautoindent.h"
11 #include "kateconfig.h"
12 #include "katedocument.h"
13 #include "kateglobal.h"
14 #include "katehighlight.h"
15 #include "katepartdebug.h"
16 #include "katesyntaxmanager.h"
17 
18 #include <KCharsets>
19 #include <KLocalizedString>
20 
21 #include <QDate>
22 #include <QFile>
23 #include <QFileInfo>
24 #include <QTextCodec>
25 #include <QTextStream>
26 #include <QTimer>
27 
28 /**
29  * Initial value for m_maxDynamicContexts
30  */
31 static const int KATE_MAX_DYNAMIC_CONTEXTS = 512;
32 
33 /**
34  * Create an empty buffer. (with one block with one empty line)
35  */
KateBuffer(KTextEditor::DocumentPrivate * doc)36 KateBuffer::KateBuffer(KTextEditor::DocumentPrivate *doc)
37     : Kate::TextBuffer(doc)
38     , m_doc(doc)
39     , m_brokenEncoding(false)
40     , m_tooLongLinesWrapped(false)
41     , m_longestLineLoaded(0)
42     , m_highlight(nullptr)
43     , m_tabWidth(8)
44     , m_lineHighlighted(0)
45     , m_maxDynamicContexts(KATE_MAX_DYNAMIC_CONTEXTS)
46 {
47 }
48 
49 /**
50  * Cleanup on destruction
51  */
52 KateBuffer::~KateBuffer() = default;
53 
editStart()54 void KateBuffer::editStart()
55 {
56     if (!startEditing()) {
57         return;
58     }
59 }
60 
editEnd()61 void KateBuffer::editEnd()
62 {
63     // not finished, do nothing
64     if (!finishEditing()) {
65         return;
66     }
67 
68     // nothing change, OK
69     if (!editingChangedBuffer()) {
70         return;
71     }
72 
73     // if we arrive here, line changed should be OK
74     Q_ASSERT(editingMinimalLineChanged() != -1);
75     Q_ASSERT(editingMaximalLineChanged() != -1);
76     Q_ASSERT(editingMinimalLineChanged() <= editingMaximalLineChanged());
77 
78     updateHighlighting();
79 }
80 
updateHighlighting()81 void KateBuffer::updateHighlighting()
82 {
83     // no highlighting, nothing to do
84     if (!m_highlight) {
85         return;
86     }
87 
88     // if we don't touch the highlighted area => fine
89     if (editingMinimalLineChanged() > m_lineHighlighted) {
90         return;
91     }
92 
93     // look one line too far, needed for linecontinue stuff
94     const int editTagLineEnd = editingMaximalLineChanged() + 1;
95 
96     // for indentation sensitive folding, we need to redo things
97     // one line up, to e.g. get notified about new folding starts
98     // see bug 351496
99     int editTagLineStart = editingMinimalLineChanged();
100     if ((editTagLineStart > 0) && m_highlight->foldingIndentationSensitive()) {
101         --editTagLineStart;
102     }
103 
104     // really update highlighting
105     doHighlight(editTagLineStart, editTagLineEnd, true);
106 }
107 
clear()108 void KateBuffer::clear()
109 {
110     // call original clear function
111     Kate::TextBuffer::clear();
112 
113     // reset the state
114     m_brokenEncoding = false;
115     m_tooLongLinesWrapped = false;
116     m_longestLineLoaded = 0;
117 
118     // back to line 0 with hl
119     m_lineHighlighted = 0;
120 }
121 
openFile(const QString & m_file,bool enforceTextCodec)122 bool KateBuffer::openFile(const QString &m_file, bool enforceTextCodec)
123 {
124     // first: setup fallback and normal encoding
125     setEncodingProberType(KateGlobalConfig::global()->proberType());
126     setFallbackTextCodec(KateGlobalConfig::global()->fallbackCodec());
127     setTextCodec(m_doc->config()->codec());
128 
129     // setup eol
130     setEndOfLineMode((EndOfLineMode)m_doc->config()->eol());
131 
132     // NOTE: we do not remove trailing spaces on load. This was discussed
133     //       over the years again and again. bugs: 306926, 239077, ...
134 
135     // line length limit
136     setLineLengthLimit(m_doc->lineLengthLimit());
137 
138     // then, try to load the file
139     m_brokenEncoding = false;
140     m_tooLongLinesWrapped = false;
141     m_longestLineLoaded = 0;
142 
143     // allow non-existent files without error, if local file!
144     // will allow to do "kate newfile.txt" without error messages but still fail if e.g. you mistype a url
145     // and it can't be fetched via fish:// or other strange things in kio happen...
146     // just clear() + exit with success!
147 
148     QFileInfo fileInfo(m_file);
149     if (m_doc->url().isLocalFile() && !fileInfo.exists()) {
150         clear();
151         KTextEditor::Message *message = new KTextEditor::Message(i18nc("short translation, user created new file", "New file"), KTextEditor::Message::Warning);
152         message->setPosition(KTextEditor::Message::TopInView);
153         message->setAutoHide(1000);
154         m_doc->postMessage(message);
155 
156         // remember error
157         m_doc->m_openingError = true;
158         m_doc->m_openingErrorMessage = i18n("The file %1 does not exist.", m_doc->url().toString());
159         return true;
160     }
161 
162     // check if this is a normal file or not, avoids to open char devices or directories!
163     // else clear buffer and break out with error
164     if (!fileInfo.isFile()) {
165         clear();
166         return false;
167     }
168 
169     // try to load
170     if (!load(m_file, m_brokenEncoding, m_tooLongLinesWrapped, m_longestLineLoaded, enforceTextCodec)) {
171         return false;
172     }
173 
174     // save back encoding
175     m_doc->config()->setEncoding(QString::fromLatin1(textCodec()->name()));
176 
177     // set eol mode, if a eol char was found
178     if (m_doc->config()->allowEolDetection()) {
179         m_doc->config()->setEol(endOfLineMode());
180     }
181 
182     // generate a bom?
183     if (generateByteOrderMark()) {
184         m_doc->config()->setBom(true);
185     }
186 
187     // okay, loading did work
188     return true;
189 }
190 
canEncode()191 bool KateBuffer::canEncode()
192 {
193     QTextCodec *codec = m_doc->config()->codec();
194 
195     // hardcode some Unicode encodings which can encode all chars
196     if ((QString::fromLatin1(codec->name()) == QLatin1String("UTF-8")) || (QString::fromLatin1(codec->name()) == QLatin1String("ISO-10646-UCS-2"))) {
197         return true;
198     }
199 
200     for (int i = 0; i < lines(); i++) {
201         if (!codec->canEncode(line(i)->string())) {
202             qCDebug(LOG_KTE) << QLatin1String("ENC NAME: ") << codec->name();
203             qCDebug(LOG_KTE) << QLatin1String("STRING LINE: ") << line(i)->string();
204             qCDebug(LOG_KTE) << QLatin1String("ENC WORKING: FALSE");
205 
206             return false;
207         }
208     }
209 
210     return true;
211 }
212 
saveFile(const QString & m_file)213 bool KateBuffer::saveFile(const QString &m_file)
214 {
215     // first: setup fallback and normal encoding
216     setEncodingProberType(KateGlobalConfig::global()->proberType());
217     setFallbackTextCodec(KateGlobalConfig::global()->fallbackCodec());
218     setTextCodec(m_doc->config()->codec());
219 
220     // setup eol
221     setEndOfLineMode((EndOfLineMode)m_doc->config()->eol());
222 
223     // generate bom?
224     setGenerateByteOrderMark(m_doc->config()->bom());
225 
226     // append a newline character at the end of the file (eof) ?
227     setNewLineAtEof(m_doc->config()->newLineAtEof());
228 
229     // try to save
230     if (!save(m_file)) {
231         return false;
232     }
233 
234     // no longer broken encoding, or we don't care
235     m_brokenEncoding = false;
236     m_tooLongLinesWrapped = false;
237     m_longestLineLoaded = 0;
238 
239     // okay
240     return true;
241 }
242 
ensureHighlighted(int line,int lookAhead)243 void KateBuffer::ensureHighlighted(int line, int lookAhead)
244 {
245     // valid line at all?
246     if (line < 0 || line >= lines()) {
247         return;
248     }
249 
250     // already hl up-to-date for this line?
251     if (line < m_lineHighlighted) {
252         return;
253     }
254 
255     // update hl until this line + max lookAhead
256     int end = qMin(line + lookAhead, lines() - 1);
257 
258     // ensure we have enough highlighted
259     doHighlight(m_lineHighlighted, end, false);
260 }
261 
wrapLine(const KTextEditor::Cursor position)262 void KateBuffer::wrapLine(const KTextEditor::Cursor position)
263 {
264     // call original
265     Kate::TextBuffer::wrapLine(position);
266 
267     if (m_lineHighlighted > position.line() + 1) {
268         m_lineHighlighted++;
269     }
270 }
271 
unwrapLine(int line)272 void KateBuffer::unwrapLine(int line)
273 {
274     // reimplemented, so first call original
275     Kate::TextBuffer::unwrapLine(line);
276 
277     if (m_lineHighlighted > line) {
278         --m_lineHighlighted;
279     }
280 }
281 
setTabWidth(int w)282 void KateBuffer::setTabWidth(int w)
283 {
284     if ((m_tabWidth != w) && (m_tabWidth > 0)) {
285         m_tabWidth = w;
286 
287         if (m_highlight && m_highlight->foldingIndentationSensitive()) {
288             invalidateHighlighting();
289         }
290     }
291 }
292 
setHighlight(int hlMode)293 void KateBuffer::setHighlight(int hlMode)
294 {
295     KateHighlighting *h = KateHlManager::self()->getHl(hlMode);
296 
297     // aha, hl will change
298     if (h != m_highlight) {
299         bool invalidate = !h->noHighlighting();
300 
301         if (m_highlight) {
302             invalidate = true;
303         }
304 
305         m_highlight = h;
306 
307         if (invalidate) {
308             invalidateHighlighting();
309         }
310 
311         // inform the document that the hl was really changed
312         // needed to update attributes and more ;)
313         m_doc->bufferHlChanged();
314 
315         // try to set indentation
316         if (!h->indentation().isEmpty()) {
317             m_doc->config()->setIndentationMode(h->indentation());
318         }
319     }
320 }
321 
invalidateHighlighting()322 void KateBuffer::invalidateHighlighting()
323 {
324     m_lineHighlighted = 0;
325 }
326 
doHighlight(int startLine,int endLine,bool invalidate)327 void KateBuffer::doHighlight(int startLine, int endLine, bool invalidate)
328 {
329     // no hl around, no stuff to do
330     if (!m_highlight || m_highlight->noHighlighting()) {
331         return;
332     }
333 
334 #ifdef BUFFER_DEBUGGING
335     QTime t;
336     t.start();
337     qCDebug(LOG_KTE) << "HIGHLIGHTED START --- NEED HL, LINESTART: " << startLine << " LINEEND: " << endLine;
338     qCDebug(LOG_KTE) << "HL UNTIL LINE: " << m_lineHighlighted;
339 #endif
340 
341     // if possible get previous line, otherwise create 0 line.
342     Kate::TextLine prevLine = (startLine >= 1) ? plainLine(startLine - 1) : Kate::TextLine();
343 
344     // here we are atm, start at start line in the block
345     int current_line = startLine;
346     int start_spellchecking = -1;
347     int last_line_spellchecking = -1;
348     bool ctxChanged = false;
349     Kate::TextLine textLine = plainLine(current_line);
350     Kate::TextLine nextLine;
351     // loop over the lines of the block, from startline to endline or end of block
352     // if stillcontinue forces us to do so
353     for (; current_line < qMin(endLine + 1, lines()); ++current_line) {
354         // get next line, if any
355         if ((current_line + 1) < lines()) {
356             nextLine = plainLine(current_line + 1);
357         } else {
358             nextLine = Kate::TextLine(new Kate::TextLineData());
359         }
360 
361         ctxChanged = false;
362         m_highlight->doHighlight(prevLine.data(), textLine.data(), nextLine.data(), ctxChanged, tabWidth());
363 
364 #ifdef BUFFER_DEBUGGING
365         // debug stuff
366         qCDebug(LOG_KTE) << "current line to hl: " << current_line;
367         qCDebug(LOG_KTE) << "text length: " << textLine->length() << " attribute list size: " << textLine->attributesList().size();
368 
369         const QVector<int> &ml(textLine->attributesList());
370         for (int i = 2; i < ml.size(); i += 3) {
371             qCDebug(LOG_KTE) << "start: " << ml.at(i - 2) << " len: " << ml.at(i - 1) << " at: " << ml.at(i) << " ";
372         }
373         qCDebug(LOG_KTE);
374 #endif
375 
376         // need we to continue ?
377         bool stillcontinue = ctxChanged;
378         if (stillcontinue && start_spellchecking < 0) {
379             start_spellchecking = current_line;
380         } else if (!stillcontinue && start_spellchecking >= 0) {
381             last_line_spellchecking = current_line;
382         }
383 
384         // move around the lines
385         prevLine = textLine;
386         textLine = nextLine;
387     }
388 
389     // perhaps we need to adjust the maximal highlighted line
390     int oldHighlighted = m_lineHighlighted;
391     if (ctxChanged || current_line > m_lineHighlighted) {
392         m_lineHighlighted = current_line;
393     }
394 
395     // tag the changed lines !
396     if (invalidate) {
397 #ifdef BUFFER_DEBUGGING
398         qCDebug(LOG_KTE) << "HIGHLIGHTED TAG LINES: " << startLine << current_line;
399 #endif
400 
401         Q_EMIT tagLines({startLine, qMax(current_line, oldHighlighted)});
402 
403         if (start_spellchecking >= 0 && lines() > 0) {
404             Q_EMIT respellCheckBlock(start_spellchecking,
405                                      qMin(lines() - 1, (last_line_spellchecking == -1) ? qMax(current_line, oldHighlighted) : last_line_spellchecking));
406         }
407     }
408 
409 #ifdef BUFFER_DEBUGGING
410     qCDebug(LOG_KTE) << "HIGHLIGHTED END --- NEED HL, LINESTART: " << startLine << " LINEEND: " << endLine;
411     qCDebug(LOG_KTE) << "HL UNTIL LINE: " << m_lineHighlighted;
412     qCDebug(LOG_KTE) << "HL DYN COUNT: " << KateHlManager::self()->countDynamicCtxs() << " MAX: " << m_maxDynamicContexts;
413     qCDebug(LOG_KTE) << "TIME TAKEN: " << t.elapsed();
414 #endif
415 }
416 
computeFoldingRangeForStartLine(int startLine)417 KTextEditor::Range KateBuffer::computeFoldingRangeForStartLine(int startLine)
418 {
419     // ensure valid input
420     if (startLine < 0 || startLine >= lines()) {
421         return KTextEditor::Range::invalid();
422     }
423 
424     // no highlighting, no folding, ATM
425     if (!m_highlight || m_highlight->noHighlighting()) {
426         return KTextEditor::Range::invalid();
427     }
428 
429     // first: get the wanted start line highlighted
430     ensureHighlighted(startLine);
431     Kate::TextLine startTextLine = plainLine(startLine);
432 
433     // return if no folding start!
434     if (!startTextLine->markedAsFoldingStart()) {
435         return KTextEditor::Range::invalid();
436     }
437 
438     // now: decided if indentation based folding or not!
439     if (startTextLine->markedAsFoldingStartIndentation()) {
440         // get our start indentation level
441         const int startIndentation = startTextLine->indentDepth(tabWidth());
442 
443         // search next line with indentation level <= our one
444         int lastLine = startLine + 1;
445         for (; lastLine < lines(); ++lastLine) {
446             // get line
447             Kate::TextLine textLine = plainLine(lastLine);
448 
449             // indentation higher than our start line? continue
450             if (startIndentation < textLine->indentDepth(tabWidth())) {
451                 continue;
452             }
453 
454             // empty line? continue
455             if (m_highlight->isEmptyLine(textLine.data())) {
456                 continue;
457             }
458 
459             // else, break
460             break;
461         }
462 
463         // lastLine is always one too much
464         --lastLine;
465 
466         // backtrack all empty lines, we don't want to add them to the fold!
467         while (lastLine > startLine) {
468             if (m_highlight->isEmptyLine(plainLine(lastLine).data())) {
469                 --lastLine;
470             } else {
471                 break;
472             }
473         }
474 
475         // we shall not fold one-liners
476         if (lastLine == startLine) {
477             return KTextEditor::Range::invalid();
478         }
479 
480         // be done now
481         return KTextEditor::Range(KTextEditor::Cursor(startLine, 0), KTextEditor::Cursor(lastLine, plainLine(lastLine)->length()));
482     }
483 
484     // 'normal' attribute based folding, aka token based like '{' BLUB '}'
485 
486     // first step: search the first region type, that stays open for the start line
487     short openedRegionType = 0;
488     int openedRegionOffset = -1;
489     {
490         // mapping of type to "first" offset of it and current number of not matched openings
491         QHash<short, QPair<int, int>> foldingStartToOffsetAndCount;
492 
493         // walk over all attributes of the line and compute the matchings
494         const auto &startLineAttributes = startTextLine->foldings();
495         for (size_t i = 0; i < startLineAttributes.size(); ++i) {
496             // folding close?
497             if (startLineAttributes[i].foldingValue < 0) {
498                 // search for this type, try to decrement counter, perhaps erase element!
499                 QHash<short, QPair<int, int>>::iterator end = foldingStartToOffsetAndCount.find(-startLineAttributes[i].foldingValue);
500                 if (end != foldingStartToOffsetAndCount.end()) {
501                     if (end.value().second > 1) {
502                         --(end.value().second);
503                     } else {
504                         foldingStartToOffsetAndCount.erase(end);
505                     }
506                 }
507             }
508 
509             // folding open?
510             if (startLineAttributes[i].foldingValue > 0) {
511                 // search for this type, either insert it, with current offset or increment counter!
512                 QHash<short, QPair<int, int>>::iterator start = foldingStartToOffsetAndCount.find(startLineAttributes[i].foldingValue);
513                 if (start != foldingStartToOffsetAndCount.end()) {
514                     ++(start.value().second);
515                 } else {
516                     foldingStartToOffsetAndCount.insert(startLineAttributes[i].foldingValue, qMakePair(startLineAttributes[i].offset, 1));
517                 }
518             }
519         }
520 
521         // compute first type with offset
522         QHashIterator<short, QPair<int, int>> hashIt(foldingStartToOffsetAndCount);
523         while (hashIt.hasNext()) {
524             hashIt.next();
525             if (openedRegionOffset == -1 || hashIt.value().first < openedRegionOffset) {
526                 openedRegionType = hashIt.key();
527                 openedRegionOffset = hashIt.value().first;
528             }
529         }
530     }
531 
532     // no opening region found, bad, nothing to do
533     if (openedRegionType == 0) {
534         return KTextEditor::Range::invalid();
535     }
536 
537     // second step: search for matching end region marker!
538     int countOfOpenRegions = 1;
539     for (int line = startLine + 1; line < lines(); ++line) {
540         // ensure line is highlighted
541         ensureHighlighted(line);
542         Kate::TextLine textLine = plainLine(line);
543 
544         // search for matching end marker
545         const auto &lineAttributes = textLine->foldings();
546         for (size_t i = 0; i < lineAttributes.size(); ++i) {
547             // matching folding close?
548             if (lineAttributes[i].foldingValue == -openedRegionType) {
549                 --countOfOpenRegions;
550 
551                 // end reached?
552                 // compute resulting range!
553                 if (countOfOpenRegions == 0) {
554                     // Don't return a valid range without content!
555                     if (line - startLine == 1) {
556                         return KTextEditor::Range::invalid();
557                     }
558 
559                     // return computed range
560                     return KTextEditor::Range(KTextEditor::Cursor(startLine, openedRegionOffset), KTextEditor::Cursor(line, lineAttributes[i].offset));
561                 }
562             }
563 
564             // matching folding open?
565             if (lineAttributes[i].foldingValue == openedRegionType) {
566                 ++countOfOpenRegions;
567             }
568         }
569     }
570 
571     // if we arrive here, the opened range spans to the end of the document!
572     return KTextEditor::Range(KTextEditor::Cursor(startLine, openedRegionOffset), KTextEditor::Cursor(lines() - 1, plainLine(lines() - 1)->length()));
573 }
574