KTextEditor

katetextbuffer.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6#include "config.h"
7
8#include "katetextbuffer.h"
9#include "katetextloader.h"
10
11#include "katedocument.h"
12
13// this is unfortunate, but needed for performance
14#include "katepartdebug.h"
15#include "kateview.h"
16
17#ifndef Q_OS_WIN
18#include <cerrno>
19#include <unistd.h>
20// sadly there seems to be no possibility in Qt to determine detailed error
21// codes about e.g. file open errors, so we need to resort to evaluating
22// errno directly on platforms that support this
23#define CAN_USE_ERRNO
24#endif
25
26#include <QBuffer>
27#include <QCryptographicHash>
28#include <QFile>
29#include <QFileInfo>
30#include <QScopeGuard>
31#include <QStandardPaths>
32#include <QStringEncoder>
33#include <QTemporaryFile>
34
35#if HAVE_KAUTH
36#include "katesecuretextbuffer_p.h"
37#include <KAuth/Action>
38#include <KAuth/ExecuteJob>
39#endif
40
41#if 0
42#define BUFFER_DEBUG qCDebug(LOG_KTE)
43#else
44#define BUFFER_DEBUG \
45 if (0) \
46 qCDebug(LOG_KTE)
47#endif
48
49namespace Kate
50{
53 , m_document(parent)
54 , m_history(*this)
55 , m_lines(0)
56 , m_revision(-1)
57 , m_editingTransactions(0)
58 , m_editingLastRevision(0)
59 , m_editingLastLines(0)
60 , m_editingMinimalLineChanged(-1)
61 , m_editingMaximalLineChanged(-1)
62 , m_encodingProberType(KEncodingProber::Universal)
63 , m_generateByteOrderMark(false)
64 , m_endOfLineMode(eolUnix)
65 , m_lineLengthLimit(4096)
66 , m_alwaysUseKAuthForSave(alwaysUseKAuth)
67{
68 // create initial state, this will set m_revision to 0
69 clear();
70}
71
73{
74 // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect
75 m_document = nullptr;
76
77 // not allowed during editing
78 Q_ASSERT(m_editingTransactions == 0);
79
80 // invalidate all moving stuff
81 std::vector<Kate::TextRange *> rangesWithFeedback;
82 for (auto b : m_blocks) {
83 auto cursors = std::move(b->m_cursors);
84 for (auto it = cursors.begin(); it != cursors.end(); ++it) {
85 auto cursor = *it;
86 // update the block
87 cursor->m_block = nullptr;
88 cursor->m_line = cursor->m_column = -1;
89 cursor->m_buffer = nullptr;
90 if (auto r = cursor->kateRange()) {
91 r->m_buffer = nullptr;
92 if (r->feedback()) {
93 rangesWithFeedback.push_back(r);
94 }
95 }
96 }
97 }
98
99 // uniquify ranges
100 std::sort(rangesWithFeedback.begin(), rangesWithFeedback.end());
101 auto it = std::unique(rangesWithFeedback.begin(), rangesWithFeedback.end());
102 std::for_each(rangesWithFeedback.begin(), it, [](Kate::TextRange *range) {
103 range->feedback()->rangeInvalid(range);
104 });
105
106 // clean out all cursors and lines, only cursors belonging to range will survive
107 for (TextBlock *block : m_blocks) {
108 block->clearLines();
109 }
110
111 // delete all blocks, now that all cursors are really deleted
112 // else asserts in destructor of blocks will fail!
113 qDeleteAll(m_blocks);
114 m_blocks.clear();
115}
116
118{
119 std::vector<Kate::TextRange *> ranges;
120 ranges.reserve(m_blocks.size());
121 for (TextBlock *block : m_blocks) {
122 for (auto cursor : block->m_cursors) {
123 if (cursor->kateRange()) {
124 ranges.push_back(cursor->kateRange());
125 }
126 }
127 }
128 // uniquify ranges
129 std::sort(ranges.begin(), ranges.end());
130 auto it = std::unique(ranges.begin(), ranges.end());
131 std::for_each(ranges.begin(), it, [](Kate::TextRange *range) {
132 range->setRange({KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()});
133 });
134}
135
137{
138 // not allowed during editing
139 Q_ASSERT(m_editingTransactions == 0);
140
141 m_multilineRanges.clear();
143
144 // new block for empty buffer
145 TextBlock *newBlock = new TextBlock(this, 0);
146 newBlock->appendLine(QString());
147
148 // clean out all cursors and lines, move them to newBlock if not belonging to a range
149 for (TextBlock *block : std::as_const(m_blocks)) {
150 auto cursors = std::move(block->m_cursors);
151 for (auto it = cursors.begin(); it != cursors.end(); ++it) {
152 auto cursor = *it;
153 if (!cursor->kateRange()) {
154 // update the block
155 cursor->m_block = newBlock;
156 // move the cursor into the target block
157 cursor->m_line = cursor->m_column = 0;
158 newBlock->m_cursors.push_back(cursor);
159 // remove it and advance to next element
160 }
161 // skip cursors with ranges, we need to invalidate the ranges later
162 }
163 block->clearLines();
164 }
165 std::sort(newBlock->m_cursors.begin(), newBlock->m_cursors.end());
166
167 // kill all buffer blocks
168 qDeleteAll(m_blocks);
169 // insert one block with one empty line
170 m_blocks = {newBlock};
171 m_startLines = {0};
172 m_blockSizes = {1};
173
174 // reset lines and last used block
175 m_lines = 1;
176
177 // increment revision, we did reset it here in the past
178 // that is no good idea as we can mix up content variants after an reload
179 ++m_revision;
180
181 // reset bom detection
182 m_generateByteOrderMark = false;
183
184 // reset the filter device
185 m_mimeTypeForFilterDev = QStringLiteral("text/plain");
186
187 // clear edit history
188 m_history.clear();
189
190 // we got cleared
191 Q_EMIT cleared();
192}
193
195{
196 // get block, this will assert on invalid line
197 int blockIndex = blockForLine(line);
198
199 // get line
200 return m_blocks.at(blockIndex)->line(line - m_startLines[blockIndex]);
201}
202
203void TextBuffer::setLineMetaData(int line, const TextLine &textLine)
204{
205 // get block, this will assert on invalid line
206 int blockIndex = blockForLine(line);
207
208 // get line
209 return m_blocks.at(blockIndex)->setLineMetaData(line - m_startLines[blockIndex], textLine);
210}
211
213{
214 if ((c.line() < 0) || (c.line() >= lines())) {
215 return -1;
216 }
217
218 int off = 0;
219 const int blockIndex = blockForLine(c.line());
220 for (auto it = m_blockSizes.begin(), end = m_blockSizes.begin() + blockIndex; it != end; ++it) {
221 off += *it;
222 }
223
224 auto block = m_blocks[blockIndex];
225 int start = block->startLine();
226 int end = start + block->lines();
227 for (int line = start; line < end; ++line) {
228 if (line >= c.line()) {
229 off += qMin(c.column(), block->lineLength(line));
230 return off;
231 }
232 off += block->lineLength(line) + 1;
233 }
234
235 Q_ASSERT(false);
236 return -1;
237}
238
240{
241 if (offset >= 0) {
242 int off = 0;
243 int blockIdx = 0;
244 for (int blockSize : m_blockSizes) {
245 if (off + blockSize < offset) {
246 off += blockSize;
247 } else {
248 auto block = m_blocks[blockIdx];
249 const int lines = block->lines();
250 int start = block->startLine();
251 int end = start + lines;
252 for (int line = start; line < end; ++line) {
253 const int len = block->lineLength(line);
254 if (off + len >= offset) {
255 return KTextEditor::Cursor(line, offset - off);
256 }
257 off += len + 1;
258 }
259 }
260 blockIdx++;
261 }
262 }
264}
265
267{
269 qsizetype size = 0;
270 for (int blockSize : m_blockSizes) {
271 size += blockSize;
272 }
273 text.reserve(size);
274 size -= 1; // remove -1, last newline
275
276 // combine all blocks
277 for (TextBlock *block : m_blocks) {
278 block->text(text);
279 }
280 text.chop(1); // remove last \n
281
282 Q_ASSERT(size == text.size());
283 return text;
284}
285
287{
288 // increment transaction counter
289 ++m_editingTransactions;
290
291 // if not first running transaction, do nothing
292 if (m_editingTransactions > 1) {
293 return false;
294 }
295
296 // reset information about edit...
297 m_editingLastRevision = m_revision;
298 m_editingLastLines = m_lines;
299 m_editingMinimalLineChanged = -1;
300 m_editingMaximalLineChanged = -1;
301
302 // transaction has started
303 Q_EMIT m_document->KTextEditor::Document::editingStarted(m_document);
304
305 // first transaction started
306 return true;
307}
308
310{
311 // only allowed if still transactions running
312 Q_ASSERT(m_editingTransactions > 0);
313
314 // decrement counter
315 --m_editingTransactions;
316
317 // if not last running transaction, do nothing
318 if (m_editingTransactions > 0) {
319 return false;
320 }
321
322 // assert that if buffer changed, the line ranges are set and valid!
323 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1));
324 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged));
325 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines));
326 Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines));
327
328 // transaction has finished
329 Q_EMIT m_document->KTextEditor::Document::editingFinished(m_document);
330
331 // last transaction finished
332 return true;
333}
334
336{
337 // debug output for REAL low-level debugging
338 BUFFER_DEBUG << "wrapLine" << position;
339
340 // only allowed if editing transaction running
341 Q_ASSERT(m_editingTransactions > 0);
342
343 // get block, this will assert on invalid line
344 int blockIndex = blockForLine(position.line());
345
346 // let the block handle the wrapLine
347 // this can only lead to one more line in this block
348 // no other blocks will change
349 // this call will trigger fixStartLines
350 ++m_lines; // first alter the line counter, as functions called will need the valid one
351 m_blocks.at(blockIndex)->wrapLine(position, blockIndex);
352 m_blockSizes[blockIndex] += 1;
353
354 // remember changes
355 ++m_revision;
356
357 // update changed line interval
358 if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
359 m_editingMinimalLineChanged = position.line();
360 }
361
362 if (position.line() <= m_editingMaximalLineChanged) {
363 ++m_editingMaximalLineChanged;
364 } else {
365 m_editingMaximalLineChanged = position.line() + 1;
366 }
367
368 // balance the changed block if needed
369 balanceBlock(blockIndex);
370
371 // emit signal about done change
372 Q_EMIT m_document->KTextEditor::Document::lineWrapped(m_document, position);
373}
374
376{
377 // debug output for REAL low-level debugging
378 BUFFER_DEBUG << "unwrapLine" << line;
379
380 // only allowed if editing transaction running
381 Q_ASSERT(m_editingTransactions > 0);
382
383 // line 0 can't be unwrapped
384 Q_ASSERT(line > 0);
385
386 // get block, this will assert on invalid line
387 int blockIndex = blockForLine(line);
388
389 // is this the first line in the block?
390 const int blockStartLine = m_startLines[blockIndex];
391 const bool firstLineInBlock = line == blockStartLine;
392
393 // let the block handle the unwrapLine
394 // this can either lead to one line less in this block or the previous one
395 // the previous one could even end up with zero lines
396 // this call will trigger fixStartLines
397
398 m_blocks.at(blockIndex)
399 ->unwrapLine(line - blockStartLine, (blockIndex > 0) ? m_blocks.at(blockIndex - 1) : nullptr, firstLineInBlock ? (blockIndex - 1) : blockIndex);
400 --m_lines;
401
402 // decrement index for later fixup, if we modified the block in front of the found one
403 if (firstLineInBlock) {
404 --blockIndex;
405 }
406
407 // remember changes
408 ++m_revision;
409
410 // update changed line interval
411 if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
412 m_editingMinimalLineChanged = line - 1;
413 }
414
415 if (line <= m_editingMaximalLineChanged) {
416 --m_editingMaximalLineChanged;
417 } else {
418 m_editingMaximalLineChanged = line - 1;
419 }
420
421 // balance the changed block if needed
422 balanceBlock(blockIndex);
423
424 // emit signal about done change
425 Q_EMIT m_document->KTextEditor::Document::lineUnwrapped(m_document, line);
426}
427
429{
430 // debug output for REAL low-level debugging
431 BUFFER_DEBUG << "insertText" << position << text;
432
433 // only allowed if editing transaction running
434 Q_ASSERT(m_editingTransactions > 0);
435
436 // skip work, if no text to insert
437 if (text.isEmpty()) {
438 return;
439 }
440
441 // get block, this will assert on invalid line
442 int blockIndex = blockForLine(position.line());
443
444 // let the block handle the insertText
445 m_blocks.at(blockIndex)->insertText(position, text);
446 m_blockSizes[blockIndex] += text.size();
447
448 // remember changes
449 ++m_revision;
450
451 // update changed line interval
452 if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
453 m_editingMinimalLineChanged = position.line();
454 }
455
456 if (position.line() > m_editingMaximalLineChanged) {
457 m_editingMaximalLineChanged = position.line();
458 }
459
460 // emit signal about done change
461 Q_EMIT m_document->KTextEditor::Document::textInserted(m_document, position, text);
462}
463
465{
466 // debug output for REAL low-level debugging
467 BUFFER_DEBUG << "removeText" << range;
468
469 // only allowed if editing transaction running
470 Q_ASSERT(m_editingTransactions > 0);
471
472 // only ranges on one line are supported
473 Q_ASSERT(range.start().line() == range.end().line());
474
475 // start column <= end column and >= 0
476 Q_ASSERT(range.start().column() <= range.end().column());
477 Q_ASSERT(range.start().column() >= 0);
478
479 // skip work, if no text to remove
480 if (range.isEmpty()) {
481 return;
482 }
483
484 // get block, this will assert on invalid line
485 int blockIndex = blockForLine(range.start().line());
486
487 // let the block handle the removeText, retrieve removed text
489 m_blocks.at(blockIndex)->removeText(range, text);
490 m_blockSizes[blockIndex] -= text.size();
491
492 // remember changes
493 ++m_revision;
494
495 // update changed line interval
496 if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
497 m_editingMinimalLineChanged = range.start().line();
498 }
499
500 if (range.start().line() > m_editingMaximalLineChanged) {
501 m_editingMaximalLineChanged = range.start().line();
502 }
503
504 // emit signal about done change
505 Q_EMIT m_document->KTextEditor::Document::textRemoved(m_document, range, text);
506}
507
508int TextBuffer::blockForLine(int line) const
509{
510 // only allow valid lines
511 if ((line < 0) || (line >= lines())) {
512 qFatal("out of range line requested in text buffer (%d out of [0, %d])", line, lines());
513 }
514
515 size_t b = line / BufferBlockSize;
516 if (b >= m_blocks.size()) {
517 b = m_blocks.size() - 1;
518 }
519
520 if (m_startLines[b] <= line && line < m_startLines[b] + m_blocks[b]->lines()) {
521 return b;
522 }
523
524 if (m_startLines[b] > line) {
525 for (int i = b - 1; i >= 0; --i) {
526 if (m_startLines[i] <= line && line < m_startLines[i] + m_blocks[i]->lines()) {
527 return i;
528 }
529 }
530 }
531
532 if (m_startLines[b] < line || (m_blocks[b]->lines() == 0)) {
533 for (size_t i = b + 1; i < m_blocks.size(); ++i) {
534 if (m_startLines[i] <= line && line < m_startLines[i] + m_blocks[i]->lines()) {
535 return i;
536 }
537 }
538 }
539
540 qFatal("line requested in text buffer (%d out of [0, %d[), no block found", line, lines());
541 return -1;
542}
543
544void TextBuffer::fixStartLines(int startBlock, int value)
545{
546 // only allow valid start block
547 Q_ASSERT(startBlock >= 0);
548 Q_ASSERT(startBlock <= (int)m_startLines.size());
549 // fixup block
550 for (auto it = m_startLines.begin() + startBlock, end = m_startLines.end(); it != end; ++it) {
551 // move start line by given value
552 *it += value;
553 }
554}
555
556void TextBuffer::balanceBlock(int index)
557{
558 auto check = qScopeGuard([this] {
559 if (!(m_blocks.size() == m_startLines.size() && m_blocks.size() == m_blockSizes.size())) {
560 qFatal("blocks/startlines/blocksizes are not equal in size!");
561 }
562 });
563
564 // two cases, too big or too small block
565 TextBlock *blockToBalance = m_blocks.at(index);
566
567 // first case, too big one, split it
568 if (blockToBalance->lines() >= 2 * BufferBlockSize) {
569 // half the block
570 int halfSize = blockToBalance->lines() / 2;
571
572 // create and insert new block after current one, already set right start line
573 const int newBlockStartLine = m_startLines[index] + halfSize;
574 TextBlock *newBlock = new TextBlock(this, index + 1);
575 m_blocks.insert(m_blocks.begin() + index + 1, newBlock);
576 m_startLines.insert(m_startLines.begin() + index + 1, newBlockStartLine);
577 m_blockSizes.insert(m_blockSizes.begin() + index + 1, 0);
578
579 // adjust block indexes
580 for (auto it = m_blocks.begin() + index, end = m_blocks.end(); it != end; ++it) {
581 (*it)->setBlockIndex(index++);
582 }
583
584 blockToBalance->splitBlock(halfSize, newBlock);
585
586 // split is done
587 return;
588 }
589
590 // second case: possibly too small block
591
592 // if only one block, no chance to unite
593 // same if this is first block, we always append to previous one
594 if (index == 0) {
595 // remove the block if its empty
596 if (blockToBalance->lines() == 0) {
597 m_blocks.erase(m_blocks.begin());
598 m_startLines.erase(m_startLines.begin());
599 m_blockSizes.erase(m_blockSizes.begin());
600 Q_ASSERT(m_startLines[0] == 0);
601 for (auto it = m_blocks.begin(), end = m_blocks.end(); it != end; ++it) {
602 (*it)->setBlockIndex(index++);
603 }
604 }
605 return;
606 }
607
608 // block still large enough, do nothing
609 if (2 * blockToBalance->lines() > BufferBlockSize) {
610 return;
611 }
612
613 // unite small block with predecessor
614 TextBlock *targetBlock = m_blocks.at(index - 1);
615
616 // merge block
617 blockToBalance->mergeBlock(targetBlock);
618 m_blockSizes[index - 1] += m_blockSizes[index];
619
620 // delete old block
621 delete blockToBalance;
622 m_blocks.erase(m_blocks.begin() + index);
623 m_startLines.erase(m_startLines.begin() + index);
624 m_blockSizes.erase(m_blockSizes.begin() + index);
625
626 for (auto it = m_blocks.begin() + index, end = m_blocks.end(); it != end; ++it) {
627 (*it)->setBlockIndex(index++);
628 }
629
630 Q_ASSERT(index == (int)m_blocks.size());
631}
632
633void TextBuffer::debugPrint(const QString &title) const
634{
635 // print header with title
636 printf("%s (lines: %d)\n", qPrintable(title), m_lines);
637
638 // print all blocks
639 for (size_t i = 0; i < m_blocks.size(); ++i) {
640 m_blocks.at(i)->debugPrint(i);
641 }
642}
643
644bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec)
645{
646 // fallback codec must exist
647 Q_ASSERT(!m_fallbackTextCodec.isEmpty());
648
649 // codec must be set!
650 Q_ASSERT(!m_textCodec.isEmpty());
651
652 // first: clear buffer in any case!
653 clear();
654
655 // construct the file loader for the given file, with correct prober type
656 Kate::TextLoader file(filename, m_encodingProberType, m_lineLengthLimit);
657
658 // triple play, maximal three loading rounds
659 // 0) use the given encoding, be done, if no encoding errors happen
660 // 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done
661 // 2) use fallback encoding, be done, if no encoding errors happen
662 // 3) use again given encoding, be done in any case
663 for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) {
664 // kill all blocks beside first one
665 for (size_t b = 1; b < m_blocks.size(); ++b) {
666 TextBlock *block = m_blocks.at(b);
667 block->clearLines();
668 delete block;
669 }
670 m_blocks.resize(1);
671 m_startLines.resize(1);
672 m_blockSizes.resize(1);
673
674 // remove lines in first block
675 m_blocks.back()->clearLines();
676 m_startLines.back() = 0;
677 m_blockSizes.back() = 0;
678 m_lines = 0;
679
680 // reset error flags
681 tooLongLinesWrapped = false;
682 longestLineLoaded = 0;
683
684 // try to open file, with given encoding
685 // in round 0 + 3 use the given encoding from user
686 // in round 1 use 0, to trigger detection
687 // in round 2 use fallback
688 QString codec = m_textCodec;
689 if (i == 1) {
690 codec.clear();
691 } else if (i == 2) {
692 codec = m_fallbackTextCodec;
693 }
694
695 if (!file.open(codec)) {
696 // create one dummy textline, in any case
697 m_blocks.back()->appendLine(QString());
698 m_lines++;
699 m_blockSizes[0] = 1;
700 return false;
701 }
702
703 // read in all lines...
704 encodingErrors = false;
705 while (!file.eof()) {
706 // read line
707 int offset = 0;
708 int length = 0;
709 bool currentError = !file.readLine(offset, length, tooLongLinesWrapped, longestLineLoaded);
710 encodingErrors = encodingErrors || currentError;
711
712 // bail out on encoding error, if not last round!
713 if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) {
714 BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << file.textCodec();
715 break;
716 }
717
718 // ensure blocks aren't too large
719 if (m_blocks.back()->lines() >= BufferBlockSize) {
720 int index = (int)m_blocks.size();
721 int startLine = m_blocks.back()->startLine() + m_blocks.back()->lines();
722 m_blocks.push_back(new TextBlock(this, index));
723 m_startLines.push_back(startLine);
724 m_blockSizes.push_back(0);
725 }
726
727 // append line to last block
728 m_blocks.back()->appendLine(QString(file.unicode() + offset, length));
729 m_blockSizes.back() += length + 1;
730 ++m_lines;
731 }
732
733 // if no encoding error, break out of reading loop
734 if (!encodingErrors) {
735 // remember used codec, might change bom setting
736 setTextCodec(file.textCodec());
737 break;
738 }
739 }
740
741 // save checksum of file on disk
742 setDigest(file.digest());
743
744 // remember if BOM was found
745 if (file.byteOrderMarkFound()) {
747 }
748
749 // remember eol mode, if any found in file
750 if (file.eol() != eolUnknown) {
751 setEndOfLineMode(file.eol());
752 }
753
754 // remember mime type for filter device
755 m_mimeTypeForFilterDev = file.mimeTypeForFilterDev();
756
757 // assert that one line is there!
758 Q_ASSERT(m_lines > 0);
759
760 // report CODEC + ERRORS
761 BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec << (encodingErrors ? "with" : "without") << "encoding errors";
762
763 // report BOM
764 BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find") << "byte order mark";
765
766 // report filter device mime-type
767 BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev;
768
769 // emit success
770 Q_EMIT loaded(filename, encodingErrors);
771
772 // file loading worked, modulo encoding problems
773 return true;
774}
775
777{
778 return m_digest;
779}
780
782{
783 m_digest = checksum;
784}
785
787{
788 m_textCodec = codec;
789
790 // enforce bom for some encodings
791 if (const auto setEncoding = QStringConverter::encodingForName(m_textCodec.toUtf8().constData())) {
792 for (const auto encoding : {QStringConverter::Utf16,
798 if (setEncoding == encoding) {
800 break;
801 }
802 }
803 }
804}
805
806bool TextBuffer::save(const QString &filename)
807{
808 // codec must be set, else below we fail!
809 Q_ASSERT(!m_textCodec.isEmpty());
810
811 // ensure we do not kill symlinks, see bug 498589
812 auto realFile = filename;
813 if (const auto realFileResolved = QFileInfo(realFile).canonicalFilePath(); !realFileResolved.isEmpty()) {
814 realFile = realFileResolved;
815 }
816
817 const auto saveRes = saveBufferUnprivileged(realFile);
818 if (saveRes == SaveResult::Failed) {
819 return false;
820 }
821 if (saveRes == SaveResult::MissingPermissions) {
822 // either unit-test mode or we're missing permissions to write to the
823 // file => use temporary file and try to use authhelper
824 if (!saveBufferEscalated(realFile)) {
825 return false;
826 }
827 }
828
829 // remember this revision as last saved
830 m_history.setLastSavedRevision();
831
832 // inform that we have saved the state
833 markModifiedLinesAsSaved();
834
835 // emit that file was saved and be done
836 Q_EMIT saved(filename);
837 return true;
838}
839
840bool TextBuffer::saveBuffer(const QString &filename, KCompressionDevice &saveFile)
841{
842 QStringEncoder encoder(m_textCodec.toUtf8().constData(), generateByteOrderMark() ? QStringConverter::Flag::WriteBom : QStringConverter::Flag::Default);
843
844 // our loved eol string ;)
845 QString eol = QStringLiteral("\n");
846 if (endOfLineMode() == eolDos) {
847 eol = QStringLiteral("\r\n");
848 } else if (endOfLineMode() == eolMac) {
849 eol = QStringLiteral("\r");
850 }
851
852 // just dump the lines out ;)
853 for (int i = 0; i < m_lines; ++i) {
854 // dump current line
855 saveFile.write(encoder.encode(line(i).text()));
856
857 // append correct end of line string
858 if ((i + 1) < m_lines) {
859 saveFile.write(encoder.encode(eol));
860 }
861
862 // early out on stream errors
863 if (saveFile.error() != QFileDevice::NoError) {
864 return false;
865 }
866 }
867
868 // TODO: this only writes bytes when there is text. This is a fine optimization for most cases, but this makes saving
869 // an empty file with the BOM set impossible (results to an empty file with 0 bytes, no BOM)
870
871 // close the file, we might want to read from underlying buffer below
872 saveFile.close();
873
874 // did save work?
875 if (saveFile.error() != QFileDevice::NoError) {
876 BUFFER_DEBUG << "Saving file " << filename << "failed with error" << saveFile.errorString();
877 return false;
878 }
879
880 return true;
881}
882
883TextBuffer::SaveResult TextBuffer::saveBufferUnprivileged(const QString &filename)
884{
885 if (m_alwaysUseKAuthForSave) {
886 // unit-testing mode, simulate we need privileges
887 return SaveResult::MissingPermissions;
888 }
889
890 // construct correct filter device
891 // we try to use the same compression as for opening
893 auto saveFile = std::make_unique<KCompressionDevice>(filename, type);
894
895 if (!saveFile->open(QIODevice::WriteOnly)) {
896#ifdef CAN_USE_ERRNO
897 if (errno != EACCES) {
898 return SaveResult::Failed;
899 }
900#endif
901 return SaveResult::MissingPermissions;
902 }
903
904 if (!saveBuffer(filename, *saveFile)) {
905 return SaveResult::Failed;
906 }
907
908 return SaveResult::Success;
909}
910
911bool TextBuffer::saveBufferEscalated(const QString &filename)
912{
913#if HAVE_KAUTH
914 // construct correct filter device
915 // we try to use the same compression as for opening
917 auto saveFile = std::make_unique<KCompressionDevice>(filename, type);
918 uint ownerId = -2;
919 uint groupId = -2;
920 std::unique_ptr<QIODevice> temporaryBuffer;
921
922 // Memorize owner and group.
923 const QFileInfo fileInfo(filename);
924 if (fileInfo.exists()) {
925 ownerId = fileInfo.ownerId();
926 groupId = fileInfo.groupId();
927 }
928
929 // if that fails we need more privileges to save this file
930 // -> we write to a temporary file and then send its path to KAuth action for privileged save
931 temporaryBuffer = std::make_unique<QBuffer>();
932
933 // open buffer for write and read (read is used for checksum computing and writing to temporary file)
934 if (!temporaryBuffer->open(QIODevice::ReadWrite)) {
935 return false;
936 }
937
938 // we are now saving to a temporary buffer with potential compression proxy
939 saveFile = std::make_unique<KCompressionDevice>(temporaryBuffer.get(), false, type);
940 if (!saveFile->open(QIODevice::WriteOnly)) {
941 return false;
942 }
943
944 if (!saveBuffer(filename, *saveFile)) {
945 return false;
946 }
947
948 // temporary buffer was used to save the file
949 // -> computing checksum
950 // -> saving to temporary file
951 // -> copying the temporary file to the original file location with KAuth action
952 QTemporaryFile tempFile;
953 if (!tempFile.open()) {
954 return false;
955 }
956
957 // go to QBuffer start
958 temporaryBuffer->seek(0);
959
960 // read contents of QBuffer and add them to checksum utility as well as to QTemporaryFile
961 char buffer[bufferLength];
962 qint64 read = -1;
963 QCryptographicHash cryptographicHash(SecureTextBuffer::checksumAlgorithm);
964 while ((read = temporaryBuffer->read(buffer, bufferLength)) > 0) {
965 cryptographicHash.addData(QByteArrayView(buffer, read));
966 if (tempFile.write(buffer, read) == -1) {
967 return false;
968 }
969 }
970 if (!tempFile.flush()) {
971 return false;
972 }
973
974 // prepare data for KAuth action
975 QVariantMap kAuthActionArgs;
976 kAuthActionArgs.insert(QStringLiteral("sourceFile"), tempFile.fileName());
977 kAuthActionArgs.insert(QStringLiteral("targetFile"), filename);
978 kAuthActionArgs.insert(QStringLiteral("checksum"), cryptographicHash.result());
979 kAuthActionArgs.insert(QStringLiteral("ownerId"), ownerId);
980 kAuthActionArgs.insert(QStringLiteral("groupId"), groupId);
981
982 // call save with elevated privileges
983 if (QStandardPaths::isTestModeEnabled()) {
984 // unit testing purposes only
985 if (!SecureTextBuffer::savefile(kAuthActionArgs).succeeded()) {
986 return false;
987 }
988 } else {
989 KAuth::Action kAuthSaveAction(QStringLiteral("org.kde.ktexteditor6.katetextbuffer.savefile"));
990 kAuthSaveAction.setHelperId(QStringLiteral("org.kde.ktexteditor6.katetextbuffer"));
991 kAuthSaveAction.setArguments(kAuthActionArgs);
992 KAuth::ExecuteJob *job = kAuthSaveAction.execute();
993 if (!job->exec()) {
994 return false;
995 }
996 }
997
998 return true;
999#else
1000 Q_UNUSED(filename);
1001 return false;
1002#endif
1003}
1004
1005void TextBuffer::notifyAboutRangeChange(KTextEditor::View *view, KTextEditor::LineRange lineRange, bool needsRepaint, TextRange *deleteRange)
1006{
1007 // ignore calls if no document is around
1008 if (!m_document) {
1009 return;
1010 }
1011
1012 // update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really
1013 // just create 20k ranges in a go and you wait seconds on a decent machine
1014 const QList<KTextEditor::View *> views = m_document->views();
1015 for (KTextEditor::View *curView : views) {
1016 // filter wrong views
1017 if (view && view != curView && !deleteRange) {
1018 continue;
1019 }
1020
1021 // notify view, it is really a kate view
1022 static_cast<KTextEditor::ViewPrivate *>(curView)->notifyAboutRangeChange(lineRange, needsRepaint, deleteRange);
1023 }
1024}
1025
1026void TextBuffer::markModifiedLinesAsSaved()
1027{
1028 for (TextBlock *block : std::as_const(m_blocks)) {
1029 block->markModifiedLinesAsSaved();
1030 }
1031}
1032
1033void TextBuffer::addMultilineRange(TextRange *range)
1034{
1035 auto it = std::find(m_multilineRanges.begin(), m_multilineRanges.end(), range);
1036 if (it == m_multilineRanges.end()) {
1037 m_multilineRanges.push_back(range);
1038 return;
1039 }
1040}
1041
1042void TextBuffer::removeMultilineRange(TextRange *range)
1043{
1044 m_multilineRanges.erase(std::remove(m_multilineRanges.begin(), m_multilineRanges.end(), range), m_multilineRanges.end());
1045}
1046
1047bool TextBuffer::hasMultlineRange(KTextEditor::MovingRange *range) const
1048{
1049 return std::find(m_multilineRanges.begin(), m_multilineRanges.end(), range) != m_multilineRanges.end();
1050}
1051
1052void TextBuffer::rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly, QList<TextRange *> &outRanges) const
1053{
1054 outRanges.clear();
1055 // get block, this will assert on invalid line
1056 const int blockIndex = blockForLine(line);
1057 m_blocks.at(blockIndex)->rangesForLine(line, view, rangesWithAttributeOnly, outRanges);
1058 // printf("Requested range for line %d, available %d\n", (int)line, (int)m_multilineRanges.size());
1059 for (TextRange *range : std::as_const(m_multilineRanges)) {
1060 if (rangesWithAttributeOnly && !range->hasAttribute()) {
1061 continue;
1062 }
1063
1064 // we want ranges for no view, but this one's attribute is only valid for views
1065 if (!view && range->attributeOnlyForViews()) {
1066 continue;
1067 }
1068
1069 // the range's attribute is not valid for this view
1070 if (range->view() && range->view() != view) {
1071 continue;
1072 }
1073
1074 // if line is in the range, ok
1075 if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) {
1076 outRanges.append(range);
1077 }
1078 }
1079 std::sort(outRanges.begin(), outRanges.end());
1080 outRanges.erase(std::unique(outRanges.begin(), outRanges.end()), outRanges.end());
1081}
1082}
1083
1084#include "moc_katetextbuffer.cpp"
static CompressionType compressionTypeForMimeType(const QString &mimetype)
QFileDevice::FileError error() const
void close() override
bool open(QIODevice::OpenMode mode) override
bool exec()
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Definition cursor.h:112
Backend of KTextEditor::Document related public KTextEditor interfaces.
An object representing lines from a start line to an end line.
Definition linerange.h:41
A range that is bound to a specific Document, and maintains its position.
virtual bool attributeOnlyForViews() const =0
Is this range's attribute only visible in views, not for example prints?
virtual View * view() const =0
Gets the active view for this range.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
void appendLine(const QString &textOfLine)
Append a new line with given text.
void clearLines()
Clear the lines.
virtual void removeText(KTextEditor::Range range)
Remove text at given range.
int cursorToOffset(KTextEditor::Cursor c) const
Retrieve offset in text for the given cursor position.
TextBuffer(KTextEditor::DocumentPrivate *parent, bool alwaysUseKAuth=false)
Construct an empty text buffer.
virtual bool save(const QString &filename)
Save the current buffer content to the given file.
TextLine line(int line) const
Retrieve a text line.
void cleared()
Buffer got cleared.
virtual void wrapLine(const KTextEditor::Cursor position)
Wrap line at given cursor position.
virtual void unwrapLine(int line)
Unwrap given line.
void setDigest(const QByteArray &checksum)
Set the checksum of this buffer.
bool editingChangedBuffer() const
Query information from the last editing transaction: was the content of the buffer changed?
~TextBuffer() override
Destruct the text buffer Virtual, we allow inheritance.
int lines() const
Lines currently stored in this buffer.
KTextEditor::Cursor offsetToCursor(int offset) const
Retrieve cursor in text for the given offset.
virtual void insertText(const KTextEditor::Cursor position, const QString &text)
Insert text at given cursor position.
void setLineMetaData(int line, const TextLine &textLine)
Transfer all non text attributes for the given line from the given text line to the one in the buffer...
void invalidateRanges()
Invalidate all ranges in this buffer.
void setEndOfLineMode(EndOfLineMode endOfLineMode)
Set end of line mode for this buffer, not allowed to be set to unknown.
void loaded(const QString &filename, bool encodingErrors)
Buffer loaded successfully a file.
void saved(const QString &filename)
Buffer saved successfully a file.
void setTextCodec(const QString &codec)
Set codec for this buffer to use for load/save.
virtual bool load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec)
Load the given file.
virtual bool startEditing()
Start an editing transaction, the wrapLine/unwrapLine/insertText and removeText functions are only al...
virtual void clear()
Clears the buffer, reverts to initial empty state.
virtual bool finishEditing()
Finish an editing transaction.
void setGenerateByteOrderMark(bool generateByteOrderMark)
Generate byte order mark on save.
KTEXTEDITOR_NO_EXPORT void addMultilineRange(TextRange *range)
Add/Remove a multiline range that spans multiple blocks.
void debugPrint(const QString &title) const
Debug output, print whole buffer content with line numbers and line length.
const QByteArray & digest() const
Checksum of the document on disk, set either through file loading in openFile() or in KTextEditor::Do...
QString text() const
Retrieve text of complete buffer.
Class representing a single text line.
File Loader, will handle reading of files + detecting encoding.
const QChar * unicode() const
internal Unicode data array
QString textCodec() const
Get codec for this loader.
bool eof() const
end of file reached?
const QString & mimeTypeForFilterDev() const
mime type used to create filter dev
bool readLine(int &offset, int &length, bool &tooLongLinesWrapped, int &longestLineLoaded)
read a line, return length + offset in Unicode data
bool open(const QString &codec)
open file with given codec
TextBuffer::EndOfLineMode eol() const
Detected end of line mode for this file.
bool byteOrderMarkFound() const
BOM found?
Class representing a 'clever' text range.
Q_SCRIPTABLE QString start(QString train="")
QVariant read(const QByteArray &data, int versionOverride=0)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
bool flush()
virtual bool seek(qint64 pos) override
QString errorString() const const
qint64 write(const QByteArray &data)
void append(QList< T > &&value)
iterator begin()
void clear()
iterator end()
iterator erase(const_iterator begin, const_iterator end)
QObject(QObject *parent)
Q_EMITQ_EMIT
QObject * parent() const const
void clear()
std::optional< Encoding > encodingForName(const char *name)
virtual QString fileName() const const override
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 12:00:11 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.