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