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

source code of ktexteditor/src/document/katebuffer.cpp