diff --git a/autotests/src/completion_test.h b/autotests/src/completion_test.h --- a/autotests/src/completion_test.h +++ b/autotests/src/completion_test.h @@ -56,6 +56,7 @@ void testJumpToListBottomAfterCursorUpWhileAtTop(); void testAbbrevAndContainsMatching(); void testAbbreviationEngine(); + void testCompletionBlockSelection(); void benchAbbreviationEngineNormalCase(); void benchAbbreviationEngineWorstCase(); void benchAbbreviationEngineGoodCase(); diff --git a/autotests/src/completion_test.cpp b/autotests/src/completion_test.cpp --- a/autotests/src/completion_test.cpp +++ b/autotests/src/completion_test.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -170,6 +171,53 @@ verifyCompletionAborted(m_view); } +void CompletionTest::testCompletionBlockSelection() +{ + QString textBlock = QStringLiteral("First line contains some words\n" + "2nd line consists of some text\n" + "thrice is just a space filler\n" + "a fourth makes them four"); + + m_doc->setText(textBlock); + m_view->setBlockSelection(true); + KTextEditor::Range range(Cursor(0, 0), Cursor(2, 4)); + m_view->setSelection(range); + const int lines = m_doc->lines(); + // get the text on the fourth line to make sure the + // completion doesn't touch it + const QString &text4 = m_view->doc()->line(3); + + // test with remove tail enabled + m_view->config()->setWordCompletionRemoveTail(true); + m_view->doc()->typeChars(m_view, QStringLiteral("fil")); + QTest::qWait(1000); + // select the first completion, "filler" + QTest::keyClick(m_view->focusProxy(), Qt::Key_Return); + + QCOMPARE(lines, m_doc->lines()); + + for (int i = m_view->selectionRange().start().line(); i <= m_view->selectionRange().end().line(); ++i) { + const QString &line = m_view->doc()->line(i); + QVERIFY(line.startsWith(QStringLiteral("filler "))); + } + + QCOMPARE(m_view->doc()->line(3), text4); + + // test with remove tail disabled + m_doc->setText(textBlock); + m_view->setSelection(range); + m_view->config()->setWordCompletionRemoveTail(false); + m_view->doc()->typeChars(m_view, QStringLiteral("fil")); + QTest::qWait(1000); + QTest::keyClick(m_view->focusProxy(), Qt::Key_Return); + + const int startLine = m_view->selectionRange().start().line(); + + QVERIFY(m_view->doc()->line(startLine).startsWith(QStringLiteral("fillert "))); + QVERIFY(m_view->doc()->line(startLine + 1).startsWith(QStringLiteral("fillerline "))); + QVERIFY(m_view->doc()->line(startLine + 2).startsWith(QStringLiteral("fillerce "))); +} + void CompletionTest::testCustomRange1() { m_doc->setText(QStringLiteral("$aa bb cc\ndd")); diff --git a/src/completion/katewordcompletion.cpp b/src/completion/katewordcompletion.cpp --- a/src/completion/katewordcompletion.cpp +++ b/src/completion/katewordcompletion.cpp @@ -211,6 +211,9 @@ { QSet result; const int minWordSize = qMax(2, qobject_cast(view)->config()->wordCompletionMinimalWordLength()); + + const bool blockSelection = view->blockSelection() && view->selection(); + const int lines = view->document()->lines(); for (int line = 0; line < lines; line++) { const QString &text = view->document()->line(line); @@ -224,10 +227,21 @@ if ((! c.isLetterOrNumber() && c != QLatin1Char('_')) || (offset == end - 1 && offset++)) { if (offset - wordBegin > minWordSize && (line != range.end().line() || offset != range.end().column())) { /** - * don't add the word we are inside with cursor! + * Don't add the word we are inside with cursor! + * Handle completion with block selection, by not adding + * the words the cursor is inside on each line in the selection */ - if (!cursorLine || (view->cursorPosition().column() < wordBegin || view->cursorPosition().column() > offset)) { - result.insert(text.mid(wordBegin, offset - wordBegin)); + if (blockSelection) { + KTextEditor::Range selectionRange = view->selectionRange(); + const int startLine = selectionRange.start().line(); + const int endLine = selectionRange.end().line(); + if ((line < startLine || line > endLine) || (view->cursorPosition().column() < wordBegin || view->cursorPosition().column() > offset)) { + result.insert(text.mid(wordBegin, offset - wordBegin)); + } + } else { + if (!cursorLine || (view->cursorPosition().column() < wordBegin || view->cursorPosition().column() > offset)) { + result.insert(text.mid(wordBegin, offset - wordBegin)); + } } } wordBegin = offset + 1; @@ -247,31 +261,56 @@ ) const { KTextEditor::ViewPrivate *v = qobject_cast (view); + + const bool blockSelection = v->blockSelection() && v->selection(); + + KTextEditor::Range range = blockSelection? KTextEditor::Range(word.start(), v->selectionRange().end()) : word; + if (v->config()->wordCompletionRemoveTail()) { - int tailStart = word.end().column(); - const QString &line = view->document()->line(word.end().line()); - int tailEnd = line.length(); - for (int i = word.end().column(); i < tailEnd; ++i) { - // Letters, numbers and underscore are part of a word! - /// \todo Introduce configurable \e word-separators?? - if (!line[i].isLetterOrNumber() && line[i] != QLatin1Char('_')) { - tailEnd = i; + v->doc()->replaceText(range, m_matches.at(index.row()), blockSelection); + v->doc()->editEnd(); + + if (blockSelection) { + // get the block selection range after the above replaceText() + KTextEditor::Range selectionRange = v->selectionRange(); + const int selectionEndColumn = selectionRange.end().column(); + v->doc()->editStart(); + for (int i = selectionRange.start().line(); i <= selectionRange.end().line(); ++i) { + const QString &line = v->doc()->line(i); + int tailEnd = line.length(); + for (int j = selectionEndColumn; j < tailEnd; ++j) { + // Letters, numbers and underscore are part of a word! + /// \todo Introduce configurable \e word-separators?? + if (!line[j].isLetterOrNumber() && line[j] != QLatin1Char('_')) { + tailEnd = j; + } + } + v->doc()->removeText(KTextEditor::Range(KTextEditor::Cursor(i, selectionEndColumn), KTextEditor::Cursor(i, tailEnd)), blockSelection); + } + } else { + int tailStart = word.end().column(); + const QString &line = view->document()->line(word.end().line()); + int tailEnd = line.length(); + for (int i = word.end().column(); i < tailEnd; ++i) { + // Letters, numbers and underscore are part of a word! + /// \todo Introduce configurable \e word-separators?? + if (!line[i].isLetterOrNumber() && line[i] != QLatin1Char('_')) { + tailEnd = i; + } } - } - int sizeDiff = m_matches.at(index.row()).size() - (word.end().column() - word.start().column()); + int sizeDiff = m_matches.at(index.row()).size() - (word.end().column() - word.start().column()); - tailStart += sizeDiff; - tailEnd += sizeDiff; + tailStart += sizeDiff; + tailEnd += sizeDiff; - KTextEditor::Range tail(KTextEditor::Cursor(word.start().line(), tailStart), KTextEditor::Cursor(word.end().line(), tailEnd)); + KTextEditor::Range tail(KTextEditor::Cursor(word.start().line(), tailStart), KTextEditor::Cursor(word.end().line(), tailEnd)); - view->document()->replaceText(word, m_matches.at(index.row())); - v->doc()->editEnd(); - v->doc()->editStart(); - view->document()->replaceText(tail, QString()); + v->doc()->editStart(); + view->document()->replaceText(tail, QString()); + } } else { - view->document()->replaceText(word, m_matches.at(index.row())); + v->doc()->replaceText(range, m_matches.at(index.row()), blockSelection); } } diff --git a/src/document/katedocument.h b/src/document/katedocument.h --- a/src/document/katedocument.h +++ b/src/document/katedocument.h @@ -779,6 +779,12 @@ */ bool typeChars(KTextEditor::ViewPrivate *type, const QString &realChars); + /** + * Helper function to insert text in block selection mode, by inserting the + * text on every line in the block selection. + */ + bool insertTextInBlock(KTextEditor::ViewPrivate *view, const QString &text); + /** * gets the last line number (lines() - 1) */ diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -805,6 +805,31 @@ return insertText(position, textLines.join(QStringLiteral("\n")), block); } +bool KTextEditor::DocumentPrivate::insertTextInBlock(KTextEditor::ViewPrivate *view, const QString &s) +{ + if (!isReadWrite()) { + return false; + } + + KTextEditor::Range selectionRange = view->selectionRange(); + + const int startLine = qMax(0, selectionRange.start().line()); + const int endLine = qMin(selectionRange.end().line(), lastLine()); + const int column = toVirtualColumn(selectionRange.end()); + for (int line = endLine; line >= startLine; --line) { + editInsertText(line, fromVirtualColumn(line, column), s); + } + + const int newSelectionColumn = toVirtualColumn(view->cursorPosition()); + const int startColumn = fromVirtualColumn(selectionRange.start().line(), newSelectionColumn); + const int endColumn = fromVirtualColumn(selectionRange.end().line(), newSelectionColumn); + + selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), startColumn), + KTextEditor::Cursor(selectionRange.end().line(), endColumn)); + view->setSelection(selectionRange); + return true; +} + bool KTextEditor::DocumentPrivate::removeText(const KTextEditor::Range &_range, bool block) { KTextEditor::Range range = _range; @@ -3097,17 +3122,7 @@ } if (multiLineBlockMode) { - KTextEditor::Range selectionRange = view->selectionRange(); - const int startLine = qMax(0, selectionRange.start().line()); - const int endLine = qMin(selectionRange.end().line(), lastLine()); - const int column = toVirtualColumn(selectionRange.end()); - for (int line = endLine; line >= startLine; --line) { - editInsertText(line, fromVirtualColumn(line, column), chars); - } - int newSelectionColumn = toVirtualColumn(view->cursorPosition()); - selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)) - , KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn))); - view->setSelection(selectionRange); + insertTextInBlock(view, chars); } else { chars = eventuallyReplaceTabs(view->cursorPosition(), chars); insertText(view->cursorPosition(), chars); @@ -5175,7 +5190,13 @@ // TODO more efficient? editStart(); bool changed = removeText(range, block); - changed |= insertText(range.start(), s, block); + + if (block && activeView() != nullptr && activeView()->selection()) { + changed |= insertTextInBlock(static_cast(activeView()), s); + } else { + changed |= insertText(range.start(), s, block); + } + editEnd(); return changed; }