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{
52 : QObject(parent)
53 , m_document(parent)
54 , m_history(*this)
55 , m_lines(0)
56 , m_revision(0)
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
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
136void TextBuffer::clear()
137{
138 // not allowed during editing
139 Q_ASSERT(m_editingTransactions == 0);
140
141 m_multilineRanges.clear();
142 invalidateRanges();
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 // reset revision
178 m_revision = 0;
179
180 // reset bom detection
181 m_generateByteOrderMark = false;
182
183 // reset the filter device
184 m_mimeTypeForFilterDev = QStringLiteral("text/plain");
185
186 // clear edit history
187 m_history.clear();
188
189 // we got cleared
190 Q_EMIT cleared();
191}
192
193TextLine TextBuffer::line(int line) const
194{
195 // get block, this will assert on invalid line
196 int blockIndex = blockForLine(line);
197
198 // get line
199 return m_blocks.at(blockIndex)->line(line - m_startLines[blockIndex]);
200}
201
202void TextBuffer::setLineMetaData(int line, const TextLine &textLine)
203{
204 // get block, this will assert on invalid line
205 int blockIndex = blockForLine(line);
206
207 // get line
208 return m_blocks.at(blockIndex)->setLineMetaData(line - m_startLines[blockIndex], textLine);
209}
210
211int TextBuffer::cursorToOffset(KTextEditor::Cursor c) const
212{
213 if ((c.line() < 0) || (c.line() >= lines())) {
214 return -1;
215 }
216
217 int off = 0;
218 const int blockIndex = blockForLine(c.line());
219 for (auto it = m_blockSizes.begin(), end = m_blockSizes.begin() + blockIndex; it != end; ++it) {
220 off += *it;
221 }
222
223 auto block = m_blocks[blockIndex];
224 int start = block->startLine();
225 int end = start + block->lines();
226 for (int line = start; line < end; ++line) {
227 if (line >= c.line()) {
228 off += qMin(c.column(), block->lineLength(line));
229 return off;
230 }
231 off += block->lineLength(line) + 1;
232 }
233
234 Q_ASSERT(false);
235 return -1;
236}
237
238KTextEditor::Cursor TextBuffer::offsetToCursor(int offset) const
239{
240 if (offset >= 0) {
241 int off = 0;
242 int blockIdx = 0;
243 for (int blockSize : m_blockSizes) {
244 if (off + blockSize < offset) {
245 off += blockSize;
246 } else {
247 auto block = m_blocks[blockIdx];
248 const int lines = block->lines();
249 int start = block->startLine();
250 int end = start + lines;
251 for (int line = start; line < end; ++line) {
252 const int len = block->lineLength(line);
253 if (off + len >= offset) {
254 return KTextEditor::Cursor(line, offset - off);
255 }
256 off += len + 1;
257 }
258 }
259 blockIdx++;
260 }
261 }
263}
264
265QString TextBuffer::text() const
266{
267 QString text;
268 qsizetype size = 0;
269 for (int blockSize : m_blockSizes) {
270 size += blockSize;
271 }
272 text.reserve(size);
273 size -= 1; // remove -1, last newline
274
275 // combine all blocks
276 for (TextBlock *block : m_blocks) {
277 block->text(text);
278 }
279 text.chop(1); // remove last \n
280
281 Q_ASSERT(size == text.size());
282 return text;
283}
284
285bool TextBuffer::startEditing()
286{
287 // increment transaction counter
288 ++m_editingTransactions;
289
290 // if not first running transaction, do nothing
291 if (m_editingTransactions > 1) {
292 return false;
293 }
294
295 // reset information about edit...
296 m_editingLastRevision = m_revision;
297 m_editingLastLines = m_lines;
298 m_editingMinimalLineChanged = -1;
299 m_editingMaximalLineChanged = -1;
300
301 // transaction has started
302 Q_EMIT m_document->KTextEditor::Document::editingStarted(m_document);
303
304 // first transaction started
305 return true;
306}
307
308bool TextBuffer::finishEditing()
309{
310 // only allowed if still transactions running
311 Q_ASSERT(m_editingTransactions > 0);
312
313 // decrement counter
314 --m_editingTransactions;
315
316 // if not last running transaction, do nothing
317 if (m_editingTransactions > 0) {
318 return false;
319 }
320
321 // assert that if buffer changed, the line ranges are set and valid!
322 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1));
323 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged));
324 Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines));
325 Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines));
326
327 // transaction has finished
328 Q_EMIT m_document->KTextEditor::Document::editingFinished(m_document);
329
330 // last transaction finished
331 return true;
332}
333
334void TextBuffer::wrapLine(const KTextEditor::Cursor position)
335{
336 // debug output for REAL low-level debugging
337 BUFFER_DEBUG << "wrapLine" << position;
338
339 // only allowed if editing transaction running
340 Q_ASSERT(m_editingTransactions > 0);
341
342 // get block, this will assert on invalid line
343 int blockIndex = blockForLine(position.line());
344
345 // let the block handle the wrapLine
346 // this can only lead to one more line in this block
347 // no other blocks will change
348 // this call will trigger fixStartLines
349 ++m_lines; // first alter the line counter, as functions called will need the valid one
350 m_blocks.at(blockIndex)->wrapLine(position, blockIndex);
351 m_blockSizes[blockIndex] += 1;
352
353 // remember changes
354 ++m_revision;
355
356 // update changed line interval
357 if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
358 m_editingMinimalLineChanged = position.line();
359 }
360
361 if (position.line() <= m_editingMaximalLineChanged) {
362 ++m_editingMaximalLineChanged;
363 } else {
364 m_editingMaximalLineChanged = position.line() + 1;
365 }
366
367 // balance the changed block if needed
368 balanceBlock(blockIndex);
369
370 // emit signal about done change
371 Q_EMIT m_document->KTextEditor::Document::lineWrapped(m_document, position);
372}
373
374void TextBuffer::unwrapLine(int line)
375{
376 // debug output for REAL low-level debugging
377 BUFFER_DEBUG << "unwrapLine" << line;
378
379 // only allowed if editing transaction running
380 Q_ASSERT(m_editingTransactions > 0);
381
382 // line 0 can't be unwrapped
383 Q_ASSERT(line > 0);
384
385 // get block, this will assert on invalid line
386 int blockIndex = blockForLine(line);
387
388 // is this the first line in the block?
389 const int blockStartLine = m_startLines[blockIndex];
390 const bool firstLineInBlock = line == blockStartLine;
391
392 // let the block handle the unwrapLine
393 // this can either lead to one line less in this block or the previous one
394 // the previous one could even end up with zero lines
395 // this call will trigger fixStartLines
396
397 m_blocks.at(blockIndex)
398 ->unwrapLine(line - blockStartLine, (blockIndex > 0) ? m_blocks.at(blockIndex - 1) : nullptr, firstLineInBlock ? (blockIndex - 1) : blockIndex);
399 --m_lines;
400
401 // decrement index for later fixup, if we modified the block in front of the found one
402 if (firstLineInBlock) {
403 --blockIndex;
404 }
405
406 // remember changes
407 ++m_revision;
408
409 // update changed line interval
410 if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
411 m_editingMinimalLineChanged = line - 1;
412 }
413
414 if (line <= m_editingMaximalLineChanged) {
415 --m_editingMaximalLineChanged;
416 } else {
417 m_editingMaximalLineChanged = line - 1;
418 }
419
420 // balance the changed block if needed
421 balanceBlock(blockIndex);
422
423 // emit signal about done change
424 Q_EMIT m_document->KTextEditor::Document::lineUnwrapped(m_document, line);
425}
426
427void TextBuffer::insertText(const KTextEditor::Cursor position, const QString &text)
428{
429 // debug output for REAL low-level debugging
430 BUFFER_DEBUG << "insertText" << position << text;
431
432 // only allowed if editing transaction running
433 Q_ASSERT(m_editingTransactions > 0);
434
435 // skip work, if no text to insert
436 if (text.isEmpty()) {
437 return;
438 }
439
440 // get block, this will assert on invalid line
441 int blockIndex = blockForLine(position.line());
442
443 // let the block handle the insertText
444 m_blocks.at(blockIndex)->insertText(position, text);
445 m_blockSizes[blockIndex] += text.size();
446
447 // remember changes
448 ++m_revision;
449
450 // update changed line interval
451 if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
452 m_editingMinimalLineChanged = position.line();
453 }
454
455 if (position.line() > m_editingMaximalLineChanged) {
456 m_editingMaximalLineChanged = position.line();
457 }
458
459 // emit signal about done change
460 Q_EMIT m_document->KTextEditor::Document::textInserted(m_document, position, text);
461}
462
463void TextBuffer::removeText(KTextEditor::Range range)
464{
465 // debug output for REAL low-level debugging
466 BUFFER_DEBUG << "removeText" << range;
467
468 // only allowed if editing transaction running
469 Q_ASSERT(m_editingTransactions > 0);
470
471 // only ranges on one line are supported
472 Q_ASSERT(range.start().line() == range.end().line());
473
474 // start column <= end column and >= 0
475 Q_ASSERT(range.start().column() <= range.end().column());
476 Q_ASSERT(range.start().column() >= 0);
477
478 // skip work, if no text to remove
479 if (range.isEmpty()) {
480 return;
481 }
482
483 // get block, this will assert on invalid line
484 int blockIndex = blockForLine(range.start().line());
485
486 // let the block handle the removeText, retrieve removed text
487 QString text;
488 m_blocks.at(blockIndex)->removeText(range, text);
489 m_blockSizes[blockIndex] -= text.size();
490
491 // remember changes
492 ++m_revision;
493
494 // update changed line interval
495 if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) {
496 m_editingMinimalLineChanged = range.start().line();
497 }
498
499 if (range.start().line() > m_editingMaximalLineChanged) {
500 m_editingMaximalLineChanged = range.start().line();
501 }
502
503 // emit signal about done change
504 Q_EMIT m_document->KTextEditor::Document::textRemoved(m_document, range, text);
505}
506
507int TextBuffer::blockForLine(int line) const
508{
509 // only allow valid lines
510 if ((line < 0) || (line >= lines())) {
511 qFatal("out of range line requested in text buffer (%d out of [0, %d])", line, lines());
512 }
513
514 size_t b = line / BufferBlockSize;
515 if (b >= m_blocks.size()) {
516 b = m_blocks.size() - 1;
517 }
518
519 if (m_startLines[b] <= line && line < m_startLines[b] + m_blocks[b]->lines()) {
520 return b;
521 }
522
523 if (m_startLines[b] > line) {
524 for (int i = b - 1; i >= 0; --i) {
525 if (m_startLines[i] <= line && line < m_startLines[i] + m_blocks[i]->lines()) {
526 return i;
527 }
528 }
529 }
530
531 if (m_startLines[b] < line || (m_blocks[b]->lines() == 0)) {
532 for (size_t i = b + 1; i < m_blocks.size(); ++i) {
533 if (m_startLines[i] <= line && line < m_startLines[i] + m_blocks[i]->lines()) {
534 return i;
535 }
536 }
537 }
538
539 qFatal("line requested in text buffer (%d out of [0, %d[), no block found", line, lines());
540 return -1;
541}
542
543void TextBuffer::fixStartLines(int startBlock, int value)
544{
545 // only allow valid start block
546 Q_ASSERT(startBlock >= 0);
547 Q_ASSERT(startBlock <= (int)m_startLines.size());
548 // fixup block
549 for (auto it = m_startLines.begin() + startBlock, end = m_startLines.end(); it != end; ++it) {
550 // move start line by given value
551 *it += value;
552 }
553}
554
555void TextBuffer::balanceBlock(int index)
556{
557 auto check = qScopeGuard([this] {
558 if (!(m_blocks.size() == m_startLines.size() && m_blocks.size() == m_blockSizes.size())) {
559 qFatal("blocks/startlines/blocksizes are not equal in size!");
560 }
561 });
562
563 // two cases, too big or too small block
564 TextBlock *blockToBalance = m_blocks.at(index);
565
566 // first case, too big one, split it
567 if (blockToBalance->lines() >= 2 * BufferBlockSize) {
568 // half the block
569 int halfSize = blockToBalance->lines() / 2;
570
571 // create and insert new block after current one, already set right start line
572 const int newBlockStartLine = m_startLines[index] + halfSize;
573 TextBlock *newBlock = new TextBlock(this, index + 1);
574 m_blocks.insert(m_blocks.begin() + index + 1, newBlock);
575 m_startLines.insert(m_startLines.begin() + index + 1, newBlockStartLine);
576 m_blockSizes.insert(m_blockSizes.begin() + index + 1, 0);
577
578 // adjust block indexes
579 for (auto it = m_blocks.begin() + index, end = m_blocks.end(); it != end; ++it) {
580 (*it)->setBlockIndex(index++);
581 }
582
583 blockToBalance->splitBlock(halfSize, newBlock);
584
585 // split is done
586 return;
587 }
588
589 // second case: possibly too small block
590
591 // if only one block, no chance to unite
592 // same if this is first block, we always append to previous one
593 if (index == 0) {
594 // remove the block if its empty
595 if (blockToBalance->lines() == 0) {
596 m_blocks.erase(m_blocks.begin());
597 m_startLines.erase(m_startLines.begin());
598 m_blockSizes.erase(m_blockSizes.begin());
599 Q_ASSERT(m_startLines[0] == 0);
600 for (auto it = m_blocks.begin(), end = m_blocks.end(); it != end; ++it) {
601 (*it)->setBlockIndex(index++);
602 }
603 }
604 return;
605 }
606
607 // block still large enough, do nothing
608 if (2 * blockToBalance->lines() > BufferBlockSize) {
609 return;
610 }
611
612 // unite small block with predecessor
613 TextBlock *targetBlock = m_blocks.at(index - 1);
614
615 // merge block
616 blockToBalance->mergeBlock(targetBlock);
617 m_blockSizes[index - 1] += m_blockSizes[index];
618
619 // delete old block
620 delete blockToBalance;
621 m_blocks.erase(m_blocks.begin() + index);
622 m_startLines.erase(m_startLines.begin() + index);
623 m_blockSizes.erase(m_blockSizes.begin() + index);
624
625 for (auto it = m_blocks.begin() + index, end = m_blocks.end(); it != end; ++it) {
626 (*it)->setBlockIndex(index++);
627 }
628
629 Q_ASSERT(index == (int)m_blocks.size());
630}
631
632void TextBuffer::debugPrint(const QString &title) const
633{
634 // print header with title
635 printf("%s (lines: %d)\n", qPrintable(title), m_lines);
636
637 // print all blocks
638 for (size_t i = 0; i < m_blocks.size(); ++i) {
639 m_blocks.at(i)->debugPrint(i);
640 }
641}
642
643bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec)
644{
645 // fallback codec must exist
646 Q_ASSERT(!m_fallbackTextCodec.isEmpty());
647
648 // codec must be set!
649 Q_ASSERT(!m_textCodec.isEmpty());
650
651 // first: clear buffer in any case!
652 clear();
653
654 // construct the file loader for the given file, with correct prober type
655 Kate::TextLoader file(filename, m_encodingProberType, m_lineLengthLimit);
656
657 // triple play, maximal three loading rounds
658 // 0) use the given encoding, be done, if no encoding errors happen
659 // 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done
660 // 2) use fallback encoding, be done, if no encoding errors happen
661 // 3) use again given encoding, be done in any case
662 for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) {
663 // kill all blocks beside first one
664 for (size_t b = 1; b < m_blocks.size(); ++b) {
665 TextBlock *block = m_blocks.at(b);
666 block->clearLines();
667 delete block;
668 }
669 m_blocks.resize(1);
670 m_startLines.resize(1);
671 m_blockSizes.resize(1);
672
673 // remove lines in first block
674 m_blocks.back()->clearLines();
675 m_startLines.back() = 0;
676 m_blockSizes.back() = 0;
677 m_lines = 0;
678
679 // reset error flags
680 tooLongLinesWrapped = false;
681 longestLineLoaded = 0;
682
683 // try to open file, with given encoding
684 // in round 0 + 3 use the given encoding from user
685 // in round 1 use 0, to trigger detection
686 // in round 2 use fallback
687 QString codec = m_textCodec;
688 if (i == 1) {
689 codec.clear();
690 } else if (i == 2) {
691 codec = m_fallbackTextCodec;
692 }
693
694 if (!file.open(codec)) {
695 // create one dummy textline, in any case
696 m_blocks.back()->appendLine(QString());
697 m_lines++;
698 m_blockSizes[0] = 1;
699 return false;
700 }
701
702 // read in all lines...
703 encodingErrors = false;
704 while (!file.eof()) {
705 // read line
706 int offset = 0;
707 int length = 0;
708 bool currentError = !file.readLine(offset, length, tooLongLinesWrapped, longestLineLoaded);
709 encodingErrors = encodingErrors || currentError;
710
711 // bail out on encoding error, if not last round!
712 if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) {
713 BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << file.textCodec();
714 break;
715 }
716
717 // ensure blocks aren't too large
718 if (m_blocks.back()->lines() >= BufferBlockSize) {
719 int index = (int)m_blocks.size();
720 int startLine = m_blocks.back()->startLine() + m_blocks.back()->lines();
721 m_blocks.push_back(new TextBlock(this, index));
722 m_startLines.push_back(startLine);
723 m_blockSizes.push_back(0);
724 }
725
726 // append line to last block
727 m_blocks.back()->appendLine(QString(file.unicode() + offset, length));
728 m_blockSizes.back() += length + 1;
729 ++m_lines;
730 }
731
732 // if no encoding error, break out of reading loop
733 if (!encodingErrors) {
734 // remember used codec, might change bom setting
735 setTextCodec(file.textCodec());
736 break;
737 }
738 }
739
740 // save checksum of file on disk
741 setDigest(file.digest());
742
743 // remember if BOM was found
744 if (file.byteOrderMarkFound()) {
745 setGenerateByteOrderMark(true);
746 }
747
748 // remember eol mode, if any found in file
749 if (file.eol() != eolUnknown) {
750 setEndOfLineMode(file.eol());
751 }
752
753 // remember mime type for filter device
754 m_mimeTypeForFilterDev = file.mimeTypeForFilterDev();
755
756 // assert that one line is there!
757 Q_ASSERT(m_lines > 0);
758
759 // report CODEC + ERRORS
760 BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec << (encodingErrors ? "with" : "without") << "encoding errors";
761
762 // report BOM
763 BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find") << "byte order mark";
764
765 // report filter device mime-type
766 BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev;
767
768 // emit success
769 Q_EMIT loaded(filename, encodingErrors);
770
771 // file loading worked, modulo encoding problems
772 return true;
773}
774
775const QByteArray &TextBuffer::digest() const
776{
777 return m_digest;
778}
779
780void TextBuffer::setDigest(const QByteArray &checksum)
781{
782 m_digest = checksum;
783}
784
785void TextBuffer::setTextCodec(const QString &codec)
786{
787 m_textCodec = codec;
788
789 // enforce bom for some encodings
790 if (const auto setEncoding = QStringConverter::encodingForName(m_textCodec.toUtf8().constData())) {
791 for (const auto encoding : {QStringConverter::Utf16,
797 if (setEncoding == encoding) {
798 setGenerateByteOrderMark(true);
799 break;
800 }
801 }
802 }
803}
804
805bool TextBuffer::save(const QString &filename)
806{
807 // codec must be set, else below we fail!
808 Q_ASSERT(!m_textCodec.isEmpty());
809
810 SaveResult saveRes = saveBufferUnprivileged(filename);
811
812 if (saveRes == SaveResult::Failed) {
813 return false;
814 } else if (saveRes == SaveResult::MissingPermissions) {
815 // either unit-test mode or we're missing permissions to write to the
816 // file => use temporary file and try to use authhelper
817 if (!saveBufferEscalated(filename)) {
818 return false;
819 }
820 }
821
822 // remember this revision as last saved
823 m_history.setLastSavedRevision();
824
825 // inform that we have saved the state
826 markModifiedLinesAsSaved();
827
828 // emit that file was saved and be done
829 Q_EMIT saved(filename);
830 return true;
831}
832
833bool TextBuffer::saveBuffer(const QString &filename, KCompressionDevice &saveFile)
834{
835 QStringEncoder encoder(m_textCodec.toUtf8().constData(), generateByteOrderMark() ? QStringConverter::Flag::WriteBom : QStringConverter::Flag::Default);
836
837 // our loved eol string ;)
838 QString eol = QStringLiteral("\n");
839 if (endOfLineMode() == eolDos) {
840 eol = QStringLiteral("\r\n");
841 } else if (endOfLineMode() == eolMac) {
842 eol = QStringLiteral("\r");
843 }
844
845 // just dump the lines out ;)
846 for (int i = 0; i < m_lines; ++i) {
847 // dump current line
848 saveFile.write(encoder.encode(line(i).text()));
849
850 // append correct end of line string
851 if ((i + 1) < m_lines) {
852 saveFile.write(encoder.encode(eol));
853 }
854
855 // early out on stream errors
856 if (saveFile.error() != QFileDevice::NoError) {
857 return false;
858 }
859 }
860
861 // TODO: this only writes bytes when there is text. This is a fine optimization for most cases, but this makes saving
862 // an empty file with the BOM set impossible (results to an empty file with 0 bytes, no BOM)
863
864 // close the file, we might want to read from underlying buffer below
865 saveFile.close();
866
867 // did save work?
868 if (saveFile.error() != QFileDevice::NoError) {
869 BUFFER_DEBUG << "Saving file " << filename << "failed with error" << saveFile.errorString();
870 return false;
871 }
872
873 return true;
874}
875
876TextBuffer::SaveResult TextBuffer::saveBufferUnprivileged(const QString &filename)
877{
878 if (m_alwaysUseKAuthForSave) {
879 // unit-testing mode, simulate we need privileges
880 return SaveResult::MissingPermissions;
881 }
882
883 // construct correct filter device
884 // we try to use the same compression as for opening
886 auto saveFile = std::make_unique<KCompressionDevice>(filename, type);
887
888 if (!saveFile->open(QIODevice::WriteOnly)) {
889#ifdef CAN_USE_ERRNO
890 if (errno != EACCES) {
891 return SaveResult::Failed;
892 }
893#endif
894 return SaveResult::MissingPermissions;
895 }
896
897 if (!saveBuffer(filename, *saveFile)) {
898 return SaveResult::Failed;
899 }
900
901 return SaveResult::Success;
902}
903
904bool TextBuffer::saveBufferEscalated(const QString &filename)
905{
906#if HAVE_KAUTH
907 // construct correct filter device
908 // we try to use the same compression as for opening
910 auto saveFile = std::make_unique<KCompressionDevice>(filename, type);
911 uint ownerId = -2;
912 uint groupId = -2;
913 std::unique_ptr<QIODevice> temporaryBuffer;
914
915 // Memorize owner and group.
916 const QFileInfo fileInfo(filename);
917 if (fileInfo.exists()) {
918 ownerId = fileInfo.ownerId();
919 groupId = fileInfo.groupId();
920 }
921
922 // if that fails we need more privileges to save this file
923 // -> we write to a temporary file and then send its path to KAuth action for privileged save
924 temporaryBuffer = std::make_unique<QBuffer>();
925
926 // open buffer for write and read (read is used for checksum computing and writing to temporary file)
927 if (!temporaryBuffer->open(QIODevice::ReadWrite)) {
928 return false;
929 }
930
931 // we are now saving to a temporary buffer with potential compression proxy
932 saveFile = std::make_unique<KCompressionDevice>(temporaryBuffer.get(), false, type);
933 if (!saveFile->open(QIODevice::WriteOnly)) {
934 return false;
935 }
936
937 if (!saveBuffer(filename, *saveFile)) {
938 return false;
939 }
940
941 // temporary buffer was used to save the file
942 // -> computing checksum
943 // -> saving to temporary file
944 // -> copying the temporary file to the original file location with KAuth action
945 QTemporaryFile tempFile;
946 if (!tempFile.open()) {
947 return false;
948 }
949
950 // go to QBuffer start
951 temporaryBuffer->seek(0);
952
953 // read contents of QBuffer and add them to checksum utility as well as to QTemporaryFile
954 char buffer[bufferLength];
955 qint64 read = -1;
956 QCryptographicHash cryptographicHash(SecureTextBuffer::checksumAlgorithm);
957 while ((read = temporaryBuffer->read(buffer, bufferLength)) > 0) {
958 cryptographicHash.addData(QByteArrayView(buffer, read));
959 if (tempFile.write(buffer, read) == -1) {
960 return false;
961 }
962 }
963 if (!tempFile.flush()) {
964 return false;
965 }
966
967 // prepare data for KAuth action
968 QVariantMap kAuthActionArgs;
969 kAuthActionArgs.insert(QStringLiteral("sourceFile"), tempFile.fileName());
970 kAuthActionArgs.insert(QStringLiteral("targetFile"), filename);
971 kAuthActionArgs.insert(QStringLiteral("checksum"), cryptographicHash.result());
972 kAuthActionArgs.insert(QStringLiteral("ownerId"), ownerId);
973 kAuthActionArgs.insert(QStringLiteral("groupId"), groupId);
974
975 // call save with elevated privileges
976 if (QStandardPaths::isTestModeEnabled()) {
977 // unit testing purposes only
978 if (!SecureTextBuffer::savefile(kAuthActionArgs).succeeded()) {
979 return false;
980 }
981 } else {
982 KAuth::Action kAuthSaveAction(QStringLiteral("org.kde.ktexteditor6.katetextbuffer.savefile"));
983 kAuthSaveAction.setHelperId(QStringLiteral("org.kde.ktexteditor6.katetextbuffer"));
984 kAuthSaveAction.setArguments(kAuthActionArgs);
985 KAuth::ExecuteJob *job = kAuthSaveAction.execute();
986 if (!job->exec()) {
987 return false;
988 }
989 }
990
991 return true;
992#else
993 Q_UNUSED(filename);
994 return false;
995#endif
996}
997
998void TextBuffer::notifyAboutRangeChange(KTextEditor::View *view, KTextEditor::LineRange lineRange, bool needsRepaint, TextRange *deleteRange)
999{
1000 // ignore calls if no document is around
1001 if (!m_document) {
1002 return;
1003 }
1004
1005 // update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really
1006 // just create 20k ranges in a go and you wait seconds on a decent machine
1007 const QList<KTextEditor::View *> views = m_document->views();
1008 for (KTextEditor::View *curView : views) {
1009 // filter wrong views
1010 if (view && view != curView && !deleteRange) {
1011 continue;
1012 }
1013
1014 // notify view, it is really a kate view
1015 static_cast<KTextEditor::ViewPrivate *>(curView)->notifyAboutRangeChange(lineRange, needsRepaint, deleteRange);
1016 }
1017}
1018
1019void TextBuffer::markModifiedLinesAsSaved()
1020{
1021 for (TextBlock *block : std::as_const(m_blocks)) {
1022 block->markModifiedLinesAsSaved();
1023 }
1024}
1025
1026void TextBuffer::addMultilineRange(TextRange *range)
1027{
1028 auto it = std::find(m_multilineRanges.begin(), m_multilineRanges.end(), range);
1029 if (it == m_multilineRanges.end()) {
1030 m_multilineRanges.push_back(range);
1031 return;
1032 }
1033}
1034
1035void TextBuffer::removeMultilineRange(TextRange *range)
1036{
1037 m_multilineRanges.erase(std::remove(m_multilineRanges.begin(), m_multilineRanges.end(), range), m_multilineRanges.end());
1038}
1039
1040bool TextBuffer::hasMultlineRange(KTextEditor::MovingRange *range) const
1041{
1042 return std::find(m_multilineRanges.begin(), m_multilineRanges.end(), range) != m_multilineRanges.end();
1043}
1044
1045void TextBuffer::rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly, QList<TextRange *> &outRanges) const
1046{
1047 outRanges.clear();
1048 // get block, this will assert on invalid line
1049 const int blockIndex = blockForLine(line);
1050 m_blocks.at(blockIndex)->rangesForLine(line, view, rangesWithAttributeOnly, outRanges);
1051 // printf("Requested range for line %d, available %d\n", (int)line, (int)m_multilineRanges.size());
1052 for (TextRange *range : std::as_const(m_multilineRanges)) {
1053 if (rangesWithAttributeOnly && !range->hasAttribute()) {
1054 continue;
1055 }
1056
1057 // we want ranges for no view, but this one's attribute is only valid for views
1058 if (!view && range->attributeOnlyForViews()) {
1059 continue;
1060 }
1061
1062 // the range's attribute is not valid for this view
1063 if (range->view() && range->view() != view) {
1064 continue;
1065 }
1066
1067 // if line is in the range, ok
1068 if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) {
1069 outRanges.append(range);
1070 }
1071 }
1072 std::sort(outRanges.begin(), outRanges.end());
1073 outRanges.erase(std::unique(outRanges.begin(), outRanges.end()), outRanges.end());
1074}
1075}
1076
1077#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? Default is false.
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
Class representing a text block.
void appendLine(const QString &textOfLine)
Append a new line with given text.
void clearLines()
Clear the lines.
TextBuffer(KTextEditor::DocumentPrivate *parent, bool alwaysUseKAuth=false)
Construct an empty text buffer.
~TextBuffer() override
Destruct the text buffer Virtual, we allow inheritance.
void invalidateRanges()
Invalidate all ranges in this buffer.
virtual void clear()
Clears the buffer, reverts to initial empty state.
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 Q_NOREPLY void start()
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)
const QChar at(qsizetype position) const const
void chop(qsizetype n)
void clear()
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
std::optional< Encoding > encodingForName(const char *name)
virtual QString fileName() const const override
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 20 2024 11:56:00 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.