diff --git a/autotests/src/katedocument_test.h b/autotests/src/katedocument_test.h --- a/autotests/src/katedocument_test.h +++ b/autotests/src/katedocument_test.h @@ -35,6 +35,7 @@ private Q_SLOTS: void testWordWrap(); + void testWrapParagraph(); void testReplaceQStringList(); void testMovingInterfaceSignals(); diff --git a/autotests/src/katedocument_test.cpp b/autotests/src/katedocument_test.cpp --- a/autotests/src/katedocument_test.cpp +++ b/autotests/src/katedocument_test.cpp @@ -128,6 +128,26 @@ QCOMPARE(c->toCursor(), Cursor(1, 15)); } +void KateDocumentTest::testWrapParagraph() +{ + // Each paragraph must be kept as an own but re-wrapped nicely + KTextEditor::DocumentPrivate doc(false, false); + doc.setWordWrapAt(30); // Keep needed test data small + + const QString before = QLatin1String("aaaaa a aaaa\naaaaa aaa aa aaaa aaaa \naaaa a aaa aaaaaaa a aaaa\n\nxxxxx x\nxxxx xxxxx\nxxx xx xxxx \nxxxx xxxx x xxx xxxxxxx x xxxx"); + const QString after = QLatin1String("aaaaa a aaaa aaaaa aaa aa aaaa \naaaa aaaa a aaa aaaaaaa a aaaa\n\nxxxxx x xxxx xxxxx xxx xx xxxx \nxxxx xxxx x xxx xxxxxxx x xxxx"); + + doc.setWordWrap(false); // First we try with disabled hard wrap + doc.setText(before); + doc.wrapParagraph(0, doc.lines() - 1); + QCOMPARE(doc.text(), after); + + doc.setWordWrap(true); // Test again with enabled hard wrap, that had cause trouble due to twice wrapping + doc.setText(before); + doc.wrapParagraph(0, doc.lines() - 1); + QCOMPARE(doc.text(), after); +} + void KateDocumentTest::testReplaceQStringList() { KTextEditor::DocumentPrivate doc(false, false); diff --git a/src/document/katedocument.h b/src/document/katedocument.h --- a/src/document/katedocument.h +++ b/src/document/katedocument.h @@ -267,6 +267,7 @@ * @return true on success */ bool editInsertText(int line, int col, const QString &s); + /** * Remove a string in the given line/column * @param line line number @@ -297,6 +298,7 @@ * @return true on success */ bool editWrapLine(int line, int col, bool newLine = true, bool *newLineAdded = nullptr); + /** * Unwrap @p line. If @p removeLine is true, we force to join the lines. If * @p removeLine is true, @p length is ignored (eg not needed). @@ -313,6 +315,7 @@ * @return true on success */ bool editInsertLine(int line, const QString &s); + /** * Remove a line * @param line line number @@ -323,12 +326,25 @@ bool editRemoveLines(int from, int to); /** - * Remove a line + * Warp a line * @param startLine line to begin wrapping * @param endLine line to stop wrapping * @return true on success */ bool wrapText(int startLine, int endLine); + + /** + * Wrap lines touched by the selection with respect of existing paragraphs. + * To do so will the paragraph prior to the wrap joined as one single line + * which cause an almost perfect wrapped paragraph as long as there are no + * unneeded spaces exist or some formatting like this comment block. + * Without any selection the current line is wrapped. + * Empty lines around each paragraph are untouched. + * @param first line to begin wrapping + * @param last line to stop wrapping + * @return true on success + */ + bool wrapParagraph(int first, int last); //END LINE BASED INSERT/REMOVE STUFF Q_SIGNALS: diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -1214,6 +1214,67 @@ return true; } +bool KTextEditor::DocumentPrivate::wrapParagraph(int first, int last) +{ + if (first == last) { + return wrapText(first, last); + } + + if (first < 0 || last < first) { + return false; + } + + if (last >= lines() || first > last) { + return false; + } + + if (!isReadWrite()) { + return false; + } + + editStart(); + + // Because we shrink and expand lines, we need to track the working set by powerful "MovingStuff" + std::unique_ptr range(newMovingRange(KTextEditor::Range(first, 0, last, 0))); + std::unique_ptr curr(newMovingCursor(KTextEditor::Cursor(range->start()))); + + // Scan the selected range for paragraphs, whereas each empty line trigger a new paragraph + for (int line = first; line <= range->end().line(); ++line) { + // Is our first line a somehow filled line? + if(plainKateTextLine(first)->firstChar() < 0) { + // Fast forward to first non empty line + ++first; + curr->setPosition(curr->line() + 1, 0); + continue; + } + + // Is our current line a somehow filled line? If not, wrap the paragraph + if (plainKateTextLine(line)->firstChar() < 0) { + curr->setPosition(line, 0); // Set on empty line + joinLines(first, line - 1); + // Don't wrap twice! That may cause a bad result + if (!wordWrap()) { + wrapText(first, first); + } + first = curr->line() + 1; + line = first; + } + } + + // If there was no paragraph, we need to wrap now + bool needWrap = (curr->line() != range->end().line()); + if (needWrap && plainKateTextLine(first)->firstChar() != -1) { + joinLines(first, range->end().line()); + // Don't wrap twice! That may cause a bad result + if (!wordWrap()) { + wrapText(first, first); + } + } + + editEnd(); + return true; +} + bool KTextEditor::DocumentPrivate::editInsertText(int line, int col, const QString &s) { // verbose debug diff --git a/src/view/kateview.h b/src/view/kateview.h --- a/src/view/kateview.h +++ b/src/view/kateview.h @@ -135,7 +135,8 @@ private Q_SLOTS: /** - * internal use, apply word wrap + * Wrap lines touched by the selection with respect of existing paragraphs. + * Work is done by KTextEditor::DocumentPrivate::wrapParagraph */ void applyWordWrap(); diff --git a/src/view/kateview.cpp b/src/view/kateview.cpp --- a/src/view/kateview.cpp +++ b/src/view/kateview.cpp @@ -499,9 +499,9 @@ a = ac->addAction(QStringLiteral("tools_apply_wordwrap")); a->setText(i18n("Apply &Word Wrap")); - a->setWhatsThis(i18n("Use this command to wrap all lines of the current document which are longer than the width of the" - " current view, to fit into this view.

This is a static word wrap, meaning it is not updated" - " when the view is resized.")); + a->setWhatsThis(i18n("Use this to wrap the current line, or to reformat the selected lines as paragraph, " + "to fit the 'Wrap words at' setting in the configuration dialog.

" + "This is a static word wrap, meaning the document is changed.")); connect(a, SIGNAL(triggered(bool)), SLOT(applyWordWrap())); a = ac->addAction(QStringLiteral("tools_cleanIndent")); @@ -697,7 +697,8 @@ a = m_toggleDynWrap = toggleAction = new KToggleAction(i18n("&Dynamic Word Wrap"), this); ac->addAction(QStringLiteral("view_dynamic_word_wrap"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F10)); - a->setWhatsThis(i18n("If this option is checked, the text lines will be wrapped at the view border on the screen.")); + a->setWhatsThis(i18n("If this option is checked, the text lines will be wrapped at the view border on the screen.

" + "This is only a view option, meaning the document will not changed.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleDynWordWrap())); a = m_setDynWrapIndicators = new KSelectAction(i18n("Dynamic Word Wrap Indicators"), this); @@ -2338,11 +2339,16 @@ void KTextEditor::ViewPrivate::applyWordWrap() { - if (selection()) { - doc()->wrapText(selectionRange().start().line(), selectionRange().end().line()); - } else { - doc()->wrapText(0, doc()->lastLine()); + int first = selectionRange().start().line(); + int last = selectionRange().end().line(); + + if (first == last) { + // Either no selection or only one line selected, wrap only the current line + first = cursorPosition().line(); + last = first; } + + doc()->wrapParagraph(first, last); } //END