1 | /* This file is part of the Kate project. |
2 | * |
3 | * Copyright (C) 2010 Christoph Cullmann <cullmann@kde.org> |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Library General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2 of the License, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Library General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Library General Public License |
16 | * along with this library; see the file COPYING.LIB. If not, write to |
17 | * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
18 | * Boston, MA 02110-1301, USA. |
19 | */ |
20 | |
21 | #include "config.h" |
22 | |
23 | #include "katetextbuffer.h" |
24 | #include "katetextloader.h" |
25 | |
26 | // this is unfortunate, but needed for performance |
27 | #include "katedocument.h" |
28 | #include "kateview.h" |
29 | |
30 | // fdatasync |
31 | #include <kde_file.h> |
32 | |
33 | #include <KSaveFile> |
34 | #include <kdeversion.h> |
35 | |
36 | #if 0 |
37 | #define EDIT_DEBUG kDebug() |
38 | #else |
39 | #define EDIT_DEBUG if (0) kDebug() |
40 | #endif |
41 | |
42 | namespace Kate { |
43 | |
44 | TextBuffer::TextBuffer (KateDocument *parent, int blockSize) |
45 | : QObject (parent) |
46 | , m_document (parent) |
47 | , m_history (*this) |
48 | , m_blockSize (blockSize) |
49 | , m_lines (0) |
50 | , m_lastUsedBlock (0) |
51 | , m_revision (0) |
52 | , m_editingTransactions (0) |
53 | , m_editingLastRevision (0) |
54 | , m_editingLastLines (0) |
55 | , m_editingMinimalLineChanged (-1) |
56 | , m_editingMaximalLineChanged (-1) |
57 | , m_encodingProberType (KEncodingProber::Universal) |
58 | , m_fallbackTextCodec (0) |
59 | , m_textCodec (0) |
60 | , m_generateByteOrderMark (false) |
61 | , m_endOfLineMode (eolUnix) |
62 | , m_newLineAtEof (false) |
63 | , m_lineLengthLimit (4096) |
64 | { |
65 | // minimal block size must be > 0 |
66 | Q_ASSERT (m_blockSize > 0); |
67 | |
68 | // create initial state |
69 | clear (); |
70 | } |
71 | |
72 | TextBuffer::~TextBuffer () |
73 | { |
74 | // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect |
75 | m_document = 0; |
76 | |
77 | // not allowed during editing |
78 | Q_ASSERT (m_editingTransactions == 0); |
79 | |
80 | // kill all ranges, work on copy, they will remove themself from the hash |
81 | QSet<TextRange *> copyRanges = m_ranges; |
82 | qDeleteAll (copyRanges); |
83 | Q_ASSERT (m_ranges.empty()); |
84 | |
85 | // clean out all cursors and lines, only cursors belonging to range will survive |
86 | foreach(TextBlock* block, m_blocks) |
87 | block->deleteBlockContent (); |
88 | |
89 | // delete all blocks, now that all cursors are really deleted |
90 | // else asserts in destructor of blocks will fail! |
91 | qDeleteAll (m_blocks); |
92 | m_blocks.clear (); |
93 | |
94 | // kill all invalid cursors, do this after block deletion, to uncover if they might be still linked in blocks |
95 | QSet<TextCursor *> copyCursors = m_invalidCursors; |
96 | qDeleteAll (copyCursors); |
97 | Q_ASSERT (m_invalidCursors.empty()); |
98 | } |
99 | |
100 | void TextBuffer::invalidateRanges() |
101 | { |
102 | // invalidate all ranges, work on copy, they might delete themself... |
103 | QSet<TextRange *> copyRanges = m_ranges; |
104 | foreach (TextRange *range, copyRanges) |
105 | range->setRange (KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()); |
106 | } |
107 | |
108 | void TextBuffer::clear () |
109 | { |
110 | // not allowed during editing |
111 | Q_ASSERT (m_editingTransactions == 0); |
112 | |
113 | invalidateRanges(); |
114 | |
115 | // new block for empty buffer |
116 | TextBlock *newBlock = new TextBlock (this, 0); |
117 | newBlock->appendLine (QString()); |
118 | |
119 | // clean out all cursors and lines, either move them to newBlock or invalidate them, if belonging to a range |
120 | foreach(TextBlock* block, m_blocks) |
121 | block->clearBlockContent (newBlock); |
122 | |
123 | // kill all buffer blocks |
124 | qDeleteAll (m_blocks); |
125 | m_blocks.clear (); |
126 | |
127 | // insert one block with one empty line |
128 | m_blocks.append (newBlock); |
129 | |
130 | // reset lines and last used block |
131 | m_lines = 1; |
132 | m_lastUsedBlock = 0; |
133 | |
134 | // reset revision |
135 | m_revision = 0; |
136 | |
137 | // reset bom detection |
138 | m_generateByteOrderMark = false; |
139 | |
140 | // reset the filter device |
141 | m_mimeTypeForFilterDev = "text/plain" ; |
142 | |
143 | // clear edit history |
144 | m_history.clear (); |
145 | |
146 | // we got cleared |
147 | emit cleared (); |
148 | } |
149 | |
150 | TextLine TextBuffer::line (int line) const |
151 | { |
152 | // get block, this will assert on invalid line |
153 | int blockIndex = blockForLine (line); |
154 | |
155 | // get line |
156 | return m_blocks.at(blockIndex)->line (line); |
157 | } |
158 | |
159 | QString TextBuffer::text () const |
160 | { |
161 | QString text; |
162 | |
163 | // combine all blocks |
164 | foreach(TextBlock* block, m_blocks) |
165 | block->text (text); |
166 | |
167 | // return generated string |
168 | return text; |
169 | } |
170 | |
171 | bool TextBuffer::startEditing () |
172 | { |
173 | // increment transaction counter |
174 | ++m_editingTransactions; |
175 | |
176 | // if not first running transaction, do nothing |
177 | if (m_editingTransactions > 1) |
178 | return false; |
179 | |
180 | // reset information about edit... |
181 | m_editingLastRevision = m_revision; |
182 | m_editingLastLines = m_lines; |
183 | m_editingMinimalLineChanged = -1; |
184 | m_editingMaximalLineChanged = -1; |
185 | |
186 | // transaction has started |
187 | emit editingStarted (); |
188 | |
189 | // first transaction started |
190 | return true; |
191 | } |
192 | |
193 | bool TextBuffer::finishEditing () |
194 | { |
195 | // only allowed if still transactions running |
196 | Q_ASSERT (m_editingTransactions > 0); |
197 | |
198 | // decrement counter |
199 | --m_editingTransactions; |
200 | |
201 | // if not last running transaction, do nothing |
202 | if (m_editingTransactions > 0) |
203 | return false; |
204 | |
205 | // assert that if buffer changed, the line ranges are set and valid! |
206 | Q_ASSERT (!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); |
207 | Q_ASSERT (!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); |
208 | Q_ASSERT (!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); |
209 | Q_ASSERT (!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); |
210 | |
211 | // transaction has finished |
212 | emit editingFinished (); |
213 | |
214 | // last transaction finished |
215 | return true; |
216 | } |
217 | |
218 | void TextBuffer::wrapLine (const KTextEditor::Cursor &position) |
219 | { |
220 | // debug output for REAL low-level debugging |
221 | EDIT_DEBUG << "wrapLine" << position; |
222 | |
223 | // only allowed if editing transaction running |
224 | Q_ASSERT (m_editingTransactions > 0); |
225 | |
226 | // get block, this will assert on invalid line |
227 | int blockIndex = blockForLine (position.line()); |
228 | |
229 | /** |
230 | * let the block handle the wrapLine |
231 | * this can only lead to one more line in this block |
232 | * no other blocks will change |
233 | * this call will trigger fixStartLines |
234 | */ |
235 | ++m_lines; // first alter the line counter, as functions called will need the valid one |
236 | m_blocks.at(blockIndex)->wrapLine (position, blockIndex); |
237 | |
238 | // remember changes |
239 | ++m_revision; |
240 | |
241 | // update changed line interval |
242 | if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) |
243 | m_editingMinimalLineChanged = position.line(); |
244 | |
245 | if (position.line() <= m_editingMaximalLineChanged) |
246 | ++m_editingMaximalLineChanged; |
247 | else |
248 | m_editingMaximalLineChanged = position.line() + 1; |
249 | |
250 | // balance the changed block if needed |
251 | balanceBlock (blockIndex); |
252 | |
253 | // emit signal about done change |
254 | emit lineWrapped (position); |
255 | } |
256 | |
257 | void TextBuffer::unwrapLine (int line) |
258 | { |
259 | // debug output for REAL low-level debugging |
260 | EDIT_DEBUG << "unwrapLine" << line; |
261 | |
262 | // only allowed if editing transaction running |
263 | Q_ASSERT (m_editingTransactions > 0); |
264 | |
265 | // line 0 can't be unwrapped |
266 | Q_ASSERT (line > 0); |
267 | |
268 | // get block, this will assert on invalid line |
269 | int blockIndex = blockForLine (line); |
270 | |
271 | // is this the first line in the block? |
272 | bool firstLineInBlock = (line == m_blocks.at(blockIndex)->startLine()); |
273 | |
274 | /** |
275 | * let the block handle the unwrapLine |
276 | * this can either lead to one line less in this block or the previous one |
277 | * the previous one could even end up with zero lines |
278 | * this call will trigger fixStartLines |
279 | */ |
280 | m_blocks.at(blockIndex)->unwrapLine (line, (blockIndex > 0) ? m_blocks.at(blockIndex-1) : 0, firstLineInBlock ? (blockIndex - 1) : blockIndex); |
281 | --m_lines; |
282 | |
283 | // decrement index for later fixup, if we modified the block in front of the found one |
284 | if (firstLineInBlock) |
285 | --blockIndex; |
286 | |
287 | // remember changes |
288 | ++m_revision; |
289 | |
290 | // update changed line interval |
291 | if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) |
292 | m_editingMinimalLineChanged = line - 1; |
293 | |
294 | if (line <= m_editingMaximalLineChanged) |
295 | --m_editingMaximalLineChanged; |
296 | else |
297 | m_editingMaximalLineChanged = line -1; |
298 | |
299 | // balance the changed block if needed |
300 | balanceBlock (blockIndex); |
301 | |
302 | // emit signal about done change |
303 | emit lineUnwrapped (line); |
304 | } |
305 | |
306 | void TextBuffer::insertText (const KTextEditor::Cursor &position, const QString &text) |
307 | { |
308 | // debug output for REAL low-level debugging |
309 | EDIT_DEBUG << "insertText" << position << text; |
310 | |
311 | // only allowed if editing transaction running |
312 | Q_ASSERT (m_editingTransactions > 0); |
313 | |
314 | // skip work, if no text to insert |
315 | if (text.isEmpty()) |
316 | return; |
317 | |
318 | // get block, this will assert on invalid line |
319 | int blockIndex = blockForLine (position.line()); |
320 | |
321 | // let the block handle the insertText |
322 | m_blocks.at(blockIndex)->insertText (position, text); |
323 | |
324 | // remember changes |
325 | ++m_revision; |
326 | |
327 | // update changed line interval |
328 | if (position.line () < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) |
329 | m_editingMinimalLineChanged = position.line (); |
330 | |
331 | if (position.line () > m_editingMaximalLineChanged) |
332 | m_editingMaximalLineChanged = position.line (); |
333 | |
334 | // emit signal about done change |
335 | emit textInserted (position, text); |
336 | } |
337 | |
338 | void TextBuffer::removeText (const KTextEditor::Range &range) |
339 | { |
340 | // debug output for REAL low-level debugging |
341 | EDIT_DEBUG << "removeText" << range; |
342 | |
343 | // only allowed if editing transaction running |
344 | Q_ASSERT (m_editingTransactions > 0); |
345 | |
346 | // only ranges on one line are supported |
347 | Q_ASSERT (range.start().line() == range.end().line()); |
348 | |
349 | // start column <= end column and >= 0 |
350 | Q_ASSERT (range.start().column() <= range.end().column()); |
351 | Q_ASSERT (range.start().column() >= 0); |
352 | |
353 | // skip work, if no text to remove |
354 | if (range.isEmpty()) |
355 | return; |
356 | |
357 | // get block, this will assert on invalid line |
358 | int blockIndex = blockForLine (range.start().line()); |
359 | |
360 | // let the block handle the removeText, retrieve removed text |
361 | QString text; |
362 | m_blocks.at(blockIndex)->removeText (range, text); |
363 | |
364 | // remember changes |
365 | ++m_revision; |
366 | |
367 | // update changed line interval |
368 | if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) |
369 | m_editingMinimalLineChanged = range.start().line(); |
370 | |
371 | if (range.start().line() > m_editingMaximalLineChanged) |
372 | m_editingMaximalLineChanged = range.start().line(); |
373 | |
374 | // emit signal about done change |
375 | emit textRemoved (range, text); |
376 | } |
377 | |
378 | int TextBuffer::blockForLine (int line) const |
379 | { |
380 | // only allow valid lines |
381 | if ((line < 0) || (line >= lines())) |
382 | qFatal ("out of range line requested in text buffer (%d out of [0, %d[)" , line, lines()); |
383 | |
384 | // we need blocks and last used block should not be negative |
385 | Q_ASSERT (!m_blocks.isEmpty()); |
386 | Q_ASSERT (m_lastUsedBlock >= 0); |
387 | |
388 | /** |
389 | * shortcut: try last block first |
390 | */ |
391 | if (m_lastUsedBlock < m_blocks.size()) { |
392 | /** |
393 | * check if block matches |
394 | * if yes, just return again this block |
395 | */ |
396 | TextBlock* block = m_blocks[m_lastUsedBlock]; |
397 | const int start = block->startLine(); |
398 | const int lines = block->lines (); |
399 | if (start <= line && line < (start + lines)) |
400 | return m_lastUsedBlock; |
401 | } |
402 | |
403 | /** |
404 | * search for right block |
405 | * use binary search |
406 | * if we leave this loop not by returning the found element we have an error |
407 | */ |
408 | int blockStart = 0; |
409 | int blockEnd = m_blocks.size() - 1; |
410 | while (blockEnd >= blockStart) { |
411 | // get middle and ensure it is OK |
412 | int middle = blockStart + ((blockEnd - blockStart) / 2); |
413 | Q_ASSERT (middle >= 0); |
414 | Q_ASSERT (middle < m_blocks.size()); |
415 | |
416 | // facts bout this block |
417 | TextBlock* block = m_blocks[middle]; |
418 | const int start = block->startLine(); |
419 | const int lines = block->lines (); |
420 | |
421 | // right block found, remember it and return it |
422 | if (start <= line && line < (start + lines)) { |
423 | m_lastUsedBlock = middle; |
424 | return middle; |
425 | } |
426 | |
427 | // half our stuff ;) |
428 | if (line < start) |
429 | blockEnd = middle - 1; |
430 | else |
431 | blockStart = middle + 1; |
432 | } |
433 | |
434 | // we should always find a block |
435 | qFatal ("line requested in text buffer (%d out of [0, %d[), no block found" , line, lines()); |
436 | return -1; |
437 | } |
438 | |
439 | void TextBuffer::fixStartLines (int startBlock) |
440 | { |
441 | // only allow valid start block |
442 | Q_ASSERT (startBlock >= 0); |
443 | Q_ASSERT (startBlock < m_blocks.size()); |
444 | |
445 | // new start line for next block |
446 | TextBlock* block = m_blocks.at(startBlock); |
447 | int newStartLine = block->startLine () + block->lines (); |
448 | |
449 | // fixup block |
450 | for (int index = startBlock + 1; index < m_blocks.size(); ++index) { |
451 | // set new start line |
452 | block = m_blocks.at(index); |
453 | block->setStartLine (newStartLine); |
454 | |
455 | // calculate next start line |
456 | newStartLine += block->lines (); |
457 | } |
458 | } |
459 | |
460 | void TextBuffer::balanceBlock (int index) |
461 | { |
462 | /** |
463 | * two cases, too big or too small block |
464 | */ |
465 | TextBlock *blockToBalance = m_blocks.at(index); |
466 | |
467 | // first case, too big one, split it |
468 | if (blockToBalance->lines () >= 2 * m_blockSize) { |
469 | // half the block |
470 | int halfSize = blockToBalance->lines () / 2; |
471 | |
472 | // create and insert new block behind current one, already set right start line |
473 | TextBlock *newBlock = blockToBalance->splitBlock (halfSize); |
474 | Q_ASSERT (newBlock); |
475 | m_blocks.insert (m_blocks.begin() + index + 1, newBlock); |
476 | |
477 | // split is done |
478 | return; |
479 | } |
480 | |
481 | // second case: possibly too small block |
482 | |
483 | // if only one block, no chance to unite |
484 | // same if this is first block, we always append to previous one |
485 | if (index == 0) |
486 | return; |
487 | |
488 | // block still large enough, do nothing |
489 | if (2 * blockToBalance->lines () > m_blockSize) |
490 | return; |
491 | |
492 | // unite small block with predecessor |
493 | TextBlock *targetBlock = m_blocks.at(index-1); |
494 | |
495 | // merge block |
496 | blockToBalance->mergeBlock (targetBlock); |
497 | |
498 | // delete old block |
499 | delete blockToBalance; |
500 | m_blocks.erase (m_blocks.begin() + index); |
501 | } |
502 | |
503 | void TextBuffer::debugPrint (const QString &title) const |
504 | { |
505 | // print header with title |
506 | printf ("%s (lines: %d bs: %d)\n" , qPrintable (title), m_lines, m_blockSize); |
507 | |
508 | // print all blocks |
509 | for (int i = 0; i < m_blocks.size(); ++i) |
510 | m_blocks.at(i)->debugPrint (i); |
511 | } |
512 | |
513 | bool TextBuffer::load (const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, bool enforceTextCodec) |
514 | { |
515 | // fallback codec must exist |
516 | Q_ASSERT (m_fallbackTextCodec); |
517 | |
518 | // codec must be set! |
519 | Q_ASSERT (m_textCodec); |
520 | |
521 | /** |
522 | * first: clear buffer in any case! |
523 | */ |
524 | clear (); |
525 | |
526 | /** |
527 | * construct the file loader for the given file, with correct prober type |
528 | */ |
529 | Kate::TextLoader file (filename, m_encodingProberType); |
530 | |
531 | /** |
532 | * triple play, maximal three loading rounds |
533 | * 0) use the given encoding, be done, if no encoding errors happen |
534 | * 1) use BOM to decided if unicode or if that fails, use encoding prober, if no encoding errors happen, be done |
535 | * 2) use fallback encoding, be done, if no encoding errors happen |
536 | * 3) use again given encoding, be done in any case |
537 | */ |
538 | for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { |
539 | /** |
540 | * kill all blocks beside first one |
541 | */ |
542 | for (int b = 1; b < m_blocks.size(); ++b) { |
543 | TextBlock* block = m_blocks.at(b); |
544 | block->clearLines (); |
545 | delete block; |
546 | } |
547 | m_blocks.resize (1); |
548 | |
549 | /** |
550 | * remove lines in first block |
551 | */ |
552 | m_blocks.last()->clearLines (); |
553 | m_lines = 0; |
554 | |
555 | /** |
556 | * try to open file, with given encoding |
557 | * in round 0 + 3 use the given encoding from user |
558 | * in round 1 use 0, to trigger detection |
559 | * in round 2 use fallback |
560 | */ |
561 | QTextCodec *codec = m_textCodec; |
562 | if (i == 1) |
563 | codec = 0; |
564 | else if (i == 2) |
565 | codec = m_fallbackTextCodec; |
566 | |
567 | if (!file.open (codec)) { |
568 | // create one dummy textline, in any case |
569 | m_blocks.last()->appendLine (QString()); |
570 | m_lines++; |
571 | return false; |
572 | } |
573 | |
574 | // read in all lines... |
575 | encodingErrors = false; |
576 | while ( !file.eof() ) |
577 | { |
578 | // read line |
579 | int offset = 0, length = 0; |
580 | bool currentError = !file.readLine (offset, length); |
581 | encodingErrors = encodingErrors || currentError; |
582 | |
583 | // bail out on encoding error, if not last round! |
584 | if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { |
585 | kDebug (13020) << "Failed try to load file" << filename << "with codec" << |
586 | (file.textCodec() ? file.textCodec()->name() : "(null)" ); |
587 | break; |
588 | } |
589 | |
590 | // get unicode data for this line |
591 | const QChar *unicodeData = file.unicode () + offset; |
592 | |
593 | /** |
594 | * split lines, if too large |
595 | */ |
596 | do { |
597 | /** |
598 | * calculate line length |
599 | */ |
600 | int lineLength = length; |
601 | if ((m_lineLengthLimit > 0) && (lineLength > m_lineLengthLimit)) { |
602 | /** |
603 | * search for place to wrap |
604 | */ |
605 | int spacePosition = m_lineLengthLimit-1; |
606 | for (int testPosition = m_lineLengthLimit-1; (testPosition >= 0) && (testPosition >= (m_lineLengthLimit - (m_lineLengthLimit/10))); --testPosition) { |
607 | /** |
608 | * wrap place found? |
609 | */ |
610 | if (unicodeData[testPosition].isSpace() || unicodeData[testPosition].isPunct()) { |
611 | spacePosition = testPosition; |
612 | break; |
613 | } |
614 | } |
615 | |
616 | /** |
617 | * wrap the line |
618 | */ |
619 | lineLength = spacePosition+1; |
620 | length -= lineLength; |
621 | tooLongLinesWrapped = true; |
622 | } else { |
623 | /** |
624 | * be done after this round |
625 | */ |
626 | length = 0; |
627 | } |
628 | |
629 | /** |
630 | * construct new text line with content from file |
631 | * move data pointer |
632 | */ |
633 | QString textLine (unicodeData, lineLength); |
634 | unicodeData += lineLength; |
635 | |
636 | /** |
637 | * ensure blocks aren't too large |
638 | */ |
639 | if (m_blocks.last()->lines() >= m_blockSize) |
640 | m_blocks.append (new TextBlock (this, m_blocks.last()->startLine() + m_blocks.last()->lines())); |
641 | |
642 | /** |
643 | * append line to last block |
644 | */ |
645 | m_blocks.last()->appendLine (textLine); |
646 | ++m_lines; |
647 | } while (length > 0); |
648 | } |
649 | |
650 | // if no encoding error, break out of reading loop |
651 | if (!encodingErrors) { |
652 | // remember used codec, might change bom setting |
653 | setTextCodec (file.textCodec ()); |
654 | break; |
655 | } |
656 | } |
657 | |
658 | // save md5sum of file on disk |
659 | setDigest (file.digest ()); |
660 | |
661 | // remember if BOM was found |
662 | if (file.byteOrderMarkFound ()) |
663 | setGenerateByteOrderMark (true); |
664 | |
665 | // remember eol mode, if any found in file |
666 | if (file.eol() != eolUnknown) |
667 | setEndOfLineMode (file.eol()); |
668 | |
669 | // remember mime type for filter device |
670 | m_mimeTypeForFilterDev = file.mimeTypeForFilterDev (); |
671 | |
672 | // assert that one line is there! |
673 | Q_ASSERT (m_lines > 0); |
674 | |
675 | // report CODEC + ERRORS |
676 | kDebug (13020) << "Loaded file " << filename << "with codec" << m_textCodec->name() |
677 | << (encodingErrors ? "with" : "without" ) << "encoding errors" ; |
678 | |
679 | // report BOM |
680 | kDebug (13020) << (file.byteOrderMarkFound () ? "Found" : "Didn't find" ) << "byte order mark" ; |
681 | |
682 | // report filter device mime-type |
683 | kDebug (13020) << "used filter device for mime-type" << m_mimeTypeForFilterDev; |
684 | |
685 | // emit success |
686 | emit loaded (filename, encodingErrors); |
687 | |
688 | // file loading worked, modulo encoding problems |
689 | return true; |
690 | } |
691 | |
692 | const QByteArray &TextBuffer::digest () const |
693 | { |
694 | return m_digest; |
695 | } |
696 | |
697 | void TextBuffer::setDigest (const QByteArray & md5sum) |
698 | { |
699 | m_digest = md5sum; |
700 | } |
701 | |
702 | void TextBuffer::setTextCodec (QTextCodec *codec) |
703 | { |
704 | m_textCodec = codec; |
705 | |
706 | // enforce bom for some encodings |
707 | int mib = m_textCodec->mibEnum (); |
708 | if (mib == 1013 || mib == 1014 || mib == 1015) // utf16 |
709 | setGenerateByteOrderMark (true); |
710 | if (mib == 1017 || mib == 1018 || mib == 1019) // utf32 |
711 | setGenerateByteOrderMark (true); |
712 | } |
713 | |
714 | bool TextBuffer::save (const QString &filename) |
715 | { |
716 | // codec must be set! |
717 | Q_ASSERT (m_textCodec); |
718 | |
719 | /** |
720 | * use KSaveFile for save write + rename |
721 | */ |
722 | KSaveFile saveFile (filename); |
723 | |
724 | #if KDE_IS_VERSION(4,10,3) |
725 | /** |
726 | * allow fallback if directory not writable |
727 | * fixes bug 312415 |
728 | */ |
729 | saveFile.setDirectWriteFallback (true); |
730 | #endif |
731 | |
732 | /** |
733 | * try to open or fail |
734 | */ |
735 | if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { |
736 | return false; |
737 | } |
738 | |
739 | /** |
740 | * construct correct filter device and try to open |
741 | */ |
742 | QIODevice *file = KFilterDev::device (&saveFile, m_mimeTypeForFilterDev, false); |
743 | const bool deleteFile = file; |
744 | if (!file) |
745 | file = &saveFile; |
746 | |
747 | /** |
748 | * try to open, if new file |
749 | */ |
750 | if (deleteFile) { |
751 | if (!file->open (QIODevice::WriteOnly | QIODevice::Truncate)) { |
752 | delete file; |
753 | return false; |
754 | } |
755 | } |
756 | |
757 | /** |
758 | * construct stream + disable Unicode headers |
759 | */ |
760 | QTextStream stream (file); |
761 | stream.setCodec (QTextCodec::codecForName("UTF-16" )); |
762 | |
763 | // set the correct codec |
764 | stream.setCodec (m_textCodec); |
765 | |
766 | // generate byte order mark? |
767 | stream.setGenerateByteOrderMark (generateByteOrderMark()); |
768 | |
769 | // our loved eol string ;) |
770 | QString eol = "\n" ; //m_doc->config()->eolString (); |
771 | if (endOfLineMode() == eolDos) |
772 | eol = QString ("\r\n" ); |
773 | else if (endOfLineMode() == eolMac) |
774 | eol = QString ("\r" ); |
775 | |
776 | // just dump the lines out ;) |
777 | for (int i = 0; i < m_lines; ++i) |
778 | { |
779 | // get line to save |
780 | Kate::TextLine textline = line (i); |
781 | |
782 | stream << textline->text(); |
783 | |
784 | // append correct end of line string |
785 | if ((i+1) < m_lines) |
786 | stream << eol; |
787 | } |
788 | |
789 | if (m_newLineAtEof) { |
790 | Q_ASSERT(m_lines > 0); // see .h file |
791 | const Kate::TextLine lastLine = line (m_lines - 1); |
792 | const int firstChar = lastLine->firstChar(); |
793 | if (firstChar > -1 || lastLine->length() > 0) { |
794 | stream << eol; |
795 | } |
796 | } |
797 | |
798 | // flush stream |
799 | stream.flush (); |
800 | |
801 | // close and delete file |
802 | if (deleteFile) { |
803 | file->close (); |
804 | delete file; |
805 | } |
806 | |
807 | // flush file |
808 | if (!saveFile.flush()) |
809 | return false; |
810 | |
811 | #ifndef Q_OS_WIN |
812 | // ensure that the file is written to disk |
813 | #ifdef HAVE_FDATASYNC |
814 | fdatasync (saveFile.handle()); |
815 | #else |
816 | fsync (saveFile.handle()); |
817 | #endif |
818 | #endif |
819 | |
820 | // did save work? |
821 | // only finalize if stream status == OK |
822 | bool ok = (stream.status() == QTextStream::Ok) && saveFile.finalize(); |
823 | |
824 | // remember this revision as last saved if we had success! |
825 | if (ok) |
826 | m_history.setLastSavedRevision (); |
827 | |
828 | // report CODEC + ERRORS |
829 | kDebug (13020) << "Saved file " << filename << "with codec" << m_textCodec->name() |
830 | << (ok ? "without" : "with" ) << "errors" ; |
831 | |
832 | if (ok) |
833 | markModifiedLinesAsSaved(); |
834 | |
835 | // emit signal on success |
836 | if (ok) |
837 | emit saved (filename); |
838 | |
839 | // return success or not |
840 | return ok; |
841 | } |
842 | |
843 | void TextBuffer::notifyAboutRangeChange (KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute) |
844 | { |
845 | /** |
846 | * ignore calls if no document is around |
847 | */ |
848 | if (!m_document) |
849 | return; |
850 | |
851 | /** |
852 | * update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really |
853 | * just create 20k ranges in a go and you wait seconds on a decent machine |
854 | */ |
855 | const QList<KTextEditor::View *> &views = m_document->views (); |
856 | foreach(KTextEditor::View* curView, views) { |
857 | // filter wrong views |
858 | if (view && view != curView) |
859 | continue; |
860 | |
861 | // notify view, it is really a kate view |
862 | static_cast<KateView *> (curView)->notifyAboutRangeChange (startLine, endLine, rangeWithAttribute); |
863 | } |
864 | } |
865 | |
866 | void TextBuffer::markModifiedLinesAsSaved() |
867 | { |
868 | foreach(TextBlock* block, m_blocks) |
869 | block->markModifiedLinesAsSaved (); |
870 | } |
871 | |
872 | QList<TextRange *> TextBuffer::rangesForLine (int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const |
873 | { |
874 | // get block, this will assert on invalid line |
875 | const int blockIndex = blockForLine (line); |
876 | |
877 | // get the ranges of the right block |
878 | QList<TextRange *> rightRanges; |
879 | foreach (const QSet<TextRange *> &ranges, m_blocks.at(blockIndex)->rangesForLine (line)) { |
880 | foreach (TextRange * const range, ranges) { |
881 | /** |
882 | * we want only ranges with attributes, but this one has none |
883 | */ |
884 | if (rangesWithAttributeOnly && !range->hasAttribute()) |
885 | continue; |
886 | |
887 | /** |
888 | * we want ranges for no view, but this one's attribute is only valid for views |
889 | */ |
890 | if (!view && range->attributeOnlyForViews()) |
891 | continue; |
892 | |
893 | /** |
894 | * the range's attribute is not valid for this view |
895 | */ |
896 | if (range->view() && range->view() != view) |
897 | continue; |
898 | |
899 | /** |
900 | * if line is in the range, ok |
901 | */ |
902 | if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) |
903 | rightRanges.append (range); |
904 | } |
905 | } |
906 | |
907 | // return right ranges |
908 | return rightRanges; |
909 | } |
910 | |
911 | } |
912 | |