KTextEditor

katetextblock.cpp
1 /*
2  SPDX-FileCopyrightText: 2010 Christoph Cullmann <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "katetextblock.h"
8 #include "katetextbuffer.h"
9 
10 #include <QVarLengthArray>
11 
12 namespace Kate
13 {
14 TextBlock::TextBlock(TextBuffer *buffer, int startLine)
15  : m_buffer(buffer)
16  , m_startLine(startLine)
17 {
18  // reserve the block size
19  m_lines.reserve(m_buffer->m_blockSize);
20 }
21 
23 {
24  // blocks should be empty before they are deleted!
25  Q_ASSERT(m_lines.empty());
26  Q_ASSERT(m_cursors.empty());
27 
28  // it only is a hint for ranges for this block, not the storage of them
29 }
30 
32 {
33  // allow only valid lines
34  Q_ASSERT(startLine >= 0);
35  Q_ASSERT(startLine < m_buffer->lines());
36 
37  m_startLine = startLine;
38 }
39 
41 {
42  // right input
43  Q_ASSERT(line >= startLine());
44 
45  // get text line, at will bail out on out-of-range
46  return m_lines.at(line - startLine());
47 }
48 
49 void TextBlock::appendLine(const QString &textOfLine)
50 {
51  m_lines.push_back(TextLine::create(textOfLine));
52 }
53 
55 {
56  m_lines.clear();
57 }
58 
60 {
61  // combine all lines
62  for (size_t i = 0; i < m_lines.size(); ++i) {
63  // not first line, insert \n
64  if (i > 0 || startLine() > 0) {
65  text.append(QLatin1Char('\n'));
66  }
67 
68  text.append(m_lines.at(i)->text());
69  }
70 }
71 
72 void TextBlock::wrapLine(const KTextEditor::Cursor &position, int fixStartLinesStartIndex)
73 {
74  // calc internal line
75  int line = position.line() - startLine();
76 
77  // get text
78  QString &text = m_lines.at(line)->textReadWrite();
79 
80  // check if valid column
81  Q_ASSERT(position.column() >= 0);
82  Q_ASSERT(position.column() <= text.size());
83 
84  // create new line and insert it
85  m_lines.insert(m_lines.begin() + line + 1, TextLine(new TextLineData()));
86 
87  // cases for modification:
88  // 1. line is wrapped in the middle
89  // 2. if empty line is wrapped, mark new line as modified
90  // 3. line-to-be-wrapped is already modified
91  if (position.column() > 0 || text.size() == 0 || m_lines.at(line)->markedAsModified()) {
92  m_lines.at(line + 1)->markAsModified(true);
93  } else if (m_lines.at(line)->markedAsSavedOnDisk()) {
94  m_lines.at(line + 1)->markAsSavedOnDisk(true);
95  }
96 
97  // perhaps remove some text from previous line and append it
98  if (position.column() < text.size()) {
99  // text from old line moved first to new one
100  m_lines.at(line + 1)->textReadWrite() = text.right(text.size() - position.column());
101 
102  // now remove wrapped text from old line
103  text.chop(text.size() - position.column());
104 
105  // mark line as modified
106  m_lines.at(line)->markAsModified(true);
107  }
108 
109  // fix all start lines
110  // we need to do this NOW, else the range update will FAIL!
111  // bug 313759
112  m_buffer->fixStartLines(fixStartLinesStartIndex);
113 
114  // notify the text history
115  m_buffer->history().wrapLine(position);
116 
117  // cursor and range handling below
118 
119  // no cursors will leave or join this block
120 
121  // no cursors in this block, no work to do..
122  if (m_cursors.empty()) {
123  return;
124  }
125 
126  // move all cursors on the line which has the text inserted
127  // remember all ranges modified, optimize for the standard case of a few ranges
128  QVarLengthArray<TextRange *, 32> changedRanges;
129  for (TextCursor *cursor : m_cursors) {
130  // skip cursors on lines in front of the wrapped one!
131  if (cursor->lineInBlock() < line) {
132  continue;
133  }
134 
135  // either this is simple, line behind the wrapped one
136  if (cursor->lineInBlock() > line) {
137  // patch line of cursor
138  cursor->m_line++;
139  }
140 
141  // this is the wrapped line
142  else {
143  // skip cursors with too small column
144  if (cursor->column() <= position.column()) {
145  if (cursor->column() < position.column() || !cursor->m_moveOnInsert) {
146  continue;
147  }
148  }
149 
150  // move cursor
151 
152  // patch line of cursor
153  cursor->m_line++;
154 
155  // patch column
156  cursor->m_column -= position.column();
157  }
158 
159  // remember range, if any, avoid double insert
160  auto range = cursor->kateRange();
161  if (range && !range->isValidityCheckRequired()) {
162  range->setValidityCheckRequired();
163  changedRanges.push_back(range);
164  }
165  }
166 
167  // we might need to invalidate ranges or notify about their changes
168  // checkValidity might trigger delete of the range!
169  for (TextRange *range : qAsConst(changedRanges)) {
170  range->checkValidity();
171  }
172 }
173 
174 void TextBlock::unwrapLine(int line, TextBlock *previousBlock, int fixStartLinesStartIndex)
175 {
176  // calc internal line
177  line = line - startLine();
178 
179  // two possiblities: either first line of this block or later line
180  if (line == 0) {
181  // we need previous block with at least one line
182  Q_ASSERT(previousBlock);
183  Q_ASSERT(previousBlock->lines() > 0);
184 
185  // move last line of previous block to this one, might result in empty block
186  TextLine oldFirst = m_lines.at(0);
187  int lastLineOfPreviousBlock = previousBlock->lines() - 1;
188  TextLine newFirst = previousBlock->m_lines.back();
189  m_lines[0] = newFirst;
190  previousBlock->m_lines.erase(previousBlock->m_lines.begin() + (previousBlock->lines() - 1));
191 
192  const int oldSizeOfPreviousLine = newFirst->text().size();
193  if (oldFirst->length() > 0) {
194  // append text
195  newFirst->textReadWrite().append(oldFirst->text());
196 
197  // mark line as modified, since text was appended
198  newFirst->markAsModified(true);
199  }
200 
201  // patch startLine of this block
202  --m_startLine;
203 
204  // fix all start lines
205  // we need to do this NOW, else the range update will FAIL!
206  // bug 313759
207  m_buffer->fixStartLines(fixStartLinesStartIndex);
208 
209  // notify the text history in advance
210  m_buffer->history().unwrapLine(startLine() + line, oldSizeOfPreviousLine);
211 
212  // cursor and range handling below
213 
214  // no cursors in this block and the previous one, no work to do..
215  if (m_cursors.empty() && previousBlock->m_cursors.empty()) {
216  return;
217  }
218 
219  // move all cursors because of the unwrapped line
220  // remember all ranges modified, optimize for the standard case of a few ranges
221  QVarLengthArray<TextRange *, 32> changedRanges;
222  for (TextCursor *cursor : m_cursors) {
223  // this is the unwrapped line
224  if (cursor->lineInBlock() == 0) {
225  // patch column
226  cursor->m_column += oldSizeOfPreviousLine;
227 
228  // remember range, if any, avoid double insert
229  auto range = cursor->kateRange();
230  if (range && !range->isValidityCheckRequired()) {
231  range->setValidityCheckRequired();
232  changedRanges.push_back(range);
233  }
234  }
235  }
236 
237  // move cursors of the moved line from previous block to this block now
238  for (auto it = previousBlock->m_cursors.begin(); it != previousBlock->m_cursors.end();) {
239  auto cursor = *it;
240  if (cursor->lineInBlock() == lastLineOfPreviousBlock) {
241  cursor->m_line = 0;
242  cursor->m_block = this;
243  m_cursors.insert(cursor);
244 
245  // remember range, if any, avoid double insert
246  auto range = cursor->kateRange();
247  if (range && !range->isValidityCheckRequired()) {
248  range->setValidityCheckRequired();
249  changedRanges.push_back(range);
250  }
251 
252  // remove from previous block
253  it = previousBlock->m_cursors.erase(it);
254  } else {
255  // keep in previous block
256  ++it;
257  }
258  }
259 
260  // fixup the ranges that might be effected, because they moved from last line to this block
261  // we might need to invalidate ranges or notify about their changes
262  // checkValidity might trigger delete of the range!
263  for (TextRange *range : qAsConst(changedRanges)) {
264  // update both blocks
265  updateRange(range);
266  previousBlock->updateRange(range);
267 
268  // afterwards check validity, might delete this range!
269  range->checkValidity();
270  }
271 
272  // be done
273  return;
274  }
275 
276  // easy: just move text to previous line and remove current one
277  const int oldSizeOfPreviousLine = m_lines.at(line - 1)->length();
278  const int sizeOfCurrentLine = m_lines.at(line)->length();
279  if (sizeOfCurrentLine > 0) {
280  m_lines.at(line - 1)->textReadWrite().append(m_lines.at(line)->text());
281  }
282 
283  const bool lineChanged = (oldSizeOfPreviousLine > 0 && m_lines.at(line - 1)->markedAsModified()) || (sizeOfCurrentLine > 0 && (oldSizeOfPreviousLine > 0 || m_lines.at(line)->markedAsModified()));
284  m_lines.at(line - 1)->markAsModified(lineChanged);
285  if (oldSizeOfPreviousLine == 0 && m_lines.at(line)->markedAsSavedOnDisk()) {
286  m_lines.at(line - 1)->markAsSavedOnDisk(true);
287  }
288 
289  m_lines.erase(m_lines.begin() + line);
290 
291  // fix all start lines
292  // we need to do this NOW, else the range update will FAIL!
293  // bug 313759
294  m_buffer->fixStartLines(fixStartLinesStartIndex);
295 
296  // notify the text history in advance
297  m_buffer->history().unwrapLine(startLine() + line, oldSizeOfPreviousLine);
298 
299  // cursor and range handling below
300 
301  // no cursors in this block, no work to do..
302  if (m_cursors.empty()) {
303  return;
304  }
305 
306  // move all cursors because of the unwrapped line
307  // remember all ranges modified, optimize for the standard case of a few ranges
308  QVarLengthArray<TextRange *, 32> changedRanges;
309  for (TextCursor *cursor : m_cursors) {
310  // skip cursors in lines in front of removed one
311  if (cursor->lineInBlock() < line) {
312  continue;
313  }
314 
315  // this is the unwrapped line
316  if (cursor->lineInBlock() == line) {
317  // patch column
318  cursor->m_column += oldSizeOfPreviousLine;
319  }
320 
321  // patch line of cursor
322  cursor->m_line--;
323 
324  // remember range, if any, avoid double insert
325  auto range = cursor->kateRange();
326  if (range && !range->isValidityCheckRequired()) {
327  range->setValidityCheckRequired();
328  changedRanges.push_back(range);
329  }
330  }
331 
332  // we might need to invalidate ranges or notify about their changes
333  // checkValidity might trigger delete of the range!
334  for (TextRange *range : qAsConst(changedRanges)) {
335  range->checkValidity();
336  }
337 }
338 
340 {
341  // calc internal line
342  int line = position.line() - startLine();
343 
344  // get text
345  QString &textOfLine = m_lines.at(line)->textReadWrite();
346  int oldLength = textOfLine.size();
347  m_lines.at(line)->markAsModified(true);
348 
349  // check if valid column
350  Q_ASSERT(position.column() >= 0);
351  Q_ASSERT(position.column() <= textOfLine.size());
352 
353  // insert text
354  textOfLine.insert(position.column(), text);
355 
356  // notify the text history
357  m_buffer->history().insertText(position, text.size(), oldLength);
358 
359  // cursor and range handling below
360 
361  // no cursors in this block, no work to do..
362  if (m_cursors.empty()) {
363  return;
364  }
365 
366  // move all cursors on the line which has the text inserted
367  // remember all ranges modified, optimize for the standard case of a few ranges
368  QVarLengthArray<TextRange *, 32> changedRanges;
369  for (TextCursor *cursor : m_cursors) {
370  // skip cursors not on this line!
371  if (cursor->lineInBlock() != line) {
372  continue;
373  }
374 
375  // skip cursors with too small column
376  if (cursor->column() <= position.column()) {
377  if (cursor->column() < position.column() || !cursor->m_moveOnInsert) {
378  continue;
379  }
380  }
381 
382  // patch column of cursor
383  if (cursor->m_column <= oldLength) {
384  cursor->m_column += text.size();
385  }
386 
387  // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode
388  else if (cursor->m_column < textOfLine.size()) {
389  cursor->m_column = textOfLine.size();
390  }
391 
392  // remember range, if any, avoid double insert
393  // we only need to trigger checkValidity later if the range has feedback or might be invalidated
394  auto range = cursor->kateRange();
395  if (range && !range->isValidityCheckRequired() && (range->feedback() || range->start().line() == range->end().line())) {
396  range->setValidityCheckRequired();
397  changedRanges.push_back(range);
398  }
399  }
400 
401  // we might need to invalidate ranges or notify about their changes
402  // checkValidity might trigger delete of the range!
403  for (TextRange *range : qAsConst(changedRanges)) {
404  range->checkValidity();
405  }
406 }
407 
408 void TextBlock::removeText(const KTextEditor::Range &range, QString &removedText)
409 {
410  // calc internal line
411  int line = range.start().line() - startLine();
412 
413  // get text
414  QString &textOfLine = m_lines.at(line)->textReadWrite();
415  int oldLength = textOfLine.size();
416 
417  // check if valid column
418  Q_ASSERT(range.start().column() >= 0);
419  Q_ASSERT(range.start().column() <= textOfLine.size());
420  Q_ASSERT(range.end().column() >= 0);
421  Q_ASSERT(range.end().column() <= textOfLine.size());
422 
423  // get text which will be removed
424  removedText = textOfLine.mid(range.start().column(), range.end().column() - range.start().column());
425 
426  // remove text
427  textOfLine.remove(range.start().column(), range.end().column() - range.start().column());
428  m_lines.at(line)->markAsModified(true);
429 
430  // notify the text history
431  m_buffer->history().removeText(range, oldLength);
432 
433  // cursor and range handling below
434 
435  // no cursors in this block, no work to do..
436  if (m_cursors.empty()) {
437  return;
438  }
439 
440  // move all cursors on the line which has the text removed
441  // remember all ranges modified, optimize for the standard case of a few ranges
442  QVarLengthArray<TextRange *, 32> changedRanges;
443  for (TextCursor *cursor : m_cursors) {
444  // skip cursors not on this line!
445  if (cursor->lineInBlock() != line) {
446  continue;
447  }
448 
449  // skip cursors with too small column
450  if (cursor->column() <= range.start().column()) {
451  continue;
452  }
453 
454  // patch column of cursor
455  if (cursor->column() <= range.end().column()) {
456  cursor->m_column = range.start().column();
457  } else {
458  cursor->m_column -= (range.end().column() - range.start().column());
459  }
460 
461  // remember range, if any, avoid double insert
462  // we only need to trigger checkValidity later if the range has feedback or might be invalidated
463  auto range = cursor->kateRange();
464  if (range && !range->isValidityCheckRequired() && (range->feedback() || range->start().line() == range->end().line())) {
465  range->setValidityCheckRequired();
466  changedRanges.push_back(range);
467  }
468  }
469 
470  // we might need to invalidate ranges or notify about their changes
471  // checkValidity might trigger delete of the range!
472  for (TextRange *range : qAsConst(changedRanges)) {
473  range->checkValidity();
474  }
475 }
476 
477 void TextBlock::debugPrint(int blockIndex) const
478 {
479  // print all blocks
480  for (size_t i = 0; i < m_lines.size(); ++i)
481  printf("%4d - %4lld : %4d : '%s'\n", blockIndex, (unsigned long long)startLine() + i, m_lines.at(i)->text().size(), qPrintable(m_lines.at(i)->text()));
482 }
483 
485 {
486  // half the block
487  int linesOfNewBlock = lines() - fromLine;
488 
489  // create and insert new block
490  TextBlock *newBlock = new TextBlock(m_buffer, startLine() + fromLine);
491 
492  // move lines
493  newBlock->m_lines.reserve(linesOfNewBlock);
494  for (size_t i = fromLine; i < m_lines.size(); ++i) {
495  newBlock->m_lines.push_back(m_lines.at(i));
496  }
497  m_lines.resize(fromLine);
498 
499  // move cursors
500  for (auto it = m_cursors.begin(); it != m_cursors.end();) {
501  auto cursor = *it;
502  if (cursor->lineInBlock() >= fromLine) {
503  cursor->m_line = cursor->lineInBlock() - fromLine;
504  cursor->m_block = newBlock;
505 
506  // add to new, remove from current
507  newBlock->m_cursors.insert(cursor);
508  it = m_cursors.erase(it);
509  } else {
510  // keep in current
511  ++it;
512  }
513  }
514 
515  // fix ALL ranges!
516  const QList<TextRange *> allRanges = m_uncachedRanges.values() + m_cachedLineForRanges.keys();
517  for (TextRange *range : qAsConst(allRanges)) {
518  // update both blocks
519  updateRange(range);
520  newBlock->updateRange(range);
521  }
522 
523  // return the new generated block
524  return newBlock;
525 }
526 
528 {
529  // move cursors, do this first, now still lines() count is correct for target
530  for (TextCursor *cursor : m_cursors) {
531  cursor->m_line = cursor->lineInBlock() + targetBlock->lines();
532  cursor->m_block = targetBlock;
533  targetBlock->m_cursors.insert(cursor);
534  }
535  m_cursors.clear();
536 
537  // move lines
538  targetBlock->m_lines.reserve(targetBlock->lines() + lines());
539  for (size_t i = 0; i < m_lines.size(); ++i) {
540  targetBlock->m_lines.push_back(m_lines.at(i));
541  }
542  m_lines.clear();
543 
544  // fix ALL ranges!
545  const QList<TextRange *> allRanges = m_uncachedRanges.values() + m_cachedLineForRanges.keys();
546  for (TextRange *range : qAsConst(allRanges)) {
547  // update both blocks
548  updateRange(range);
549  targetBlock->updateRange(range);
550  }
551 }
552 
554 {
555  // kill cursors, if not belonging to a range
556  // we can do in-place editing of the current set of cursors as
557  // we remove them before deleting
558  for (auto it = m_cursors.begin(); it != m_cursors.end();) {
559  auto cursor = *it;
560  if (!cursor->kateRange()) {
561  // remove it and advance to next element
562  it = m_cursors.erase(it);
563 
564  // delete after cursor is gone from the set
565  // else the destructor will modify it!
566  delete cursor;
567  } else {
568  // keep this cursor
569  ++it;
570  }
571  }
572 
573  // kill lines
574  m_lines.clear();
575 }
576 
578 {
579  // move cursors, if not belonging to a range
580  // we can do in-place editing of the current set of cursors
581  for (auto it = m_cursors.begin(); it != m_cursors.end();) {
582  auto cursor = *it;
583  if (!cursor->kateRange()) {
584  cursor->m_column = 0;
585  cursor->m_line = 0;
586  cursor->m_block = targetBlock;
587  targetBlock->m_cursors.insert(cursor);
588 
589  // remove it and advance to next element
590  it = m_cursors.erase(it);
591  } else {
592  // keep this cursor
593  ++it;
594  }
595  }
596 
597  // kill lines
598  m_lines.clear();
599 }
600 
602 {
603  // mark all modified lines as saved
604  for (auto &textLine : m_lines) {
605  if (textLine->markedAsModified()) {
606  textLine->markAsSavedOnDisk(true);
607  }
608  }
609 }
610 
612 {
613  // get some simple facts about our nice range
614  const int startLine = range->startInternal().lineInternal();
615  const int endLine = range->endInternal().lineInternal();
616  const bool isSingleLine = startLine == endLine;
617 
618  // perhaps remove range and be done
619  if ((endLine < m_startLine) || (startLine >= (m_startLine + lines()))) {
620  removeRange(range);
621  return;
622  }
623 
624  // The range is still a single-line range, and is still cached to the correct line.
625  if (isSingleLine && m_cachedLineForRanges.contains(range) && (m_cachedLineForRanges.value(range) == startLine - m_startLine)) {
626  return;
627  }
628 
629  // The range is still a multi-line range, and is already in the correct set.
630  if (!isSingleLine && m_uncachedRanges.contains(range)) {
631  return;
632  }
633 
634  // remove, if already there!
635  removeRange(range);
636 
637  // simple case: multi-line range
638  if (!isSingleLine) {
639  // The range cannot be cached per line, as it spans multiple lines
640  m_uncachedRanges.insert(range);
641  return;
642  }
643 
644  // The range is contained by a single line, put it into the line-cache
645  const int lineOffset = startLine - m_startLine;
646 
647  // enlarge cache if needed
648  if (m_cachedRangesForLine.size() <= lineOffset) {
649  m_cachedRangesForLine.resize(lineOffset + 1);
650  }
651 
652  // insert into mapping
653  m_cachedRangesForLine[lineOffset].insert(range);
654  m_cachedLineForRanges[range] = lineOffset;
655 }
656 
658 {
659  // uncached range? remove it and be done
660  if (m_uncachedRanges.remove(range)) {
661  // must be only uncached!
662  Q_ASSERT(!m_cachedLineForRanges.contains(range));
663  return;
664  }
665 
666  // cached range?
667  QHash<TextRange *, int>::iterator it = m_cachedLineForRanges.find(range);
668  if (it != m_cachedLineForRanges.end()) {
669  // must be only cached!
670  Q_ASSERT(!m_uncachedRanges.contains(range));
671 
672  // query the range from cache, must be there
673  Q_ASSERT(m_cachedRangesForLine.at(*it).contains(range));
674 
675  // remove it and be done
676  m_cachedRangesForLine[*it].remove(range);
677  m_cachedLineForRanges.erase(it);
678  return;
679  }
680 
681  // else: range was not for this block, just do nothing, removeRange should be "safe" to use
682 }
683 
684 }
const TextCursor & startInternal() const
Non-virtual version of start(), which is faster.
QString & append(QChar ch)
void wrapLine(const KTextEditor::Cursor &position, int fixStartLinesStartIndex)
Wrap line at given cursor position.
QSharedPointer< T > create(Args &&...args)
void setStartLine(int startLine)
Set start line of this block.
void removeRange(TextRange *range)
Remove a range from this block.
void unwrapLine(int line, TextBlock *previousBlock, int fixStartLinesStartIndex)
Unwrap given line.
int lines() const
Number of lines in this block.
Definition: katetextblock.h:82
int size() const const
void push_back(const T &t)
QString & remove(int position, int n)
int lineInternal() const
Non-virtual version of line(), which is faster.
The Cursor represents a position in a Document.
Definition: cursor.h:71
void chop(int n)
int startLine() const
Start line of this block.
Definition: katetextblock.h:49
Class representing a single text line.
Definition: katetextline.h:25
void deleteBlockContent()
Delete the block content, delete all lines and delete all cursors not bound to ranges.
const TextCursor & endInternal() const
Nonvirtual version of end(), which is faster.
void debugPrint(int blockIndex) const
Debug output, print whole block content with line numbers and line length.
TextBlock(TextBuffer *buffer, int startLine)
Construct an empty text block.
~TextBlock()
Destruct the text block.
void updateRange(TextRange *range)
Update a range from this block.
void clearBlockContent(TextBlock *targetBlock)
Clear the block content, delete all lines, move all cursors not bound to range to given block at 0...
QString & insert(int position, QChar ch)
void markModifiedLinesAsSaved()
Flag all modified text lines as saved on disk.
constexpr int column() const Q_DECL_NOEXCEPT
Retrieve the column on which this cursor is situated.
Definition: cursor.h:203
constexpr Cursor start() const Q_DECL_NOEXCEPT
Get the start position of this range.
void removeText(const KTextEditor::Range &range, QString &removedText)
Remove text at given range.
QString right(int n) const const
An object representing a section of text, from one Cursor to another.
void appendLine(const QString &textOfLine)
Append a new line with given text.
void clearLines()
Clear the lines.
QString mid(int position, int n) const const
void mergeBlock(TextBlock *targetBlock)
Merge this block with given one, the given one must be a direct predecessor.
Class representing a &#39;clever&#39; text cursor.
constexpr Cursor end() const Q_DECL_NOEXCEPT
Get the end position of this range.
constexpr int line() const Q_DECL_NOEXCEPT
Retrieve the line on which this cursor is situated.
Definition: cursor.h:185
TextLine line(int line) const
Retrieve a text line.
Class representing a text buffer.
TextHistory & history()
TextHistory of this buffer.
void text(QString &text) const
Retrieve text of block.
Class representing a &#39;clever&#39; text range.
Definition: katetextrange.h:32
void insertText(const KTextEditor::Cursor &position, const QString &text)
Insert text at given cursor position.
Class representing a text block.
Definition: katetextblock.h:30
TextBlock * splitBlock(int fromLine)
Split given block.
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon Nov 30 2020 22:58:29 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.