diff --git a/.clang-format b/.clang-format index a02f0242..3bae5c76 100644 --- a/.clang-format +++ b/.clang-format @@ -1,88 +1,91 @@ --- # SPDX-License-Identifier: MIT # # Copyright (C) 2019 Christoph Cullmann # Copyright (C) 2019 Gernot Gebhard # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Style for C++ Language: Cpp # base is WebKit coding style: http://www.webkit.org/coding/coding-style.html # below are only things set that diverge from this style! BasedOnStyle: WebKit # enforce C++11 (e.g. for std::vector> Standard: Cpp11 # 4 spaces indent TabWidth: 4 # 3 * 80 wide lines ColumnLimit: 240 -# sematics shall be grouped via empty lines +# sort includes inside line separated groups SortIncludes: true # break before braces on function, namespace and class definitions. BreakBeforeBraces: Linux # CrlInstruction *a; PointerAlignment: Right # horizontally aligns arguments after an open bracket. AlignAfterOpenBracket: Align # align trailing comments AlignTrailingComments: true # don't move all parameters to new line AllowAllParametersOfDeclarationOnNextLine: false # no single line functions AllowShortFunctionsOnASingleLine: None # always break before you encounter multi line strings AlwaysBreakBeforeMultilineStrings: true # don't move arguments to own lines if they are not all on the same BinPackArguments: false # don't move parameters to own lines if they are not all on the same BinPackParameters: false # don't break binary ops BreakBeforeBinaryOperators: None -# don't break tenary ops +# don't break ternary ops BreakBeforeTernaryOperators: false # format C++11 braced lists like function calls Cpp11BracedListStyle: true -# indent case labels one level -IndentCaseLabels: true - # remove empty lines KeepEmptyLinesAtTheStartOfBlocks: false # no namespace indentation to keep indent level low NamespaceIndentation: None + +# we use template< without space. +SpaceAfterTemplateKeyword: false + +# macros for which the opening brace stays attached. +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ] diff --git a/autotests/src/codecompletiontestmodel.cpp b/autotests/src/codecompletiontestmodel.cpp index fc468fdd..f333876d 100644 --- a/autotests/src/codecompletiontestmodel.cpp +++ b/autotests/src/codecompletiontestmodel.cpp @@ -1,205 +1,205 @@ /* This file is part of the KDE project Copyright (C) 2005 Hamish Rodda This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "codecompletiontestmodel.h" #include #include #include #include #include CodeCompletionTestModel::CodeCompletionTestModel(KTextEditor::View *parent, const QString &startText) : KTextEditor::CodeCompletionModel(parent) , m_startText(startText) , m_autoStartText(m_startText.isEmpty()) { setRowCount(40); Q_ASSERT(cc()); cc()->setAutomaticInvocationEnabled(true); cc()->unregisterCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); // would add additional items, we don't want that in tests cc()->registerCompletionModel(this); } // Fake a series of completions QVariant CodeCompletionTestModel::data(const QModelIndex &index, int role) const { switch (role) { - case Qt::DisplayRole: - if (index.row() < rowCount() / 2) - switch (index.column()) { - case Prefix: - switch (index.row() % 3) { - default: - return "void "; - case 1: - return "const QString& "; - case 2: - if (index.row() % 6) { - return "inline virtual bool "; - } - return "virtual bool "; - } - - case Scope: - switch (index.row() % 4) { - default: - return QString(); - case 1: - return "KTextEditor::"; - case 2: - return "::"; - case 3: - return "std::"; - } - - case Name: - return QString(m_startText + QString("%1%2%3").arg(QChar('a' + (index.row() % 3))).arg(QChar('a' + index.row())).arg(index.row())); - - case Arguments: - switch (index.row() % 5) { - default: - return "()"; - case 1: - return "(bool trigger)"; - case 4: - return "(const QString& name, Qt::CaseSensitivity cs)"; - case 5: - return "(int count)"; - } - - case Postfix: - switch (index.row() % 3) { - default: - return " const"; - case 1: - return " KDE_DEPRECATED"; - case 2: - return ""; - } + case Qt::DisplayRole: + if (index.row() < rowCount() / 2) + switch (index.column()) { + case Prefix: + switch (index.row() % 3) { + default: + return "void "; + case 1: + return "const QString& "; + case 2: + if (index.row() % 6) { + return "inline virtual bool "; + } + return "virtual bool "; } - else - switch (index.column()) { - case Prefix: - switch (index.row() % 3) { - default: - return "void "; - case 1: - return "const QString "; - case 2: - return "bool "; - } - - case Scope: - switch (index.row() % 4) { - default: - return QString(); - case 1: - return "KTextEditor::"; - case 2: - return "::"; - case 3: - return "std::"; - } - - case Name: - return QString(m_startText + QString("%1%2%3").arg(QChar('a' + (index.row() % 3))).arg(QChar('a' + index.row())).arg(index.row())); - - default: - return ""; + + case Scope: + switch (index.row() % 4) { + default: + return QString(); + case 1: + return "KTextEditor::"; + case 2: + return "::"; + case 3: + return "std::"; } - break; - case Qt::DecorationRole: - break; + case Name: + return QString(m_startText + QString("%1%2%3").arg(QChar('a' + (index.row() % 3))).arg(QChar('a' + index.row())).arg(index.row())); - case CompletionRole: { - CompletionProperties p; - if (index.row() < rowCount() / 2) { - p |= Function; - } else { - p |= Variable; + case Arguments: + switch (index.row() % 5) { + default: + return "()"; + case 1: + return "(bool trigger)"; + case 4: + return "(const QString& name, Qt::CaseSensitivity cs)"; + case 5: + return "(int count)"; + } + + case Postfix: + switch (index.row() % 3) { + default: + return " const"; + case 1: + return " KDE_DEPRECATED"; + case 2: + return ""; + } } - switch (index.row() % 3) { - case 0: - p |= Const | Public; - break; + else + switch (index.column()) { + case Prefix: + switch (index.row() % 3) { + default: + return "void "; case 1: - p |= Protected; - break; + return "const QString "; case 2: - p |= Private; - break; + return "bool "; + } + + case Scope: + switch (index.row() % 4) { + default: + return QString(); + case 1: + return "KTextEditor::"; + case 2: + return "::"; + case 3: + return "std::"; + } + + case Name: + return QString(m_startText + QString("%1%2%3").arg(QChar('a' + (index.row() % 3))).arg(QChar('a' + index.row())).arg(index.row())); + + default: + return ""; } - return (int)p; + break; + + case Qt::DecorationRole: + break; + + case CompletionRole: { + CompletionProperties p; + if (index.row() < rowCount() / 2) { + p |= Function; + } else { + p |= Variable; + } + switch (index.row() % 3) { + case 0: + p |= Const | Public; + break; + case 1: + p |= Protected; + break; + case 2: + p |= Private; + break; } + return (int)p; + } - case ScopeIndex: - return (index.row() % 4) - 1; + case ScopeIndex: + return (index.row() % 4) - 1; } return QVariant(); } KTextEditor::View *CodeCompletionTestModel::view() const { return static_cast(const_cast(QObject::parent())); } KTextEditor::CodeCompletionInterface *CodeCompletionTestModel::cc() const { return dynamic_cast(const_cast(QObject::parent())); } void CodeCompletionTestModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType invocationType) { Q_UNUSED(invocationType) if (m_autoStartText) { m_startText = view->document()->text(KTextEditor::Range(range.start(), view->cursorPosition())); } qDebug() << m_startText; } AbbreviationCodeCompletionTestModel::AbbreviationCodeCompletionTestModel(KTextEditor::View *parent, const QString &startText) : CodeCompletionTestModel(parent, startText) { m_items << "SomeCoolAbbreviation" << "someCoolAbbreviation" << "sca" << "SCA"; m_items << "some_cool_abbreviation" << "Some_Cool_Abbreviation"; m_items << "thisContainsSomeWord" << "this_contains_some_word" << "thiscontainssomeword"; m_items << "notmatchedbecausemissingcaps" << "not_m_atch_ed_because_underscores"; setRowCount(m_items.size()); } QVariant AbbreviationCodeCompletionTestModel::data(const QModelIndex &index, int role) const { if (index.column() == Name && role == Qt::DisplayRole) { return m_items[index.row()]; } return QVariant(); } diff --git a/autotests/src/completion_test.cpp b/autotests/src/completion_test.cpp index a7c8aa68..47f1f8ab 100644 --- a/autotests/src/completion_test.cpp +++ b/autotests/src/completion_test.cpp @@ -1,498 +1,494 @@ /* This file is part of the KDE libraries Copyright (C) 2008 Niko Sams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "completion_test.h" #include "codecompletiontestmodels.h" //#include "codecompletiontestmodels.moc" #include #include #include #include #include #include #include #include #include #include #include QTEST_MAIN(CompletionTest) using namespace KTextEditor; int countItems(KateCompletionModel *model) { const int topLevel = model->rowCount(QModelIndex()); if (!model->hasGroups()) { return topLevel; } int ret = 0; for (int i = 0; i < topLevel; ++i) { ret += model->rowCount(model->index(i, 0)); } return ret; } static void verifyCompletionStarted(KTextEditor::ViewPrivate *view) { const QDateTime startTime = QDateTime::currentDateTime(); while (startTime.msecsTo(QDateTime::currentDateTime()) < 1000) { QApplication::processEvents(); if (view->completionWidget()->isCompletionActive()) { break; } } QVERIFY(view->completionWidget()->isCompletionActive()); } static void verifyCompletionAborted(KTextEditor::ViewPrivate *view) { const QDateTime startTime = QDateTime::currentDateTime(); while (startTime.msecsTo(QDateTime::currentDateTime()) < 1000) { QApplication::processEvents(); if (!view->completionWidget()->isCompletionActive()) { break; } } QVERIFY(!view->completionWidget()->isCompletionActive()); } static void invokeCompletionBox(KTextEditor::ViewPrivate *view) { view->userInvokedCompletion(); verifyCompletionStarted(view); } void CompletionTest::init() { KTextEditor::EditorPrivate::enableUnitTestMode(); Editor *editor = KTextEditor::Editor::instance(); QVERIFY(editor); m_doc = editor->createDocument(this); QVERIFY(m_doc); m_doc->setText(QStringLiteral("aa bb cc\ndd")); KTextEditor::View *v = m_doc->createView(nullptr); QApplication::setActiveWindow(v); m_view = static_cast(v); Q_ASSERT(m_view); // view needs to be shown as completion won't work if the cursor is off screen m_view->show(); } void CompletionTest::cleanup() { delete m_view; delete m_doc; } void CompletionTest::testFilterEmptyRange() { KateCompletionModel *model = m_view->completionWidget()->model(); new CodeCompletionTestModel(m_view, QStringLiteral("a")); m_view->setCursorPosition(Cursor(0, 0)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 40); m_view->insertText(QStringLiteral("aa")); QTest::qWait(1000); // process events QCOMPARE(countItems(model), 14); } void CompletionTest::testFilterWithRange() { KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel = new CodeCompletionTestModel(m_view, QStringLiteral("a")); m_view->setCursorPosition(Cursor(0, 2)); invokeCompletionBox(m_view); Range complRange = *m_view->completionWidget()->completionRange(testModel); QCOMPARE(complRange, Range(Cursor(0, 0), Cursor(0, 2))); QCOMPARE(countItems(model), 14); m_view->insertText(QStringLiteral("a")); QTest::qWait(1000); // process events QCOMPARE(countItems(model), 1); } void CompletionTest::testAbortCursorMovedOutOfRange() { KateCompletionModel *model = m_view->completionWidget()->model(); new CodeCompletionTestModel(m_view, QStringLiteral("a")); m_view->setCursorPosition(Cursor(0, 2)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 14); QVERIFY(m_view->completionWidget()->isCompletionActive()); m_view->setCursorPosition(Cursor(0, 4)); QTest::qWait(1000); // process events QVERIFY(!m_view->completionWidget()->isCompletionActive()); } void CompletionTest::testAbortInvalidText() { KateCompletionModel *model = m_view->completionWidget()->model(); new CodeCompletionTestModel(m_view, QStringLiteral("a")); m_view->setCursorPosition(Cursor(0, 2)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 14); QVERIFY(m_view->completionWidget()->isCompletionActive()); m_view->insertText(QStringLiteral(".")); verifyCompletionAborted(m_view); } void CompletionTest::testCustomRange1() { m_doc->setText(QStringLiteral("$aa bb cc\ndd")); KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel = new CustomRangeModel(m_view, QStringLiteral("$a")); m_view->setCursorPosition(Cursor(0, 3)); invokeCompletionBox(m_view); Range complRange = *m_view->completionWidget()->completionRange(testModel); qDebug() << complRange; QCOMPARE(complRange, Range(Cursor(0, 0), Cursor(0, 3))); QCOMPARE(countItems(model), 14); m_view->insertText(QStringLiteral("a")); QTest::qWait(1000); // process events QCOMPARE(countItems(model), 1); } void CompletionTest::testCustomRange2() { m_doc->setText(QStringLiteral("$ bb cc\ndd")); KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel = new CustomRangeModel(m_view, QStringLiteral("$a")); m_view->setCursorPosition(Cursor(0, 1)); invokeCompletionBox(m_view); Range complRange = *m_view->completionWidget()->completionRange(testModel); QCOMPARE(complRange, Range(Cursor(0, 0), Cursor(0, 1))); QCOMPARE(countItems(model), 40); m_view->insertText(QStringLiteral("aa")); QTest::qWait(1000); // process events QCOMPARE(countItems(model), 14); } void CompletionTest::testCustomRangeMultipleModels() { m_doc->setText(QStringLiteral("$a bb cc\ndd")); KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel1 = new CustomRangeModel(m_view, QStringLiteral("$a")); CodeCompletionTestModel *testModel2 = new CodeCompletionTestModel(m_view, QStringLiteral("a")); m_view->setCursorPosition(Cursor(0, 1)); invokeCompletionBox(m_view); QCOMPARE(Range(*m_view->completionWidget()->completionRange(testModel1)), Range(Cursor(0, 0), Cursor(0, 2))); QCOMPARE(Range(*m_view->completionWidget()->completionRange(testModel2)), Range(Cursor(0, 1), Cursor(0, 2))); QCOMPARE(model->currentCompletion(testModel1), QStringLiteral("$")); QCOMPARE(model->currentCompletion(testModel2), QString()); QCOMPARE(countItems(model), 80); m_view->insertText(QStringLiteral("aa")); QTest::qWait(1000); // process events QCOMPARE(model->currentCompletion(testModel1), QStringLiteral("$aa")); QCOMPARE(model->currentCompletion(testModel2), QStringLiteral("aa")); QCOMPARE(countItems(model), 14 * 2); } void CompletionTest::testAbortController() { KateCompletionModel *model = m_view->completionWidget()->model(); new CustomRangeModel(m_view, QStringLiteral("$a")); m_view->setCursorPosition(Cursor(0, 0)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 40); QVERIFY(m_view->completionWidget()->isCompletionActive()); m_view->insertText(QStringLiteral("$a")); QTest::qWait(1000); // process events QVERIFY(m_view->completionWidget()->isCompletionActive()); m_view->insertText(QStringLiteral(".")); verifyCompletionAborted(m_view); } void CompletionTest::testAbortControllerMultipleModels() { KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel1 = new CodeCompletionTestModel(m_view, QStringLiteral("aa")); CodeCompletionTestModel *testModel2 = new CustomAbortModel(m_view, QStringLiteral("a-")); m_view->setCursorPosition(Cursor(0, 0)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 80); QVERIFY(m_view->completionWidget()->isCompletionActive()); m_view->insertText(QStringLiteral("a")); QTest::qWait(1000); // process events QVERIFY(m_view->completionWidget()->isCompletionActive()); QCOMPARE(countItems(model), 80); m_view->insertText(QStringLiteral("-")); QTest::qWait(1000); // process events QVERIFY(m_view->completionWidget()->isCompletionActive()); QVERIFY(!m_view->completionWidget()->completionRanges().contains(testModel1)); QVERIFY(m_view->completionWidget()->completionRanges().contains(testModel2)); QCOMPARE(countItems(model), 40); m_view->insertText(QLatin1String(" ")); QTest::qWait(1000); // process events QVERIFY(!m_view->completionWidget()->isCompletionActive()); } void CompletionTest::testEmptyFilterString() { KateCompletionModel *model = m_view->completionWidget()->model(); new EmptyFilterStringModel(m_view, QStringLiteral("aa")); m_view->setCursorPosition(Cursor(0, 0)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 40); m_view->insertText(QStringLiteral("a")); QTest::qWait(1000); // process events QCOMPARE(countItems(model), 40); m_view->insertText(QStringLiteral("bam")); QTest::qWait(1000); // process events QCOMPARE(countItems(model), 40); } void CompletionTest::testUpdateCompletionRange() { m_doc->setText(QStringLiteral("ab bb cc\ndd")); KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel = new UpdateCompletionRangeModel(m_view, QStringLiteral("ab ab")); m_view->setCursorPosition(Cursor(0, 3)); invokeCompletionBox(m_view); QCOMPARE(countItems(model), 40); QCOMPARE(Range(*m_view->completionWidget()->completionRange(testModel)), Range(Cursor(0, 3), Cursor(0, 3))); m_view->insertText(QStringLiteral("ab")); QTest::qWait(1000); // process events QCOMPARE(Range(*m_view->completionWidget()->completionRange(testModel)), Range(Cursor(0, 0), Cursor(0, 5))); QCOMPARE(countItems(model), 40); } void CompletionTest::testCustomStartCompl() { KateCompletionModel *model = m_view->completionWidget()->model(); m_view->completionWidget()->setAutomaticInvocationDelay(1); new StartCompletionModel(m_view, QStringLiteral("aa")); m_view->setCursorPosition(Cursor(0, 0)); m_view->insertText("%"); QTest::qWait(1000); QVERIFY(m_view->completionWidget()->isCompletionActive()); QCOMPARE(countItems(model), 40); } void CompletionTest::testKateCompletionModel() { KateCompletionModel *model = m_view->completionWidget()->model(); CodeCompletionTestModel *testModel1 = new CodeCompletionTestModel(m_view, QStringLiteral("aa")); CodeCompletionTestModel *testModel2 = new CodeCompletionTestModel(m_view, QStringLiteral("bb")); model->setCompletionModel(testModel1); QCOMPARE(countItems(model), 40); model->addCompletionModel(testModel2); QCOMPARE(countItems(model), 80); model->removeCompletionModel(testModel2); QCOMPARE(countItems(model), 40); } void CompletionTest::testAbortImmideatelyAfterStart() { new ImmideatelyAbortCompletionModel(m_view); m_view->setCursorPosition(Cursor(0, 3)); QVERIFY(!m_view->completionWidget()->isCompletionActive()); emit m_view->userInvokedCompletion(); QVERIFY(!m_view->completionWidget()->isCompletionActive()); } void CompletionTest::testJumpToListBottomAfterCursorUpWhileAtTop() { new CodeCompletionTestModel(m_view, QStringLiteral("aa")); invokeCompletionBox(m_view); m_view->completionWidget()->cursorUp(); m_view->completionWidget()->bottom(); // TODO - better way of finding the index? QCOMPARE(m_view->completionWidget()->treeView()->selectionModel()->currentIndex().row(), 39); } void CompletionTest::testAbbreviationEngine() { QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("fb"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("foob"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("fbar"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("fba"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("foba"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarBazBang"), QStringLiteral("fbbb"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("foo_bar_cat"), QStringLiteral("fbc"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("foo_bar_cat"), QStringLiteral("fb"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarArr"), QStringLiteral("fba"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarArr"), QStringLiteral("fbara"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarArr"), QStringLiteral("fobaar"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarArr"), QStringLiteral("fb"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("QualifiedIdentifier"), QStringLiteral("qid"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("QualifiedIdentifier"), QStringLiteral("qualid"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("QualifiedIdentifier"), QStringLiteral("qualidentifier"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("QualifiedIdentifier"), QStringLiteral("qi"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kcmodel"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kc"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kcomplmodel"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kacomplmodel"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kacom"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("QualifiedIdentifier"), QStringLiteral("identifier"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarArr"), QStringLiteral("fobaara"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBarArr"), QStringLiteral("fbac"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kamodel"), Qt::CaseInsensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("AbcdefBcdefCdefDefEfFzZ"), QStringLiteral("AbcdefBcdefCdefDefEfFzZ"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("AbcdefBcdefCdefDefEfFzZ"), QStringLiteral("ABCDEFX"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("AaaaaaBbbbbCcccDddEeFzZ"), QStringLiteral("XZYBFA"), Qt::CaseInsensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("fb"), Qt::CaseSensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("FooBar"), QStringLiteral("FB"), Qt::CaseSensitive)); QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("kcmodel"), Qt::CaseSensitive)); QVERIFY(KateCompletionModel::matchesAbbreviation(QStringLiteral("KateCompletionModel"), QStringLiteral("KCModel"), Qt::CaseSensitive)); } void CompletionTest::benchAbbreviationEngineGoodCase() { - QBENCHMARK - { + QBENCHMARK { for (int i = 0; i < 10000; i++) { QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("AaaaaaBbbbbCcccDddEeFzZ"), QStringLiteral("XZYBFA"), Qt::CaseInsensitive)); } } } void CompletionTest::benchAbbreviationEngineNormalCase() { - QBENCHMARK - { + QBENCHMARK { for (int i = 0; i < 10000; i++) { QVERIFY(!KateCompletionModel::matchesAbbreviation(QStringLiteral("AaaaaaBbbbbCcccDddEeFzZ"), QStringLiteral("ABCDEFX"), Qt::CaseInsensitive)); } } } void CompletionTest::benchAbbreviationEngineWorstCase() { - QBENCHMARK - { + QBENCHMARK { for (int i = 0; i < 10000; i++) { // This case is quite horrible, because it requires a branch at every letter. // The current code will at some point drop out and just return false. KateCompletionModel::matchesAbbreviation(QStringLiteral("XxBbbbbbBbbbbbBbbbbBbbbBbbbbbbBbbbbbBbbbbbBbbbFox"), QStringLiteral("XbbbbbbbbbbbbbbbbbbbbFx"), Qt::CaseInsensitive); } } } void CompletionTest::testAbbrevAndContainsMatching() { KateCompletionModel *model = m_view->completionWidget()->model(); new AbbreviationCodeCompletionTestModel(m_view, QString()); m_view->document()->setText(QStringLiteral("SCA")); invokeCompletionBox(m_view); QCOMPARE(model->filteredItemCount(), (uint)6); m_view->document()->setText(QStringLiteral("SC")); invokeCompletionBox(m_view); QCOMPARE(model->filteredItemCount(), (uint)6); m_view->document()->setText(QStringLiteral("sca")); invokeCompletionBox(m_view); QCOMPARE(model->filteredItemCount(), (uint)6); m_view->document()->setText(QStringLiteral("contains")); invokeCompletionBox(m_view); QCOMPARE(model->filteredItemCount(), (uint)2); m_view->document()->setText(QStringLiteral("CONTAINS")); invokeCompletionBox(m_view); QCOMPARE(model->filteredItemCount(), (uint)2); m_view->document()->setText(QStringLiteral("containssome")); invokeCompletionBox(m_view); QCOMPARE(model->filteredItemCount(), (uint)1); m_view->document()->setText(QStringLiteral("matched")); m_view->userInvokedCompletion(); QApplication::processEvents(); QCOMPARE(model->filteredItemCount(), (uint)0); } void CompletionTest::benchCompletionModel() { const QString text("abcdefg abcdef"); m_doc->setText(text); CodeCompletionTestModel *testModel1 = new CodeCompletionTestModel(m_view, QStringLiteral("abcdefg")); testModel1->setRowCount(500); CodeCompletionTestModel *testModel2 = new CodeCompletionTestModel(m_view, QStringLiteral("abcdef")); testModel2->setRowCount(500); CodeCompletionTestModel *testModel3 = new CodeCompletionTestModel(m_view, QStringLiteral("abcde")); testModel3->setRowCount(500); CodeCompletionTestModel *testModel4 = new CodeCompletionTestModel(m_view, QStringLiteral("abcd")); testModel4->setRowCount(5000); - QBENCHMARK_ONCE - { + QBENCHMARK_ONCE { for (int i = 0; i < text.size(); ++i) { m_view->setCursorPosition(Cursor(0, i)); invokeCompletionBox(m_view); } } } diff --git a/autotests/src/katedocument_test.cpp b/autotests/src/katedocument_test.cpp index aa2babe1..ca3f02e3 100644 --- a/autotests/src/katedocument_test.cpp +++ b/autotests/src/katedocument_test.cpp @@ -1,742 +1,740 @@ /* This file is part of the KDE libraries Copyright (C) 2010-2018 Dominik Haumann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katedocument_test.h" #include "moc_katedocument_test.cpp" #include #include #include #include #include #include #include #include #include #include /// TODO: is there a FindValgrind cmake command we could use to /// define this automatically? // comment this out and run the test case with: // valgrind --tool=callgrind --instr-atstart=no ./katedocument_test testSetTextPerformance // or similar // // #define USE_VALGRIND #ifdef USE_VALGRIND #include #endif using namespace KTextEditor; QTEST_MAIN(KateDocumentTest) class MovingRangeInvalidator : public QObject { Q_OBJECT public: explicit MovingRangeInvalidator(QObject *parent = nullptr) : QObject(parent) { } void addRange(MovingRange *range) { m_ranges << range; } QList ranges() const { return m_ranges; } public Q_SLOTS: void aboutToInvalidateMovingInterfaceContent() { qDeleteAll(m_ranges); m_ranges.clear(); } private: QList m_ranges; }; KateDocumentTest::KateDocumentTest() : QObject() { } KateDocumentTest::~KateDocumentTest() { } void KateDocumentTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); } // tests: // KTextEditor::DocumentPrivate::insertText with word wrap enabled. It is checked whether the // text is correctly wrapped and whether the moving cursors maintain the correct // position. // see also: http://bugs.kde.org/show_bug.cgi?id=168534 void KateDocumentTest::testWordWrap() { KTextEditor::DocumentPrivate doc(false, false); doc.setWordWrap(true); doc.setWordWrapAt(80); const QString content = QLatin1String(".........1.........2.........3.........4.........5.........6 ........7 ........8"); // space after 7 is now kept // else we kill indentation... const QString firstWrap = QLatin1String(".........1.........2.........3.........4.........5.........6 ........7 \n....x....8"); // space after 6 is now kept // else we kill indentation... const QString secondWrap = QLatin1String(".........1.........2.........3.........4.........5.........6 \n....ooooooooooo....7 ....x....8"); doc.setText(content); MovingCursor *c = doc.newMovingCursor(Cursor(0, 75), MovingCursor::MoveOnInsert); QCOMPARE(doc.text(), content); QCOMPARE(c->toCursor(), Cursor(0, 75)); // type a character at (0, 75) doc.insertText(c->toCursor(), QLatin1String("x")); QCOMPARE(doc.text(), firstWrap); QCOMPARE(c->toCursor(), Cursor(1, 5)); // set cursor to (0, 65) and type "ooooooooooo" c->setPosition(Cursor(0, 65)); doc.insertText(c->toCursor(), QLatin1String("ooooooooooo")); QCOMPARE(doc.text(), secondWrap); 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); doc.setWordWrap(false); doc.setText( QLatin1String("asdf\n" "foo\n" "foo\n" "bar\n")); doc.replaceText(Range(1, 0, 3, 0), {"new", "text", ""}, false); QCOMPARE(doc.text(), QLatin1String("asdf\n" "new\n" "text\n" "bar\n")); } void KateDocumentTest::testMovingInterfaceSignals() { KTextEditor::DocumentPrivate *doc = new KTextEditor::DocumentPrivate; QSignalSpy aboutToDeleteSpy(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *))); QSignalSpy aboutToInvalidateSpy(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *))); QCOMPARE(doc->revision(), qint64(0)); QCOMPARE(aboutToInvalidateSpy.count(), 0); QCOMPARE(aboutToDeleteSpy.count(), 0); QTemporaryFile f; f.open(); doc->openUrl(QUrl::fromLocalFile(f.fileName())); QCOMPARE(doc->revision(), qint64(0)); // TODO: gets emitted once in closeFile and once in openFile - is that OK? QCOMPARE(aboutToInvalidateSpy.count(), 2); QCOMPARE(aboutToDeleteSpy.count(), 0); doc->documentReload(); QCOMPARE(doc->revision(), qint64(0)); QCOMPARE(aboutToInvalidateSpy.count(), 4); // TODO: gets emitted once in closeFile and once in openFile - is that OK? QCOMPARE(aboutToDeleteSpy.count(), 0); delete doc; QCOMPARE(aboutToInvalidateSpy.count(), 4); QCOMPARE(aboutToDeleteSpy.count(), 1); } void KateDocumentTest::testSetTextPerformance() { const int lines = 150; const int columns = 80; const int rangeLength = 4; const int rangeGap = 1; Q_ASSERT(columns % (rangeLength + rangeGap) == 0); KTextEditor::DocumentPrivate doc; MovingRangeInvalidator invalidator; connect(&doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), &invalidator, SLOT(aboutToInvalidateMovingInterfaceContent())); QString text; QVector ranges; ranges.reserve(lines * columns / (rangeLength + rangeGap)); const QString line = QString().fill('a', columns); for (int l = 0; l < lines; ++l) { text.append(line); text.append('\n'); for (int c = 0; c < columns; c += rangeLength + rangeGap) { ranges << Range(l, c, l, c + rangeLength); } } // replace - QBENCHMARK - { + QBENCHMARK { // init doc.setText(text); for (const Range &range : qAsConst(ranges)) { invalidator.addRange(doc.newMovingRange(range)); } QCOMPARE(invalidator.ranges().size(), ranges.size()); #ifdef USE_VALGRIND CALLGRIND_START_INSTRUMENTATION #endif doc.setText(text); #ifdef USE_VALGRIND CALLGRIND_STOP_INSTRUMENTATION #endif QCOMPARE(doc.text(), text); QVERIFY(invalidator.ranges().isEmpty()); } } void KateDocumentTest::testRemoveTextPerformance() { const int lines = 5000; const int columns = 80; KTextEditor::DocumentPrivate doc; QString text; const QString line = QString().fill('a', columns); for (int l = 0; l < lines; ++l) { text.append(line); text.append('\n'); } doc.setText(text); // replace - QBENCHMARK_ONCE - { + QBENCHMARK_ONCE { #ifdef USE_VALGRIND CALLGRIND_START_INSTRUMENTATION #endif doc.editStart(); doc.removeText(doc.documentRange()); doc.editEnd(); #ifdef USE_VALGRIND CALLGRIND_STOP_INSTRUMENTATION #endif } } void KateDocumentTest::testForgivingApiUsage() { KTextEditor::DocumentPrivate doc; QVERIFY(doc.isEmpty()); QVERIFY(doc.replaceText(Range(0, 0, 100, 100), "asdf")); QCOMPARE(doc.text(), QString("asdf")); QCOMPARE(doc.lines(), 1); QVERIFY(doc.replaceText(Range(2, 5, 2, 100), "asdf")); QCOMPARE(doc.lines(), 3); QCOMPARE(doc.text(), QLatin1String("asdf\n\n asdf")); QVERIFY(doc.removeText(Range(0, 0, 1000, 1000))); QVERIFY(doc.removeText(Range(0, 0, 0, 100))); QVERIFY(doc.isEmpty()); doc.insertText(Cursor(100, 0), "foobar"); QCOMPARE(doc.line(100), QString("foobar")); doc.setText("nY\nnYY\n"); QVERIFY(doc.removeText(Range(0, 0, 0, 1000))); } void KateDocumentTest::testAutoBrackets() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); auto reset = [&]() { doc.setText(""); view->setCursorPosition(Cursor(0, 0)); }; auto typeText = [&](const QString &text) { for (int i = 0; i < text.size(); ++i) { doc.typeChars(view, text.at(i)); } }; doc.setHighlightingMode("Normal"); // Just to be sure view->config()->setValue(KateViewConfig::AutoBrackets, true); QString testInput; testInput = ("("); typeText(testInput); QCOMPARE(doc.text(), "()"); reset(); testInput = ("\""); typeText(testInput); QCOMPARE(doc.text(), "\"\""); reset(); testInput = ("'"); typeText(testInput); QCOMPARE(doc.text(), "'"); // In Normal mode there is only one quote to expect // // Switch over to some other mode // doc.setHighlightingMode("C++"); reset(); typeText(testInput); QCOMPARE(doc.text(), "''"); // Now it must be two reset(); testInput = "('')"; typeText(testInput); // Known bad behaviour in case of nested brackets QCOMPARE(doc.text(), testInput); reset(); testInput = ("foo \"bar\" haz"); typeText(testInput); QCOMPARE(doc.text(), testInput); // Simulate afterwards to add quotes, bug 405089 doc.setText("foo \"bar"); typeText("\" haz"); QCOMPARE(doc.text(), testInput); // Let's check to add brackets to a selection... view->setBlockSelection(false); doc.setText("012xxx678"); view->setSelection(Range(0, 3, 0, 6)); typeText("["); QCOMPARE(doc.text(), "012[xxx]678"); QCOMPARE(view->selectionRange(), Range(0, 4, 0, 7)); // ...over multiple lines.. doc.setText("012xxx678\n012xxx678"); view->setSelection(Range(0, 3, 1, 6)); typeText("["); QCOMPARE(doc.text(), "012[xxx678\n012xxx]678"); QCOMPARE(view->selectionRange(), Range(0, 4, 1, 6)); // ..once again in in block mode with increased complexity.. view->setBlockSelection(true); doc.setText("012xxx678\n012xxx678"); view->setSelection(Range(0, 3, 1, 6)); typeText("[("); QCOMPARE(doc.text(), "012[(xxx)]678\n012[(xxx)]678"); QCOMPARE(view->selectionRange(), Range(0, 5, 1, 8)); // ..and the same with right->left selection view->setBlockSelection(true); doc.setText("012xxx678\n012xxx678"); view->setSelection(Range(0, 6, 1, 3)); typeText("[("); QCOMPARE(doc.text(), "012[(xxx)]678\n012[(xxx)]678"); QCOMPARE(view->selectionRange(), Range(0, 8, 1, 5)); } void KateDocumentTest::testReplaceTabs() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); auto reset = [&]() { doc.setText(" Hi!"); view->setCursorPosition(Cursor(0, 0)); }; doc.setHighlightingMode("C++"); doc.config()->setTabWidth(4); doc.config()->setIndentationMode("cppstyle"); // no replace tabs, no indent pasted text doc.setConfigValue("replace-tabs", false); doc.setConfigValue("indent-pasted-text", false); reset(); doc.insertText(Cursor(0, 0), "\t"); QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); reset(); doc.typeChars(view, "\t"); QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); reset(); doc.paste(view, "some\ncode\n 3\nhi\n"); QCOMPARE(doc.text(), QStringLiteral("some\ncode\n 3\nhi\n Hi!")); // now, enable replace tabs doc.setConfigValue("replace-tabs", true); reset(); doc.insertText(Cursor(0, 0), "\t"); // calling insertText does not replace tabs QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); reset(); doc.typeChars(view, "\t"); // typing text replaces tabs QCOMPARE(doc.text(), QStringLiteral(" Hi!")); reset(); doc.paste(view, "\t"); // pasting text does not with indent-pasted off QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); doc.setConfigValue("indent-pasted-text", true); doc.setText("int main() {\n \n}"); view->setCursorPosition(Cursor(1, 4)); doc.paste(view, "\tHi"); // ... and it still does not with indent-pasted on. // This behaviour is up to discussion. QCOMPARE(doc.text(), QStringLiteral("int main() {\n Hi\n}")); reset(); doc.paste(view, "some\ncode\n 3\nhi"); QCOMPARE(doc.text(), QStringLiteral("some\ncode\n3\nhi Hi!")); } /** * Provides slots to check data sent in specific signals. Slot names are derived from corresponding test names. */ class SignalHandler : public QObject { Q_OBJECT public Q_SLOTS: void slotMultipleLinesRemoved(KTextEditor::Document *, const KTextEditor::Range &, const QString &oldText) { QCOMPARE(oldText, QString("line2\nline3\n")); } void slotNewlineInserted(KTextEditor::Document *, const KTextEditor::Range &range) { QCOMPARE(range, Range(Cursor(1, 4), Cursor(2, 0))); } }; void KateDocumentTest::testRemoveMultipleLines() { KTextEditor::DocumentPrivate doc; doc.setText( "line1\n" "line2\n" "line3\n" "line4\n"); SignalHandler handler; connect(&doc, &KTextEditor::DocumentPrivate::textRemoved, &handler, &SignalHandler::slotMultipleLinesRemoved); doc.removeText(Range(1, 0, 3, 0)); } void KateDocumentTest::testInsertNewline() { KTextEditor::DocumentPrivate doc; doc.setText( "this is line\n" "this is line2\n"); SignalHandler handler; connect(&doc, SIGNAL(textInserted(KTextEditor::Document *, KTextEditor::Range)), &handler, SLOT(slotNewlineInserted(KTextEditor::Document *, KTextEditor::Range))); doc.editWrapLine(1, 4); } void KateDocumentTest::testInsertAfterEOF() { KTextEditor::DocumentPrivate doc; doc.setText( "line0\n" "line1"); const QString input = QLatin1String( "line3\n" "line4"); const QString expected = QLatin1String( "line0\n" "line1\n" "\n" "line3\n" "line4"); doc.insertText(KTextEditor::Cursor(3, 0), input); QCOMPARE(doc.text(), expected); } // we have two different ways of creating the checksum: // in KateFileLoader and KTextEditor::DocumentPrivate::createDigest. Make // sure, these two implementations result in the same checksum. void KateDocumentTest::testDigest() { // Git hash of test file (git hash-object data/md5checksum.txt): const QByteArray gitHash = "696e6d35a5d9cc28d16e56bdcb2d2a88126b814e"; // QCryptographicHash is used, therefore we need fromHex here const QByteArray fileDigest = QByteArray::fromHex(gitHash); // make sure, Kate::TextBuffer and KTextEditor::DocumentPrivate::createDigest() equal KTextEditor::DocumentPrivate doc; doc.openUrl(QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "md5checksum.txt"))); const QByteArray bufferDigest(doc.checksum()); QVERIFY(doc.createDigest()); const QByteArray docDigest(doc.checksum()); QCOMPARE(bufferDigest, fileDigest); QCOMPARE(docDigest, fileDigest); } void KateDocumentTest::testModelines() { // honor document variable indent-width { KTextEditor::DocumentPrivate doc; QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate: indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 3); } // honor document variable indent-width with * wildcard { KTextEditor::DocumentPrivate doc; QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*): indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 3); } // ignore document variable indent-width, since the wildcard does not match { KTextEditor::DocumentPrivate doc; QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*.txt): indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 4); } // document variable indent-width, since the wildcard does not match { KTextEditor::DocumentPrivate doc; doc.openUrl(QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "modelines.txt"))); QVERIFY(!doc.isEmpty()); // ignore wrong wildcard QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*.bar): indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 4); // read correct wildcard QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*.txt): indent-width 5;")); QCOMPARE(doc.config()->indentationWidth(), 5); // honor correct wildcard QCOMPARE(doc.config()->indentationWidth(), 5); doc.readVariableLine(QStringLiteral("kate-wildcard(*.foo;*.txt;*.bar): indent-width 6;")); QCOMPARE(doc.config()->indentationWidth(), 6); // ignore incorrect mimetype QCOMPARE(doc.config()->indentationWidth(), 6); doc.readVariableLine(QStringLiteral("kate-mimetype(text/unknown): indent-width 7;")); QCOMPARE(doc.config()->indentationWidth(), 6); // honor correct mimetype QCOMPARE(doc.config()->indentationWidth(), 6); doc.readVariableLine(QStringLiteral("kate-mimetype(text/plain): indent-width 8;")); QCOMPARE(doc.config()->indentationWidth(), 8); // honor correct mimetype QCOMPARE(doc.config()->indentationWidth(), 8); doc.readVariableLine(QStringLiteral("kate-mimetype(text/foo;text/plain;text/bar): indent-width 9;")); QCOMPARE(doc.config()->indentationWidth(), 9); } } void KateDocumentTest::testDefStyleNum() { KTextEditor::DocumentPrivate doc; doc.setText("foo\nbar\nasdf"); QCOMPARE(doc.defStyleNum(0, 0), 0); } void KateDocumentTest::testTypeCharsWithSurrogateAndNewLine() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); const uint surrogateUcs4String[] = {0x1f346, '\n', 0x1f346, 0}; const auto surrogateString = QString::fromUcs4(surrogateUcs4String); doc.typeChars(view, surrogateString); QCOMPARE(doc.text(), surrogateString); } void KateDocumentTest::testRemoveComposedCharacters() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); view->config()->setValue(KateViewConfig::BackspaceRemoveComposedCharacters, true); doc.setText(QString::fromUtf8("व्यक्तियों")); doc.del(view, Cursor(0, 0)); QCOMPARE(doc.text(), QString::fromUtf8(("क्तियों"))); doc.backspace(view, Cursor(0, 7)); QCOMPARE(doc.text(), QString::fromUtf8(("क्ति"))); } void KateDocumentTest::testAutoReload() { QTemporaryFile file("AutoReloadTestFile"); file.open(); QTextStream stream(&file); stream << "Hello"; stream.flush(); KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); QVERIFY(doc.openUrl(QUrl::fromLocalFile(file.fileName()))); QCOMPARE(doc.text(), "Hello"); // The cursor should be and stay in the last line... QCOMPARE(view->cursorPosition().line(), doc.documentEnd().line()); doc.autoReloadToggled(true); // Some magic value. You can wait as long as you like after write to file, // without to wait before it doesn't work here. It mostly fails with 500, // it tend to work with 600, but is not guarantied to work even with 800 QTest::qWait(1000); stream << "\nTest"; stream.flush(); // Hardcoded delay m_modOnHdTimer in DocumentPrivate // + min val with which it looks working reliable here QTest::qWait(200 + 800); QCOMPARE(doc.text(), "Hello\nTest"); // ...stay in the last line after reload! QCOMPARE(view->cursorPosition().line(), doc.documentEnd().line()); // Now we have more then one line, set the cursor to some line != last line... Cursor c(0, 3); view->setCursorPosition(c); stream << "\nWorld!"; stream.flush(); file.close(); QTest::qWait(200 + 800); QCOMPARE(doc.text(), "Hello\nTest\nWorld!"); // ...and ensure we have not move around QCOMPARE(view->cursorPosition(), c); } void KateDocumentTest::testSearch() { /** * this is the start of some new implementation of searchText that can handle multi-line regex stuff * just naturally and without always concatenating the full document. */ KTextEditor::DocumentPrivate doc; doc.setText("foo\nbar\nzonk"); const QRegularExpression pattern(QStringLiteral("ar\nzonk$"), QRegularExpression::MultilineOption); int startLine = 0; int endLine = 2; QString textToMatch; int partialMatchLine = -1; for (int currentLine = startLine; currentLine <= endLine; ++currentLine) { // if we had a partial match before, we need to keep the old text and append our new line int matchStartLine = currentLine; if (partialMatchLine >= 0) { textToMatch += doc.line(currentLine); textToMatch += QLatin1Char('\n'); matchStartLine = partialMatchLine; } else { textToMatch = doc.line(currentLine); textToMatch += QLatin1Char('\n'); } auto result = pattern.match(textToMatch, 0, QRegularExpression::PartialPreferFirstMatch); printf("lala %d %d %d %d\n", result.hasMatch(), result.hasPartialMatch(), result.capturedStart(), result.capturedLength()); if (result.hasMatch()) { printf("found stuff starting in line %d\n", matchStartLine); break; } // else: remember if we need to keep text! if (result.hasPartialMatch()) { // if we had already a partial match before, keep the line! if (partialMatchLine >= 0) { } else { partialMatchLine = currentLine; } } else { // we can forget the old text partialMatchLine = -1; } } } #include "katedocument_test.moc" diff --git a/autotests/src/plaintextsearch_test.cpp b/autotests/src/plaintextsearch_test.cpp index 44f396a9..962feab1 100644 --- a/autotests/src/plaintextsearch_test.cpp +++ b/autotests/src/plaintextsearch_test.cpp @@ -1,169 +1,169 @@ /* This file is part of the KDE libraries Copyright (C) 2010 Bernhard Beschow This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plaintextsearch_test.h" #include "moc_plaintextsearch_test.cpp" #include #include #include #include QTEST_MAIN(PlainTextSearchTest) QtMessageHandler PlainTextSearchTest::s_msgHandler = nullptr; void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { switch (type) { - case QtDebugMsg: - /* do nothing */ - break; - default: - PlainTextSearchTest::s_msgHandler(type, context, msg); + case QtDebugMsg: + /* do nothing */ + break; + default: + PlainTextSearchTest::s_msgHandler(type, context, msg); } } void PlainTextSearchTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); s_msgHandler = qInstallMessageHandler(myMessageOutput); } void PlainTextSearchTest::cleanupTestCase() { qInstallMessageHandler(nullptr); } PlainTextSearchTest::PlainTextSearchTest() : QObject() { } PlainTextSearchTest::~PlainTextSearchTest() { } void PlainTextSearchTest::init() { m_doc = new KTextEditor::DocumentPrivate(false, false, nullptr, this); m_search = new KatePlainTextSearch(m_doc, Qt::CaseSensitive, false); } void PlainTextSearchTest::cleanup() { delete m_search; delete m_doc; } void PlainTextSearchTest::testSearchBackward_data() { QTest::addColumn("searchRange"); QTest::addColumn("expectedResult"); QTest::newRow("") << KTextEditor::Range(0, 0, 1, 10) << KTextEditor::Range(1, 6, 1, 10); QTest::newRow("") << KTextEditor::Range(0, 0, 1, 5) << KTextEditor::Range(1, 0, 1, 4); QTest::newRow("") << KTextEditor::Range(0, 0, 1, 0) << KTextEditor::Range(0, 10, 0, 14); } void PlainTextSearchTest::testSearchBackward() { QFETCH(KTextEditor::Range, searchRange); QFETCH(KTextEditor::Range, expectedResult); m_doc->setText( QLatin1String("aaaa aaaa aaaa\n" "aaaa aaaa")); QCOMPARE(m_search->search(QLatin1String("aaaa"), searchRange, true), expectedResult); } void PlainTextSearchTest::testSingleLineDocument_data() { QTest::addColumn("searchRange"); QTest::addColumn("forwardResult"); QTest::addColumn("backwardResult"); QTest::newRow("[a a a a a a a a a a a a]") << KTextEditor::Range(0, 0, 0, 23) << KTextEditor::Range(0, 0, 0, 5) << KTextEditor::Range(0, 18, 0, 23); QTest::newRow("[a a a a a a a a a a a ]a") << KTextEditor::Range(0, 0, 0, 22) << KTextEditor::Range(0, 0, 0, 5) << KTextEditor::Range(0, 16, 0, 21); QTest::newRow("a[ a a a a a a a a a a a]") << KTextEditor::Range(0, 1, 0, 23) << KTextEditor::Range(0, 2, 0, 7) << KTextEditor::Range(0, 18, 0, 23); QTest::newRow("a[ a a a a a a a a a a ]a") << KTextEditor::Range(0, 1, 0, 22) << KTextEditor::Range(0, 2, 0, 7) << KTextEditor::Range(0, 16, 0, 21); QTest::newRow("[a a a a] a a a a a a a a") << KTextEditor::Range(0, 0, 0, 7) << KTextEditor::Range(0, 0, 0, 5) << KTextEditor::Range(0, 2, 0, 7); QTest::newRow("[a a a ]a a a a a a a a a") << KTextEditor::Range(0, 0, 0, 6) << KTextEditor::Range(0, 0, 0, 5) << KTextEditor::Range(0, 0, 0, 5); QTest::newRow("[a a a] a a a a a a a a a") << KTextEditor::Range(0, 0, 0, 5) << KTextEditor::Range(0, 0, 0, 5) << KTextEditor::Range(0, 0, 0, 5); QTest::newRow("[a a ]a a a a a a a a a a") << KTextEditor::Range(0, 0, 0, 4) << KTextEditor::Range::invalid() << KTextEditor::Range::invalid(); QTest::newRow("a a a a a a a a [a a a a]") << KTextEditor::Range(0, 16, 0, 23) << KTextEditor::Range(0, 16, 0, 21) << KTextEditor::Range(0, 18, 0, 23); QTest::newRow("a a a a a a a a a[ a a a]") << KTextEditor::Range(0, 17, 0, 23) << KTextEditor::Range(0, 18, 0, 23) << KTextEditor::Range(0, 18, 0, 23); QTest::newRow("a a a a a a a a a [a a a]") << KTextEditor::Range(0, 18, 0, 23) << KTextEditor::Range(0, 18, 0, 23) << KTextEditor::Range(0, 18, 0, 23); QTest::newRow("a a a a a a a a a a[ a a]") << KTextEditor::Range(0, 19, 0, 23) << KTextEditor::Range::invalid() << KTextEditor::Range::invalid(); QTest::newRow("a a a a a[ a a a a] a a a") << KTextEditor::Range(0, 9, 0, 17) << KTextEditor::Range(0, 10, 0, 15) << KTextEditor::Range(0, 12, 0, 17); QTest::newRow("a a a a a[ a a] a a a a a") << KTextEditor::Range(0, 9, 0, 13) << KTextEditor::Range::invalid() << KTextEditor::Range::invalid(); } void PlainTextSearchTest::testSingleLineDocument() { QFETCH(KTextEditor::Range, searchRange); QFETCH(KTextEditor::Range, forwardResult); QFETCH(KTextEditor::Range, backwardResult); m_doc->setText(QLatin1String("a a a a a a a a a a a a")); QCOMPARE(m_search->search(QLatin1String("a a a"), searchRange, false), forwardResult); QCOMPARE(m_search->search(QLatin1String("a a a"), searchRange, true), backwardResult); } void PlainTextSearchTest::testMultilineSearch_data() { QTest::addColumn("pattern"); QTest::addColumn("inputRange"); QTest::addColumn("forwardResult"); QTest::newRow("") << "a a a\na a\na a a" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 0, 2, 5); QTest::newRow("") << "a a a\na a\na a " << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 0, 2, 4); QTest::newRow("") << "a a a\na a\na a" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 0, 2, 3); QTest::newRow("") << "a a a\na a\na" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 0, 2, 1); QTest::newRow("") << "a a a\na a\n" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 0, 2, 0); QTest::newRow("") << "a a a\na a" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 0, 1, 3); QTest::newRow("") << "a a\na a" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 2, 1, 3); QTest::newRow("") << "a a\na a\na a" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 2, 2, 3); QTest::newRow("") << "\na a\na a" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 5, 2, 3); QTest::newRow("") << "\na a\n" << KTextEditor::Range(0, 0, 2, 5) << KTextEditor::Range(0, 5, 2, 0); QTest::newRow("") << "a a a\na a\na a a" << KTextEditor::Range(0, 0, 2, 4) << KTextEditor::Range::invalid(); QTest::newRow("") << "a a a\na a\na a " << KTextEditor::Range(0, 0, 2, 4) << KTextEditor::Range(0, 0, 2, 4); QTest::newRow("") << "a a a\na a\n" << KTextEditor::Range(0, 0, 2, 0) << KTextEditor::Range(0, 0, 2, 0); QTest::newRow("") << "a a a\na a\n" << KTextEditor::Range(0, 0, 1, 3) << KTextEditor::Range::invalid(); QTest::newRow("") << "a a\n" << KTextEditor::Range(0, 0, 1, 3) << KTextEditor::Range(0, 2, 1, 0); QTest::newRow("") << "a \n" << KTextEditor::Range(0, 0, 1, 3) << KTextEditor::Range::invalid(); } void PlainTextSearchTest::testMultilineSearch() { QFETCH(QString, pattern); QFETCH(KTextEditor::Range, inputRange); QFETCH(KTextEditor::Range, forwardResult); m_doc->setText( QLatin1String("a a a\n" "a a\n" "a a a")); QCOMPARE(m_search->search(pattern, inputRange, false), forwardResult); } diff --git a/autotests/src/script_test_base.cpp b/autotests/src/script_test_base.cpp index 4da3b236..dda9b3c5 100644 --- a/autotests/src/script_test_base.cpp +++ b/autotests/src/script_test_base.cpp @@ -1,192 +1,192 @@ /** * This file is part of the KDE project * * Copyright (C) 2001,2003 Peter Kelly (pmk@post.com) * Copyright (C) 2003,2004 Stephan Kulow (coolo@kde.org) * Copyright (C) 2004 Dirk Mueller ( mueller@kde.org ) * Copyright 2006, 2007 Leo Savernik (l.savernik@aon.at) * Copyright (C) 2010 Milian Wolff * Copyright (C) 2013 Gerald Senarclens de Grancy * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ // BEGIN Includes #include "katedocument.h" #include "kateglobal.h" #include "kateview.h" #include #include #include #include #include #include #include "testutils.h" #include "script_test_base.h" const QString testDataPath(QLatin1String(TEST_DATA_DIR)); QtMessageHandler ScriptTestBase::m_msgHandler = nullptr; void noDebugMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { switch (type) { - case QtDebugMsg: - break; - default: - ScriptTestBase::m_msgHandler(type, context, msg); + case QtDebugMsg: + break; + default: + ScriptTestBase::m_msgHandler(type, context, msg); } } void ScriptTestBase::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); m_msgHandler = qInstallMessageHandler(noDebugMessageOutput); m_toplevel = new QMainWindow(); m_document = new KTextEditor::DocumentPrivate(true, false, m_toplevel); m_view = static_cast(m_document->widget()); m_env = new TestScriptEnv(m_document, m_outputWasCustomised); } void ScriptTestBase::cleanupTestCase() { qInstallMessageHandler(m_msgHandler); } void ScriptTestBase::getTestData(const QString &script) { QTest::addColumn("testcase"); // make sure the script files are valid if (!m_script_dir.isEmpty()) { QFile scriptFile(QLatin1String(JS_DATA_DIR) + m_script_dir + QLatin1Char('/') + script + QLatin1String(".js")); if (scriptFile.exists()) { QVERIFY(scriptFile.open(QFile::ReadOnly)); QJSValue result = m_env->engine()->evaluate(QString::fromLatin1(scriptFile.readAll()), scriptFile.fileName()); QVERIFY2(!result.isError(), (result.toString() + QLatin1String(" in file ") + scriptFile.fileName()).toUtf8().constData()); } } const QString testDir(testDataPath + m_section + QLatin1Char('/') + script + QLatin1Char('/')); if (!QFile::exists(testDir)) { QSKIP(qPrintable(QString(testDir + QLatin1String(" does not exist"))), SkipAll); } QDirIterator contents(testDir); while (contents.hasNext()) { QString entry = contents.next(); if (entry.endsWith(QLatin1Char('.'))) { continue; } QFileInfo info(entry); if (!info.isDir()) { continue; } QTest::newRow(info.baseName().toLocal8Bit().constData()) << info.absoluteFilePath(); } } void ScriptTestBase::runTest(const ExpectedFailures &failures) { if (!QFile::exists(testDataPath + m_section)) { QSKIP(qPrintable(QString(testDataPath + m_section + QLatin1String(" does not exist"))), SkipAll); } QFETCH(QString, testcase); m_toplevel->resize(800, 600); // restore size // load page QUrl url; url.setScheme(QLatin1String("file")); url.setPath(testcase + QLatin1String("/origin")); m_document->openUrl(url); // evaluate test-script QFile sourceFile(testcase + QLatin1String("/input.js")); if (!sourceFile.open(QFile::ReadOnly)) { QFAIL(qPrintable(QString::fromLatin1("Failed to open file: %1").arg(sourceFile.fileName()))); } QTextStream stream(&sourceFile); stream.setCodec("UTF8"); QString code = stream.readAll(); sourceFile.close(); // Execute script QJSValue result = m_env->engine()->evaluate(code, testcase + QLatin1String("/input.js"), 1); QVERIFY2(!result.isError(), result.toString().toUtf8().constData()); const QString fileExpected = testcase + QLatin1String("/expected"); const QString fileActual = testcase + QLatin1String("/actual"); url.setPath(fileActual); m_document->saveAs(url); const QByteArray actualChecksum = m_document->checksum(); const QByteArray expectedChecksum = digestForFile(fileExpected); if (actualChecksum != expectedChecksum) { // diff actual and expected QProcess diff; QStringList args(QStringList() << QLatin1String("-u") << fileExpected << fileActual); diff.start(QLatin1String("diff"), args); diff.waitForFinished(); QByteArray out = diff.readAllStandardOutput(); QByteArray err = diff.readAllStandardError(); if (!err.isEmpty()) { qWarning() << err; } if (diff.exitCode() != EXIT_SUCCESS) { QTextStream(stdout) << out << endl; } for (const Failure &failure : failures) { QEXPECT_FAIL(failure.first, failure.second, Abort); } QCOMPARE(diff.exitCode(), EXIT_SUCCESS); } m_document->closeUrl(); } QByteArray ScriptTestBase::digestForFile(const QString &file) { QByteArray digest; QFile f(file); if (f.open(QIODevice::ReadOnly)) { // init the hash with the git header QCryptographicHash crypto(QCryptographicHash::Sha1); const QString header = QString(QLatin1String("blob %1")).arg(f.size()); crypto.addData(header.toLatin1() + '\0'); while (!f.atEnd()) { crypto.addData(f.read(256 * 1024)); } digest = crypto.result(); } return digest; } diff --git a/autotests/src/scriptdocument_test.cpp b/autotests/src/scriptdocument_test.cpp index 6c42956c..52a3baab 100644 --- a/autotests/src/scriptdocument_test.cpp +++ b/autotests/src/scriptdocument_test.cpp @@ -1,128 +1,128 @@ /* This file is part of the KDE libraries Copyright (C) 2010 Bernhard Beschow This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "scriptdocument_test.h" #include "ktexteditor/cursor.h" #include #include #include #include #include #include QTEST_MAIN(ScriptDocumentTest) QtMessageHandler ScriptDocumentTest::s_msgHandler = nullptr; void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { switch (type) { - case QtDebugMsg: - /* do nothing */ - break; - default: - ScriptDocumentTest::s_msgHandler(type, context, msg); + case QtDebugMsg: + /* do nothing */ + break; + default: + ScriptDocumentTest::s_msgHandler(type, context, msg); } } void ScriptDocumentTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); s_msgHandler = qInstallMessageHandler(myMessageOutput); } void ScriptDocumentTest::cleanupTestCase() { qInstallMessageHandler(nullptr); } ScriptDocumentTest::ScriptDocumentTest() : QObject() { } ScriptDocumentTest::~ScriptDocumentTest() { } void ScriptDocumentTest::init() { m_doc = new KTextEditor::DocumentPrivate; m_view = m_doc->createView(nullptr); m_scriptDoc = new KateScriptDocument(nullptr, this); m_scriptDoc->setDocument(m_doc); } void ScriptDocumentTest::cleanup() { delete m_scriptDoc; delete m_view; delete m_doc; } #if 0 void ScriptDocumentTest::testRfind_data() { QTest::addColumn("searchRange"); QTest::addColumn("expectedResult"); QTest::newRow("") << KTextEditor::Range(0, 0, 1, 10) << KTextEditor::Range(1, 6, 1, 10); QTest::newRow("") << KTextEditor::Range(0, 0, 1, 5) << KTextEditor::Range(1, 0, 1, 4); QTest::newRow("") << KTextEditor::Range(0, 0, 1, 0) << KTextEditor::Range(0, 10, 0, 14); } void ScriptDocumentTest::testRfind() { QFETCH(KTextEditor::Range, searchRange); QFETCH(KTextEditor::Range, expectedResult); m_doc->setText("aaaa aaaa aaaa\n" "aaaa aaaa"); QCOMPARE(m_search->search(searchRange, "aaaa", true), expectedResult); } #endif void ScriptDocumentTest::testRfind_data() { QTest::addColumn("searchStart"); QTest::addColumn("result"); QTest::newRow("a a a a a a a a a a a a|") << KTextEditor::Cursor(0, 23) << KTextEditor::Cursor(0, 18); QTest::newRow("a a a a a a a a a a a |a") << KTextEditor::Cursor(0, 22) << KTextEditor::Cursor(0, 16); QTest::newRow("a a a a| a a a a a a a a") << KTextEditor::Cursor(0, 7) << KTextEditor::Cursor(0, 2); QTest::newRow("a a a |a a a a a a a a a") << KTextEditor::Cursor(0, 6) << KTextEditor::Cursor(0, 0); QTest::newRow("a a a| a a a a a a a a a") << KTextEditor::Cursor(0, 5) << KTextEditor::Cursor(0, 0); QTest::newRow("a a |a a a a a a a a a a") << KTextEditor::Cursor(0, 4) << KTextEditor::Cursor::invalid(); } void ScriptDocumentTest::testRfind() { QFETCH(KTextEditor::Cursor, searchStart); QFETCH(KTextEditor::Cursor, result); m_scriptDoc->setText("a a a a a a a a a a a a"); KTextEditor::Cursor cursor = m_scriptDoc->rfind(searchStart, "a a a"); QCOMPARE(cursor, result); } #include "moc_scriptdocument_test.cpp" diff --git a/autotests/src/wordcompletiontest.cpp b/autotests/src/wordcompletiontest.cpp index d36a7979..74ee477f 100644 --- a/autotests/src/wordcompletiontest.cpp +++ b/autotests/src/wordcompletiontest.cpp @@ -1,117 +1,114 @@ /* * * Copyright 2013 Sven Brauch This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "wordcompletiontest.h" #include #include #include #include #include #include QTEST_MAIN(WordCompletionTest) // was 500000, but that takes 30 seconds on build.kde.org, removed two 0 ;) static const int count = 5000; using namespace KTextEditor; void WordCompletionTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); Editor *editor = KTextEditor::Editor::instance(); QVERIFY(editor); m_doc = editor->createDocument(this); QVERIFY(m_doc); } void WordCompletionTest::cleanupTestCase() { } void WordCompletionTest::init() { m_doc->clear(); } void WordCompletionTest::cleanup() { } void WordCompletionTest::benchWordRetrievalMixed() { const int distinctWordRatio = 100; QStringList s; s.reserve(count); for (int i = 0; i < count; i++) { s.append(QLatin1String("HelloWorld") + QString::number(i / distinctWordRatio)); } s.prepend("\n"); m_doc->setText(s); // creating the view only after inserting the text makes test execution much faster QSharedPointer v(m_doc->createView(nullptr)); - QBENCHMARK - { + QBENCHMARK { KateWordCompletionModel m(nullptr); QCOMPARE(m.allMatches(v.data(), KTextEditor::Range()).size(), count / distinctWordRatio); } } void WordCompletionTest::benchWordRetrievalSame() { QStringList s; s.reserve(count); // add a number so the words have roughly the same length as in the other tests const QString str = QLatin1String("HelloWorld") + QString::number(count); for (int i = 0; i < count; i++) { s.append(str); } s.prepend("\n"); m_doc->setText(s); QSharedPointer v(m_doc->createView(nullptr)); - QBENCHMARK - { + QBENCHMARK { KateWordCompletionModel m(nullptr); QCOMPARE(m.allMatches(v.data(), KTextEditor::Range()).size(), 1); } } void WordCompletionTest::benchWordRetrievalDistinct() { QStringList s; s.reserve(count); for (int i = 0; i < count; i++) { s.append(QLatin1String("HelloWorld") + QString::number(i)); } s.prepend("\n"); m_doc->setText(s); QSharedPointer v(m_doc->createView(nullptr)); - QBENCHMARK - { + QBENCHMARK { KateWordCompletionModel m(nullptr); QCOMPARE(m.allMatches(v.data(), KTextEditor::Range()).size(), count); } } diff --git a/src/buffer/katetexthistory.cpp b/src/buffer/katetexthistory.cpp index d02183a0..f1942657 100644 --- a/src/buffer/katetexthistory.cpp +++ b/src/buffer/katetexthistory.cpp @@ -1,606 +1,606 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katetexthistory.h" #include "katetextbuffer.h" namespace Kate { TextHistory::TextHistory(TextBuffer &buffer) : m_buffer(buffer) , m_lastSavedRevision(-1) , m_firstHistoryEntryRevision(0) { // just call clear to init clear(); } TextHistory::~TextHistory() { } qint64 TextHistory::revision() const { // just output last revisions of buffer return m_buffer.revision(); } void TextHistory::clear() { // reset last saved revision m_lastSavedRevision = -1; // remove all history entries and add no-change dummy for first revision m_historyEntries.clear(); m_historyEntries.push_back(Entry()); // first entry will again belong to first revision m_firstHistoryEntryRevision = 0; } void TextHistory::setLastSavedRevision() { // current revision was successful saved m_lastSavedRevision = revision(); } void TextHistory::wrapLine(const KTextEditor::Cursor &position) { // create and add new entry Entry entry; entry.type = Entry::WrapLine; entry.line = position.line(); entry.column = position.column(); addEntry(entry); } void TextHistory::unwrapLine(int line, int oldLineLength) { // create and add new entry Entry entry; entry.type = Entry::UnwrapLine; entry.line = line; entry.column = 0; entry.oldLineLength = oldLineLength; addEntry(entry); } void TextHistory::insertText(const KTextEditor::Cursor &position, int length, int oldLineLength) { // create and add new entry Entry entry; entry.type = Entry::InsertText; entry.line = position.line(); entry.column = position.column(); entry.length = length; entry.oldLineLength = oldLineLength; addEntry(entry); } void TextHistory::removeText(const KTextEditor::Range &range, int oldLineLength) { // create and add new entry Entry entry; entry.type = Entry::RemoveText; entry.line = range.start().line(); entry.column = range.start().column(); entry.length = range.end().column() - range.start().column(); entry.oldLineLength = oldLineLength; addEntry(entry); } void TextHistory::addEntry(const Entry &entry) { /** * history should never be empty */ Q_ASSERT(!m_historyEntries.empty()); /** * simple efficient check: if we only have one entry, and the entry is not referenced * just replace it with the new one and adjust the revision */ if ((m_historyEntries.size() == 1) && !m_historyEntries.front().referenceCounter) { /** * remember new revision for first element, it is the revision we get after this change */ m_firstHistoryEntryRevision = revision() + 1; /** * remember edit */ m_historyEntries.front() = entry; /** * be done... */ return; } /** * ok, we have more than one entry or the entry is referenced, just add up new entries */ m_historyEntries.push_back(entry); } void TextHistory::lockRevision(qint64 revision) { /** * some invariants must hold */ Q_ASSERT(!m_historyEntries.empty()); Q_ASSERT(revision >= m_firstHistoryEntryRevision); Q_ASSERT(revision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); /** * increment revision reference counter */ Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision]; ++entry.referenceCounter; } void TextHistory::unlockRevision(qint64 revision) { /** * some invariants must hold */ Q_ASSERT(!m_historyEntries.empty()); Q_ASSERT(revision >= m_firstHistoryEntryRevision); Q_ASSERT(revision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); /** * decrement revision reference counter */ Entry &entry = m_historyEntries[revision - m_firstHistoryEntryRevision]; Q_ASSERT(entry.referenceCounter); --entry.referenceCounter; /** * clean up no longer used revisions... */ if (!entry.referenceCounter) { /** * search for now unused stuff */ qint64 unreferencedEdits = 0; for (qint64 i = 0; i + 1 < qint64(m_historyEntries.size()); ++i) { if (m_historyEntries[i].referenceCounter) { break; } // remember deleted count ++unreferencedEdits; } /** * remove unreferred from the list now */ if (unreferencedEdits > 0) { // remove stuff from history m_historyEntries.erase(m_historyEntries.begin(), m_historyEntries.begin() + unreferencedEdits); // patch first entry revision m_firstHistoryEntryRevision += unreferencedEdits; } } } void TextHistory::Entry::transformCursor(int &cursorLine, int &cursorColumn, bool moveOnInsert) const { /** * simple stuff, sort out generic things */ /** * no change, if this change is in line behind cursor */ if (line > cursorLine) { return; } /** * handle all history types */ switch (type) { + /** + * Wrap a line + */ + case WrapLine: /** - * Wrap a line + * we wrap this line */ - case WrapLine: + if (cursorLine == line) { /** - * we wrap this line + * skip cursors with too small column */ - if (cursorLine == line) { - /** - * skip cursors with too small column - */ - if (cursorColumn <= column) { - if (cursorColumn < column || !moveOnInsert) { - return; - } + if (cursorColumn <= column) { + if (cursorColumn < column || !moveOnInsert) { + return; } - - /** - * adjust column - */ - cursorColumn = cursorColumn - column; } /** - * always increment cursor line + * adjust column */ - cursorLine += 1; - return; + cursorColumn = cursorColumn - column; + } /** - * Unwrap a line + * always increment cursor line */ - case UnwrapLine: - /** - * we unwrap this line, adjust column - */ - if (cursorLine == line) { - cursorColumn += oldLineLength; - } - - /** - * decrease cursor line - */ - cursorLine -= 1; - return; + cursorLine += 1; + return; + /** + * Unwrap a line + */ + case UnwrapLine: /** - * Insert text + * we unwrap this line, adjust column */ - case InsertText: - /** - * only interesting, if same line - */ - if (cursorLine != line) { - return; - } - - // skip cursors with too small column - if (cursorColumn <= column) - if (cursorColumn < column || !moveOnInsert) { - return; - } - - // patch column of cursor - if (cursorColumn <= oldLineLength) { - cursorColumn += length; - } - - // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode - else if (cursorColumn < oldLineLength + length) { - cursorColumn = oldLineLength + length; - } + if (cursorLine == line) { + cursorColumn += oldLineLength; + } - return; + /** + * decrease cursor line + */ + cursorLine -= 1; + return; + /** + * Insert text + */ + case InsertText: /** - * Remove text + * only interesting, if same line */ - case RemoveText: - /** - * only interesting, if same line - */ - if (cursorLine != line) { - return; - } + if (cursorLine != line) { + return; + } - // skip cursors with too small column - if (cursorColumn <= column) { + // skip cursors with too small column + if (cursorColumn <= column) + if (cursorColumn < column || !moveOnInsert) { return; } - // patch column of cursor - if (cursorColumn <= column + length) { - cursorColumn = column; - } else { - cursorColumn -= length; - } + // patch column of cursor + if (cursorColumn <= oldLineLength) { + cursorColumn += length; + } - return; + // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode + else if (cursorColumn < oldLineLength + length) { + cursorColumn = oldLineLength + length; + } + + return; + /** + * Remove text + */ + case RemoveText: /** - * nothing + * only interesting, if same line */ - default: + if (cursorLine != line) { + return; + } + + // skip cursors with too small column + if (cursorColumn <= column) { return; + } + + // patch column of cursor + if (cursorColumn <= column + length) { + cursorColumn = column; + } else { + cursorColumn -= length; + } + + return; + + /** + * nothing + */ + default: + return; } } void TextHistory::Entry::reverseTransformCursor(int &cursorLine, int &cursorColumn, bool moveOnInsert) const { /** * handle all history types */ switch (type) { + /** + * Wrap a line + */ + case WrapLine: /** - * Wrap a line + * ignore this line */ - case WrapLine: - /** - * ignore this line - */ - if (cursorLine <= line) { - return; - } - - /** - * next line is unwrapped - */ - if (cursorLine == line + 1) { - /** - * adjust column - */ - cursorColumn = cursorColumn + column; - } - - /** - * always decrement cursor line - */ - cursorLine -= 1; + if (cursorLine <= line) { return; + } /** - * Unwrap a line + * next line is unwrapped */ - case UnwrapLine: - /** - * ignore lines before unwrapped one - */ - if (cursorLine < line - 1) { - return; - } - + if (cursorLine == line + 1) { /** - * we unwrap this line, try to adjust cursor column if needed + * adjust column */ - if (cursorLine == line - 1) { - /** - * skip cursors with to small columns - */ - if (cursorColumn <= oldLineLength) { - if (cursorColumn < oldLineLength || !moveOnInsert) { - return; - } - } + cursorColumn = cursorColumn + column; + } - cursorColumn -= oldLineLength; - } + /** + * always decrement cursor line + */ + cursorLine -= 1; + return; - /** - * increase cursor line - */ - cursorLine += 1; + /** + * Unwrap a line + */ + case UnwrapLine: + /** + * ignore lines before unwrapped one + */ + if (cursorLine < line - 1) { return; + } /** - * Insert text + * we unwrap this line, try to adjust cursor column if needed */ - case InsertText: + if (cursorLine == line - 1) { /** - * only interesting, if same line + * skip cursors with to small columns */ - if (cursorLine != line) { - return; - } - - // skip cursors with too small column - if (cursorColumn <= column) { - return; + if (cursorColumn <= oldLineLength) { + if (cursorColumn < oldLineLength || !moveOnInsert) { + return; + } } - // patch column of cursor - if (cursorColumn - length < column) { - cursorColumn = column; - } else { - cursorColumn -= length; - } + cursorColumn -= oldLineLength; + } - return; + /** + * increase cursor line + */ + cursorLine += 1; + return; + /** + * Insert text + */ + case InsertText: /** - * Remove text + * only interesting, if same line */ - case RemoveText: - /** - * only interesting, if same line - */ - if (cursorLine != line) { - return; - } + if (cursorLine != line) { + return; + } - // skip cursors with too small column - if (cursorColumn <= column) - if (cursorColumn < column || !moveOnInsert) { - return; - } + // skip cursors with too small column + if (cursorColumn <= column) { + return; + } - // patch column of cursor - if (cursorColumn <= oldLineLength) { - cursorColumn += length; - } + // patch column of cursor + if (cursorColumn - length < column) { + cursorColumn = column; + } else { + cursorColumn -= length; + } - // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode - else if (cursorColumn < oldLineLength + length) { - cursorColumn = oldLineLength + length; - } - return; + return; + /** + * Remove text + */ + case RemoveText: /** - * nothing + * only interesting, if same line */ - default: + if (cursorLine != line) { return; + } + + // skip cursors with too small column + if (cursorColumn <= column) + if (cursorColumn < column || !moveOnInsert) { + return; + } + + // patch column of cursor + if (cursorColumn <= oldLineLength) { + cursorColumn += length; + } + + // special handling if cursor behind the real line, e.g. non-wrapping cursor in block selection mode + else if (cursorColumn < oldLineLength + length) { + cursorColumn = oldLineLength + length; + } + return; + + /** + * nothing + */ + default: + return; } } void TextHistory::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { /** * -1 special meaning for from/toRevision */ if (fromRevision == -1) { fromRevision = revision(); } if (toRevision == -1) { toRevision = revision(); } /** * shortcut, same revision */ if (fromRevision == toRevision) { return; } /** * some invariants must hold */ Q_ASSERT(!m_historyEntries.empty()); Q_ASSERT(fromRevision != toRevision); Q_ASSERT(fromRevision >= m_firstHistoryEntryRevision); Q_ASSERT(fromRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); Q_ASSERT(toRevision >= m_firstHistoryEntryRevision); Q_ASSERT(toRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); /** * transform cursor */ bool moveOnInsert = insertBehavior == KTextEditor::MovingCursor::MoveOnInsert; /** * forward or reverse transform? */ if (toRevision > fromRevision) { for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) { const Entry &entry = m_historyEntries.at(rev); entry.transformCursor(line, column, moveOnInsert); } } else { for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) { const Entry &entry = m_historyEntries.at(rev); entry.reverseTransformCursor(line, column, moveOnInsert); } } } void TextHistory::transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision) { /** * invalidate on empty? */ bool invalidateIfEmpty = emptyBehavior == KTextEditor::MovingRange::InvalidateIfEmpty; if (invalidateIfEmpty && range.end() <= range.start()) { range = KTextEditor::Range::invalid(); return; } /** * -1 special meaning for from/toRevision */ if (fromRevision == -1) { fromRevision = revision(); } if (toRevision == -1) { toRevision = revision(); } /** * shortcut, same revision */ if (fromRevision == toRevision) { return; } /** * some invariants must hold */ Q_ASSERT(!m_historyEntries.empty()); Q_ASSERT(fromRevision != toRevision); Q_ASSERT(fromRevision >= m_firstHistoryEntryRevision); Q_ASSERT(fromRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); Q_ASSERT(toRevision >= m_firstHistoryEntryRevision); Q_ASSERT(toRevision < (m_firstHistoryEntryRevision + qint64(m_historyEntries.size()))); /** * transform cursors */ // first: copy cursors, without range association int startLine = range.start().line(), startColumn = range.start().column(), endLine = range.end().line(), endColumn = range.end().column(); bool moveOnInsertStart = !(insertBehaviors & KTextEditor::MovingRange::ExpandLeft); bool moveOnInsertEnd = (insertBehaviors & KTextEditor::MovingRange::ExpandRight); /** * forward or reverse transform? */ if (toRevision > fromRevision) { for (int rev = fromRevision - m_firstHistoryEntryRevision + 1; rev <= (toRevision - m_firstHistoryEntryRevision); ++rev) { const Entry &entry = m_historyEntries.at(rev); entry.transformCursor(startLine, startColumn, moveOnInsertStart); entry.transformCursor(endLine, endColumn, moveOnInsertEnd); // got empty? if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) { if (invalidateIfEmpty) { range = KTextEditor::Range::invalid(); return; } else { // else normalize them endLine = startLine; endColumn = startColumn; } } } } else { for (int rev = fromRevision - m_firstHistoryEntryRevision; rev >= (toRevision - m_firstHistoryEntryRevision + 1); --rev) { const Entry &entry = m_historyEntries.at(rev); entry.reverseTransformCursor(startLine, startColumn, moveOnInsertStart); entry.reverseTransformCursor(endLine, endColumn, moveOnInsertEnd); // got empty? if (endLine < startLine || (endLine == startLine && endColumn <= startColumn)) { if (invalidateIfEmpty) { range = KTextEditor::Range::invalid(); return; } else { // else normalize them endLine = startLine; endColumn = startColumn; } } } } // now, copy cursors back range.setRange(KTextEditor::Cursor(startLine, startColumn), KTextEditor::Cursor(endLine, endColumn)); } } diff --git a/src/completion/expandingtree/expandingwidgetmodel.cpp b/src/completion/expandingtree/expandingwidgetmodel.cpp index ed17d04c..ef4245ad 100644 --- a/src/completion/expandingtree/expandingwidgetmodel.cpp +++ b/src/completion/expandingtree/expandingwidgetmodel.cpp @@ -1,554 +1,554 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "expandingwidgetmodel.h" #include #include #include #include #include #include "kcolorutils.h" #include #include #include "expandingdelegate.h" #include "katepartdebug.h" using namespace KTextEditor; inline QModelIndex firstColumn(const QModelIndex &index) { return index.sibling(index.row(), 0); } ExpandingWidgetModel::ExpandingWidgetModel(QWidget *parent) : QAbstractItemModel(parent) { } ExpandingWidgetModel::~ExpandingWidgetModel() { clearExpanding(); } static QColor doAlternate(const QColor &color) { QColor background = QApplication::palette().window().color(); return KColorUtils::mix(color, background, 0.15); } uint ExpandingWidgetModel::matchColor(const QModelIndex &index) const { int matchQuality = contextMatchQuality(index.sibling(index.row(), 0)); if (matchQuality > 0) { bool alternate = index.row() & 1; QColor badMatchColor(0xff00aa44); // Blue-ish green QColor goodMatchColor(0xff00ff00); // Green QColor background = treeView()->palette().light().color(); QColor totalColor = KColorUtils::mix(badMatchColor, goodMatchColor, ((float)matchQuality) / 10.0); if (alternate) { totalColor = doAlternate(totalColor); } const qreal dynamicTint = 0.2; const qreal minimumTint = 0.2; qreal tintStrength = (dynamicTint * matchQuality) / 10; if (tintStrength != 0.0) { tintStrength += minimumTint; // Some minimum tinting strength, else it's not visible any more } return KColorUtils::tint(background, totalColor, tintStrength).rgb(); } else { return 0; } } QVariant ExpandingWidgetModel::data(const QModelIndex &index, int role) const { switch (role) { - case Qt::BackgroundRole: { - if (index.column() == 0) { - // Highlight by match-quality - uint color = matchColor(index); - if (color) { - return QBrush(color); - } + case Qt::BackgroundRole: { + if (index.column() == 0) { + // Highlight by match-quality + uint color = matchColor(index); + if (color) { + return QBrush(color); } - // Use a special background-color for expanded items - if (isExpanded(index)) { - if (index.row() & 1) { - return doAlternate(treeView()->palette().toolTipBase().color()); - } else { - return treeView()->palette().toolTipBase(); - } + } + // Use a special background-color for expanded items + if (isExpanded(index)) { + if (index.row() & 1) { + return doAlternate(treeView()->palette().toolTipBase().color()); + } else { + return treeView()->palette().toolTipBase(); } } } + } return QVariant(); } void ExpandingWidgetModel::clearMatchQualities() { m_contextMatchQualities.clear(); } QModelIndex ExpandingWidgetModel::partiallyExpandedRow() const { if (m_partiallyExpanded.isEmpty()) { return QModelIndex(); } else { return m_partiallyExpanded.constBegin().key(); } } void ExpandingWidgetModel::clearExpanding() { clearMatchQualities(); QMap oldExpandState = m_expandState; for (auto &widget : qAsConst(m_expandingWidgets)) { if (widget) { widget->deleteLater(); // By using deleteLater, we prevent crashes when an action within a widget makes the completion cancel } } m_expandingWidgets.clear(); m_expandState.clear(); m_partiallyExpanded.clear(); for (auto it = oldExpandState.constBegin(); it != oldExpandState.constEnd(); ++it) { if (it.value() == Expanded) { emit dataChanged(it.key(), it.key()); } } } ExpandingWidgetModel::ExpansionType ExpandingWidgetModel::isPartiallyExpanded(const QModelIndex &index) const { if (m_partiallyExpanded.contains(firstColumn(index))) { return m_partiallyExpanded[firstColumn(index)]; } else { return NotExpanded; } } void ExpandingWidgetModel::partiallyUnExpand(const QModelIndex &idx_) { QModelIndex index(firstColumn(idx_)); m_partiallyExpanded.remove(index); m_partiallyExpanded.remove(idx_); } int ExpandingWidgetModel::partiallyExpandWidgetHeight() const { return 60; ///@todo use font-metrics text-height*2 for 2 lines } void ExpandingWidgetModel::rowSelected(const QModelIndex &idx_) { QModelIndex idx(firstColumn(idx_)); if (!m_partiallyExpanded.contains(idx)) { QModelIndex oldIndex = partiallyExpandedRow(); // Unexpand the previous partially expanded row if (!m_partiallyExpanded.isEmpty()) { ///@todo allow multiple partially expanded rows while (!m_partiallyExpanded.isEmpty()) { m_partiallyExpanded.erase(m_partiallyExpanded.begin()); } // partiallyUnExpand( m_partiallyExpanded.begin().key() ); } // Notify the underlying models that the item was selected, and eventually get back the text for the expanding widget. if (!idx.isValid()) { // All items have been unselected if (oldIndex.isValid()) { emit dataChanged(oldIndex, oldIndex); } } else { QVariant variant = data(idx, CodeCompletionModel::ItemSelected); if (!isExpanded(idx) && variant.type() == QVariant::String) { // Either expand upwards or downwards, choose in a way that // the visible fields of the new selected entry are not moved. if (oldIndex.isValid() && (oldIndex < idx || (!(oldIndex < idx) && oldIndex.parent() < idx.parent()))) { m_partiallyExpanded.insert(idx, ExpandUpwards); } else { m_partiallyExpanded.insert(idx, ExpandDownwards); } // Say that one row above until one row below has changed, so no items will need to be moved(the space that is taken from one item is given to the other) if (oldIndex.isValid() && oldIndex < idx) { emit dataChanged(oldIndex, idx); if (treeView()->verticalScrollMode() == QAbstractItemView::ScrollPerItem) { // Qt fails to correctly scroll in ScrollPerItem mode, so the selected index is completely visible, // so we do the scrolling by hand. QRect selectedRect = treeView()->visualRect(idx); QRect frameRect = treeView()->frameRect(); if (selectedRect.bottom() > frameRect.bottom()) { int diff = selectedRect.bottom() - frameRect.bottom(); // We need to scroll down QModelIndex newTopIndex = idx; QModelIndex nextTopIndex = idx; QRect nextRect = treeView()->visualRect(nextTopIndex); while (nextTopIndex.isValid() && nextRect.isValid() && nextRect.top() >= diff) { newTopIndex = nextTopIndex; nextTopIndex = treeView()->indexAbove(nextTopIndex); if (nextTopIndex.isValid()) { nextRect = treeView()->visualRect(nextTopIndex); } } treeView()->scrollTo(newTopIndex, QAbstractItemView::PositionAtTop); } } // This is needed to keep the item we are expanding completely visible. Qt does not scroll the view to keep the item visible. // But we must make sure that it isn't too expensive. // We need to make sure that scrolling is efficient, and the whole content is not repainted. // Since we are scrolling anyway, we can keep the next line visible, which might be a cool feature. // Since this also doesn't work smoothly, leave it for now // treeView()->scrollTo( nextLine, QAbstractItemView::EnsureVisible ); } else if (oldIndex.isValid() && idx < oldIndex) { emit dataChanged(idx, oldIndex); // For consistency with the down-scrolling, we keep one additional line visible above the current visible. // Since this also doesn't work smoothly, leave it for now /* QModelIndex prevLine = idx.sibling(idx.row()-1, idx.column()); if( prevLine.isValid() ) treeView()->scrollTo( prevLine );*/ } else { emit dataChanged(idx, idx); } } else if (oldIndex.isValid()) { // We are not partially expanding a new row, but we previously had a partially expanded row. So signalize that it has been unexpanded. emit dataChanged(oldIndex, oldIndex); } } } else { qCDebug(LOG_KTE) << "ExpandingWidgetModel::rowSelected: Row is already partially expanded"; } } QString ExpandingWidgetModel::partialExpandText(const QModelIndex &idx) const { if (!idx.isValid()) { return QString(); } return data(firstColumn(idx), CodeCompletionModel::ItemSelected).toString(); } QRect ExpandingWidgetModel::partialExpandRect(const QModelIndex &idx_) const { QModelIndex idx(firstColumn(idx_)); if (!idx.isValid()) { return QRect(); } ExpansionType expansion = ExpandDownwards; if (m_partiallyExpanded.find(idx) != m_partiallyExpanded.constEnd()) { expansion = m_partiallyExpanded[idx]; } // Get the whole rectangle of the row: QModelIndex rightMostIndex = idx; QModelIndex tempIndex = idx; while ((tempIndex = rightMostIndex.sibling(rightMostIndex.row(), rightMostIndex.column() + 1)).isValid()) { rightMostIndex = tempIndex; } QRect rect = treeView()->visualRect(idx); QRect rightMostRect = treeView()->visualRect(rightMostIndex); rect.setLeft(rect.left() + 20); rect.setRight(rightMostRect.right() - 5); // These offsets must match exactly those used in ExpandingDelegate::sizeHint() int top = rect.top() + 5; int bottom = rightMostRect.bottom() - 5; if (expansion == ExpandDownwards) { top += basicRowHeight(idx); } else { bottom -= basicRowHeight(idx); } rect.setTop(top); rect.setBottom(bottom); return rect; } bool ExpandingWidgetModel::isExpandable(const QModelIndex &idx_) const { QModelIndex idx(firstColumn(idx_)); if (!m_expandState.contains(idx)) { m_expandState.insert(idx, NotExpandable); QVariant v = data(idx, CodeCompletionModel::IsExpandable); if (v.canConvert() && v.toBool()) { m_expandState[idx] = Expandable; } } return m_expandState[idx] != NotExpandable; } bool ExpandingWidgetModel::isExpanded(const QModelIndex &idx_) const { QModelIndex idx(firstColumn(idx_)); return m_expandState.contains(idx) && m_expandState[idx] == Expanded; } void ExpandingWidgetModel::setExpanded(QModelIndex idx_, bool expanded) { QModelIndex idx(firstColumn(idx_)); // qCDebug(LOG_KTE) << "Setting expand-state of row " << idx.row() << " to " << expanded; if (!idx.isValid()) { return; } if (isExpandable(idx)) { if (!expanded && m_expandingWidgets.contains(idx) && m_expandingWidgets[idx]) { m_expandingWidgets[idx]->hide(); } m_expandState[idx] = expanded ? Expanded : Expandable; if (expanded) { partiallyUnExpand(idx); } if (expanded && !m_expandingWidgets.contains(idx)) { QVariant v = data(idx, CodeCompletionModel::ExpandingWidget); if (v.canConvert()) { m_expandingWidgets[idx] = v.value(); } else if (v.canConvert()) { // Create a html widget that shows the given string KTextEdit *edit = new KTextEdit(v.toString()); edit->setReadOnly(true); edit->resize(200, 50); // Make the widget small so it embeds nicely. m_expandingWidgets[idx] = edit; } else { m_expandingWidgets[idx] = nullptr; } } // Eventually partially expand the row if (!expanded && firstColumn(treeView()->currentIndex()) == idx && !isPartiallyExpanded(idx)) { rowSelected(idx); // Partially expand the row. } emit dataChanged(idx, idx); if (treeView()) { treeView()->scrollTo(idx); } } } int ExpandingWidgetModel::basicRowHeight(const QModelIndex &idx_) const { QModelIndex idx(firstColumn(idx_)); ExpandingDelegate *delegate = dynamic_cast(treeView()->itemDelegate(idx)); if (!delegate || !idx.isValid()) { qCDebug(LOG_KTE) << "ExpandingWidgetModel::basicRowHeight: Could not get delegate"; return 15; } return delegate->basicSizeHint(idx).height(); } void ExpandingWidgetModel::placeExpandingWidget(const QModelIndex &idx_) { QModelIndex idx(firstColumn(idx_)); if (!idx.isValid() || !isExpanded(idx)) { return; } QWidget *w = m_expandingWidgets.value(idx); if (!w) { return; } QRect rect = treeView()->visualRect(idx); if (!rect.isValid() || rect.bottom() < 0 || rect.top() >= treeView()->height()) { // The item is currently not visible w->hide(); return; } // Find out the basic width of the row rect.setLeft(rect.left() + 20); for (int i = 0, numColumns = idx.model()->columnCount(idx.parent()); i < numColumns; ++i) { QModelIndex rightMostIndex = idx.sibling(idx.row(), i); int right = treeView()->visualRect(rightMostIndex).right(); if (right > rect.right()) { rect.setRight(right); } } rect.setRight(rect.right() - 5); // These offsets must match exactly those used in KateCompletionDeleage::sizeHint() rect.setTop(rect.top() + basicRowHeight(idx) + 5); rect.setHeight(w->height()); if (w->parent() != treeView()->viewport() || w->geometry() != rect || !w->isVisible()) { w->setParent(treeView()->viewport()); w->setGeometry(rect); w->show(); } } void ExpandingWidgetModel::placeExpandingWidgets() { for (QMap>::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) { placeExpandingWidget(it.key()); } } int ExpandingWidgetModel::expandingWidgetsHeight() const { int sum = 0; for (QMap>::const_iterator it = m_expandingWidgets.constBegin(); it != m_expandingWidgets.constEnd(); ++it) { if (isExpanded(it.key()) && (*it)) { sum += (*it)->height(); } } return sum; } QWidget *ExpandingWidgetModel::expandingWidget(const QModelIndex &idx_) const { QModelIndex idx(firstColumn(idx_)); if (m_expandingWidgets.contains(idx)) { return m_expandingWidgets[idx]; } else { return nullptr; } } void ExpandingWidgetModel::cacheIcons() const { if (m_expandedIcon.isNull()) { m_expandedIcon = QIcon::fromTheme(QStringLiteral("arrow-down")); } if (m_collapsedIcon.isNull()) { m_collapsedIcon = QIcon::fromTheme(QStringLiteral("arrow-right")); } } QList mergeCustomHighlighting(int leftSize, const QList &left, int rightSize, const QList &right) { QList ret = left; if (left.isEmpty()) { ret << QVariant(0); ret << QVariant(leftSize); ret << QTextFormat(QTextFormat::CharFormat); } if (right.isEmpty()) { ret << QVariant(leftSize); ret << QVariant(rightSize); ret << QTextFormat(QTextFormat::CharFormat); } else { QList::const_iterator it = right.constBegin(); while (it != right.constEnd()) { { QList::const_iterator testIt = it; for (int a = 0; a < 2; a++) { ++testIt; if (testIt == right.constEnd()) { qCWarning(LOG_KTE) << "Length of input is not multiple of 3"; break; } } } ret << QVariant((*it).toInt() + leftSize); ++it; ret << QVariant((*it).toInt()); ++it; ret << *it; if (!(*it).value().isValid()) { qCDebug(LOG_KTE) << "Text-format is invalid"; } ++it; } } return ret; } // It is assumed that between each two strings, one space is inserted QList mergeCustomHighlighting(QStringList strings, QList highlights, int grapBetweenStrings) { if (strings.isEmpty()) { qCWarning(LOG_KTE) << "List of strings is empty"; return QList(); } if (highlights.isEmpty()) { qCWarning(LOG_KTE) << "List of highlightings is empty"; return QList(); } if (strings.count() != highlights.count()) { qCWarning(LOG_KTE) << "Length of string-list is " << strings.count() << " while count of highlightings is " << highlights.count() << ", should be same"; return QList(); } // Merge them together QString totalString = strings[0]; QVariantList totalHighlighting = highlights[0]; strings.pop_front(); highlights.pop_front(); while (!strings.isEmpty()) { totalHighlighting = mergeCustomHighlighting(totalString.length(), totalHighlighting, strings[0].length(), highlights[0]); totalString += strings[0]; for (int a = 0; a < grapBetweenStrings; a++) { totalString += QLatin1Char(' '); } strings.pop_front(); highlights.pop_front(); } // Combine the custom-highlightings return totalHighlighting; } diff --git a/src/completion/kateargumenthintmodel.cpp b/src/completion/kateargumenthintmodel.cpp index 891ca60b..d38c104f 100644 --- a/src/completion/kateargumenthintmodel.cpp +++ b/src/completion/kateargumenthintmodel.cpp @@ -1,344 +1,344 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2007 David Nolden * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateargumenthintmodel.h" #include "kateargumenthinttree.h" #include "katecompletiontree.h" #include "katecompletionwidget.h" #include "katepartdebug.h" #include #include #include #include using namespace KTextEditor; void KateArgumentHintModel::clear() { m_rows.clear(); clearExpanding(); } QModelIndex KateArgumentHintModel::mapToSource(const QModelIndex &index) const { if (index.row() < 0 || index.row() >= m_rows.count()) { return QModelIndex(); } if (m_rows[index.row()] < 0 || m_rows[index.row()] >= group()->filtered.count()) { return QModelIndex(); } KateCompletionModel::ModelRow source = group()->filtered[m_rows[index.row()]].sourceRow(); if (!source.first) { qCDebug(LOG_KTE) << "KateArgumentHintModel::data: Row does not exist in source"; return QModelIndex(); } QModelIndex sourceIndex = source.second.sibling(source.second.row(), index.column()); return sourceIndex; } void KateArgumentHintModel::parentModelReset() { clear(); buildRows(); } void KateArgumentHintModel::buildRows() { beginResetModel(); m_rows.clear(); QMap> m_depths; // Map each hint-depth to a list of functions of that depth for (int a = 0; a < group()->filtered.count(); a++) { KateCompletionModel::ModelRow source = group()->filtered[a].sourceRow(); QModelIndex sourceIndex = source.second.sibling(source.second.row(), 0); QVariant v = sourceIndex.data(CodeCompletionModel::ArgumentHintDepth); if (v.type() == QVariant::Int) { QList &lst(m_depths[v.toInt()]); lst << a; } } for (QMap>::const_iterator it = m_depths.constBegin(); it != m_depths.constEnd(); ++it) { for (int row : *it) { m_rows.push_front(row); // Insert filtered in reversed order } m_rows.push_front(-it.key()); } endResetModel(); emit contentStateChanged(!m_rows.isEmpty()); } KateArgumentHintModel::KateArgumentHintModel(KateCompletionWidget *parent) : ExpandingWidgetModel(parent) , m_parent(parent) { connect(parent->model(), SIGNAL(modelReset()), this, SLOT(parentModelReset())); connect(parent->model(), SIGNAL(argumentHintsChanged()), this, SLOT(parentModelReset())); } QVariant KateArgumentHintModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_rows.count()) { // qCDebug(LOG_KTE) << "KateArgumentHintModel::data: index out of bound: " << index.row() << " total filtered: " << m_rows.count(); return QVariant(); } if (m_rows[index.row()] < 0) { // Show labels if (role == Qt::DisplayRole && index.column() == 0) { return QString(); // QString("Depth %1").arg(-m_rows[index.row()]); } else if (role == Qt::BackgroundRole) { return QApplication::palette().toolTipBase().color(); } else if (role == Qt::ForegroundRole) { return QApplication::palette().toolTipText().color(); } else { return QVariant(); } } if (m_rows[index.row()] < 0 || m_rows[index.row()] >= group()->filtered.count()) { qCDebug(LOG_KTE) << "KateArgumentHintModel::data: index out of bound: " << m_rows[index.row()] << " total filtered: " << group()->filtered.count(); return QVariant(); } KateCompletionModel::ModelRow source = group()->filtered[m_rows[index.row()]].sourceRow(); if (!source.first) { qCDebug(LOG_KTE) << "KateArgumentHintModel::data: Row does not exist in source"; return QVariant(); } if (index.column() == 0) { switch (role) { - case Qt::DecorationRole: { - // Show the expand-handle - model()->cacheIcons(); - - if (!isExpanded(index)) { - return QVariant(model()->m_collapsedIcon); - } else { - return QVariant(model()->m_expandedIcon); - } + case Qt::DecorationRole: { + // Show the expand-handle + model()->cacheIcons(); + + if (!isExpanded(index)) { + return QVariant(model()->m_collapsedIcon); + } else { + return QVariant(model()->m_expandedIcon); } - case Qt::DisplayRole: - // Ignore text in the first column(we create our own compound text in the second) - return QVariant(); + } + case Qt::DisplayRole: + // Ignore text in the first column(we create our own compound text in the second) + return QVariant(); } } QModelIndex sourceIndex = source.second.sibling(source.second.row(), index.column()); if (!sourceIndex.isValid()) { qCDebug(LOG_KTE) << "KateArgumentHintModel::data: Source-index is not valid"; return QVariant(); } switch (role) { - case Qt::DisplayRole: { - // Construct the text - QString totalText; - for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) - if (a != CodeCompletionModel::Scope) { // Skip the scope - totalText += source.second.sibling(source.second.row(), a).data(Qt::DisplayRole).toString() + QLatin1Char(' '); - } - - return QVariant(totalText); - } - case CodeCompletionModel::HighlightingMethod: { - // Return that we are doing custom-highlighting of one of the sub-strings does it - for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) { - QVariant method = source.second.sibling(source.second.row(), a).data(CodeCompletionModel::HighlightingMethod); - if (method.type() == QVariant::Int && method.toInt() == CodeCompletionModel::CustomHighlighting) { - return QVariant(CodeCompletionModel::CustomHighlighting); - } + case Qt::DisplayRole: { + // Construct the text + QString totalText; + for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) + if (a != CodeCompletionModel::Scope) { // Skip the scope + totalText += source.second.sibling(source.second.row(), a).data(Qt::DisplayRole).toString() + QLatin1Char(' '); } - return QVariant(); + return QVariant(totalText); + } + case CodeCompletionModel::HighlightingMethod: { + // Return that we are doing custom-highlighting of one of the sub-strings does it + for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) { + QVariant method = source.second.sibling(source.second.row(), a).data(CodeCompletionModel::HighlightingMethod); + if (method.type() == QVariant::Int && method.toInt() == CodeCompletionModel::CustomHighlighting) { + return QVariant(CodeCompletionModel::CustomHighlighting); + } } - case CodeCompletionModel::CustomHighlight: { - QStringList strings; - // Collect strings - for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) { - strings << source.second.sibling(source.second.row(), a).data(Qt::DisplayRole).toString(); - } + return QVariant(); + } + case CodeCompletionModel::CustomHighlight: { + QStringList strings; - QList highlights; + // Collect strings + for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) { + strings << source.second.sibling(source.second.row(), a).data(Qt::DisplayRole).toString(); + } - // Collect custom-highlightings - for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) { - highlights << source.second.sibling(source.second.row(), a).data(CodeCompletionModel::CustomHighlight).toList(); - } + QList highlights; - // Replace invalid QTextFormats with match-quality color or yellow. - for (QList::iterator it = highlights.begin(); it != highlights.end(); ++it) { - QVariantList &list(*it); + // Collect custom-highlightings + for (int a = CodeCompletionModel::Prefix; a <= CodeCompletionModel::Postfix; a++) { + highlights << source.second.sibling(source.second.row(), a).data(CodeCompletionModel::CustomHighlight).toList(); + } - for (int a = 2; a < list.count(); a += 3) { - if (list[a].canConvert()) { - QTextFormat f = list[a].value(); + // Replace invalid QTextFormats with match-quality color or yellow. + for (QList::iterator it = highlights.begin(); it != highlights.end(); ++it) { + QVariantList &list(*it); - if (!f.isValid()) { - f = QTextFormat(QTextFormat::CharFormat); - uint color = matchColor(index); + for (int a = 2; a < list.count(); a += 3) { + if (list[a].canConvert()) { + QTextFormat f = list[a].value(); - if (color) { - f.setBackground(QBrush(color)); - } else { - f.setBackground(Qt::yellow); - } + if (!f.isValid()) { + f = QTextFormat(QTextFormat::CharFormat); + uint color = matchColor(index); - list[a] = QVariant(f); + if (color) { + f.setBackground(QBrush(color)); + } else { + f.setBackground(Qt::yellow); } + + list[a] = QVariant(f); } } } - - return mergeCustomHighlighting(strings, highlights, 1); - } - case Qt::DecorationRole: { - // Redirect the decoration to the decoration of the item-column - return source.second.sibling(source.second.row(), CodeCompletionModel::Icon).data(role); } + + return mergeCustomHighlighting(strings, highlights, 1); + } + case Qt::DecorationRole: { + // Redirect the decoration to the decoration of the item-column + return source.second.sibling(source.second.row(), CodeCompletionModel::Icon).data(role); + } } QVariant v = ExpandingWidgetModel::data(index, role); if (v.isValid()) { return v; } else { return sourceIndex.data(role); } } int KateArgumentHintModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return m_rows.count(); } else { return 0; } } int KateArgumentHintModel::columnCount(const QModelIndex & /*parent*/) const { return 2; // 2 Columns, one for the expand-handle, one for the signature } KateCompletionModel::Group *KateArgumentHintModel::group() const { return model()->m_argumentHints; } QModelIndex KateArgumentHintModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || row > rowCount() || column < 0 || column > columnCount() || parent.isValid()) { return {}; } return createIndex(row, column); } QModelIndex KateArgumentHintModel::parent(const QModelIndex & /*parent*/) const { return {}; } KateCompletionModel *KateArgumentHintModel::model() const { return m_parent->model(); } QTreeView *KateArgumentHintModel::treeView() const { return m_parent->argumentHintTree(); } void KateArgumentHintModel::emitDataChanged(const QModelIndex &start, const QModelIndex &end) { emit dataChanged(start, end); } bool KateArgumentHintModel::indexIsItem(const QModelIndex &index) const { return index.row() >= 0 && index.row() < m_rows.count() && m_rows[index.row()] >= 0; } int KateArgumentHintModel::contextMatchQuality(const QModelIndex &index) const { int row = index.row(); if (row < 0 || row >= m_rows.count()) { return -1; } if (m_rows[row] < 0 || m_rows[row] >= group()->filtered.count()) { return -1; // Probably a label } KateCompletionModel::ModelRow source = group()->filtered[m_rows[row]].sourceRow(); if (!source.first) { return -1; } QModelIndex sourceIndex = source.second.sibling(source.second.row(), 0); if (!sourceIndex.isValid()) { return -1; } int depth = sourceIndex.data(CodeCompletionModel::ArgumentHintDepth).toInt(); switch (depth) { - case 1: { - // This argument-hint is on the lowest level, match it with the currently selected item in the completion-widget - QModelIndex row = m_parent->treeView()->currentIndex(); - if (!row.isValid()) { - return -1; - } + case 1: { + // This argument-hint is on the lowest level, match it with the currently selected item in the completion-widget + QModelIndex row = m_parent->treeView()->currentIndex(); + if (!row.isValid()) { + return -1; + } - QModelIndex selectedIndex = m_parent->model()->mapToSource(row); - if (!selectedIndex.isValid()) { - return -1; - } + QModelIndex selectedIndex = m_parent->model()->mapToSource(row); + if (!selectedIndex.isValid()) { + return -1; + } - if (selectedIndex.model() != sourceIndex.model()) { - return -1; // We can only match items from the same source-model - } + if (selectedIndex.model() != sourceIndex.model()) { + return -1; // We can only match items from the same source-model + } - sourceIndex.data(CodeCompletionModel::SetMatchContext); + sourceIndex.data(CodeCompletionModel::SetMatchContext); - QVariant v = selectedIndex.data(CodeCompletionModel::MatchQuality); - if (v.type() == QVariant::Int) { - return v.toInt(); - } - } break; - default: - // Do some other nice matching here in future - break; + QVariant v = selectedIndex.data(CodeCompletionModel::MatchQuality); + if (v.type() == QVariant::Int) { + return v.toInt(); + } + } break; + default: + // Do some other nice matching here in future + break; } return -1; } diff --git a/src/completion/katecompletionmodel.cpp b/src/completion/katecompletionmodel.cpp index 65617664..4bcb0052 100644 --- a/src/completion/katecompletionmodel.cpp +++ b/src/completion/katecompletionmodel.cpp @@ -1,2437 +1,2437 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2005-2006 Hamish Rodda * Copyright (C) 2007-2008 David Nolden * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katecompletionmodel.h" #include "kateargumenthintmodel.h" #include "katecompletiondelegate.h" #include "katecompletiontree.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "katepartdebug.h" #include "katerenderer.h" #include "kateview.h" #include #include #include #include #include #include #include using namespace KTextEditor; /// A helper-class for handling completion-models with hierarchical grouping/optimization class HierarchicalModelHandler { public: explicit HierarchicalModelHandler(CodeCompletionModel *model); void addValue(CodeCompletionModel::ExtraItemDataRoles role, const QVariant &value); // Walks the index upwards and collects all defined completion-roles on the way void collectRoles(const QModelIndex &index); void takeRole(const QModelIndex &index); CodeCompletionModel *model() const; // Assumes that index is a sub-index of the indices where role-values were taken QVariant getData(CodeCompletionModel::ExtraItemDataRoles role, const QModelIndex &index) const; bool hasHierarchicalRoles() const; int inheritanceDepth(const QModelIndex &i) const; QString customGroup() const { return m_customGroup; } int customGroupingKey() const { return m_groupSortingKey; } private: typedef QMap RoleMap; RoleMap m_roleValues; QString m_customGroup; int m_groupSortingKey; CodeCompletionModel *m_model; }; CodeCompletionModel *HierarchicalModelHandler::model() const { return m_model; } bool HierarchicalModelHandler::hasHierarchicalRoles() const { return !m_roleValues.isEmpty(); } void HierarchicalModelHandler::collectRoles(const QModelIndex &index) { if (index.parent().isValid()) { collectRoles(index.parent()); } if (m_model->rowCount(index) != 0) { takeRole(index); } } int HierarchicalModelHandler::inheritanceDepth(const QModelIndex &i) const { return getData(CodeCompletionModel::InheritanceDepth, i).toInt(); } void HierarchicalModelHandler::takeRole(const QModelIndex &index) { QVariant v = index.data(CodeCompletionModel::GroupRole); if (v.isValid() && v.canConvert(QVariant::Int)) { QVariant value = index.data(v.toInt()); if (v.toInt() == Qt::DisplayRole) { m_customGroup = index.data(Qt::DisplayRole).toString(); QVariant sortingKey = index.data(CodeCompletionModel::InheritanceDepth); if (sortingKey.canConvert(QVariant::Int)) { m_groupSortingKey = sortingKey.toInt(); } } else { m_roleValues[(CodeCompletionModel::ExtraItemDataRoles)v.toInt()] = value; } } else { qCDebug(LOG_KTE) << "Did not return valid GroupRole in hierarchical completion-model"; } } QVariant HierarchicalModelHandler::getData(CodeCompletionModel::ExtraItemDataRoles role, const QModelIndex &index) const { RoleMap::const_iterator it = m_roleValues.find(role); if (it != m_roleValues.end()) { return *it; } else { return index.data(role); } } HierarchicalModelHandler::HierarchicalModelHandler(CodeCompletionModel *model) : m_groupSortingKey(-1) , m_model(model) { } void HierarchicalModelHandler::addValue(CodeCompletionModel::ExtraItemDataRoles role, const QVariant &value) { m_roleValues[role] = value; } KateCompletionModel::KateCompletionModel(KateCompletionWidget *parent) : ExpandingWidgetModel(parent) , m_ungrouped(new Group({}, 0, this)) , m_argumentHints(new Group(i18n("Argument-hints"), -1, this)) , m_bestMatches(new Group(i18n("Best matches"), BestMatchesProperty, this)) , m_filterAttributes(KTextEditor::CodeCompletionModel::NoProperty) { m_emptyGroups.append(m_ungrouped); m_emptyGroups.append(m_argumentHints); m_emptyGroups.append(m_bestMatches); m_updateBestMatchesTimer = new QTimer(this); m_updateBestMatchesTimer->setSingleShot(true); connect(m_updateBestMatchesTimer, SIGNAL(timeout()), this, SLOT(updateBestMatches())); m_groupHash.insert(0, m_ungrouped); m_groupHash.insert(-1, m_argumentHints); m_groupHash.insert(BestMatchesProperty, m_argumentHints); } KateCompletionModel::~KateCompletionModel() { clearCompletionModels(); delete m_argumentHints; delete m_ungrouped; delete m_bestMatches; } QTreeView *KateCompletionModel::treeView() const { return view()->completionWidget()->treeView(); } QVariant KateCompletionModel::data(const QModelIndex &index, int role) const { if (!hasCompletionModel() || !index.isValid()) { return QVariant(); } if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Prefix && isExpandable(index)) { cacheIcons(); if (!isExpanded(index)) { return QVariant(m_collapsedIcon); } else { return QVariant(m_expandedIcon); } } // groupOfParent returns a group when the index is a member of that group, but not the group head/label. if (!hasGroups() || groupOfParent(index)) { if (role == Qt::TextAlignmentRole) { if (isColumnMergingEnabled() && !m_columnMerges.isEmpty()) { int c = 0; for (const QList &list : qAsConst(m_columnMerges)) { if (index.column() < c + list.size()) { c += list.size(); continue; } else if (list.count() == 1 && list.first() == CodeCompletionModel::Scope) { return Qt::AlignRight; } else { return QVariant(); } } } else if ((!isColumnMergingEnabled() || m_columnMerges.isEmpty()) && index.column() == CodeCompletionModel::Scope) { return Qt::AlignRight; } } // Merge text for column merging if (role == Qt::DisplayRole && !m_columnMerges.isEmpty() && isColumnMergingEnabled()) { QString text; for (int column : m_columnMerges[index.column()]) { QModelIndex sourceIndex = mapToSource(createIndex(index.row(), column, index.internalPointer())); text.append(sourceIndex.data(role).toString()); } return text; } if (role == CodeCompletionModel::HighlightingMethod) { // Return that we are doing custom-highlighting of one of the sub-strings does it. Unfortunately internal highlighting does not work for the other substrings. for (int column : m_columnMerges[index.column()]) { QModelIndex sourceIndex = mapToSource(createIndex(index.row(), column, index.internalPointer())); QVariant method = sourceIndex.data(CodeCompletionModel::HighlightingMethod); if (method.type() == QVariant::Int && method.toInt() == CodeCompletionModel::CustomHighlighting) { return QVariant(CodeCompletionModel::CustomHighlighting); } } return QVariant(); } if (role == CodeCompletionModel::CustomHighlight) { // Merge custom highlighting if multiple columns were merged QStringList strings; // Collect strings const auto &columns = m_columnMerges[index.column()]; strings.reserve(columns.size()); for (int column : columns) { strings << mapToSource(createIndex(index.row(), column, index.internalPointer())).data(Qt::DisplayRole).toString(); } QList highlights; // Collect custom-highlightings highlights.reserve(columns.size()); for (int column : columns) { highlights << mapToSource(createIndex(index.row(), column, index.internalPointer())).data(CodeCompletionModel::CustomHighlight).toList(); } return mergeCustomHighlighting(strings, highlights, 0); } QVariant v = mapToSource(index).data(role); if (v.isValid()) { return v; } else { return ExpandingWidgetModel::data(index, role); } } // Returns a nonzero group if this index is the head of a group(A Label in the list) Group *g = groupForIndex(index); if (g && (!g->isEmpty)) { switch (role) { - case Qt::DisplayRole: - if (!index.column()) { - return g->title; - } - break; + case Qt::DisplayRole: + if (!index.column()) { + return g->title; + } + break; - case Qt::FontRole: - if (!index.column()) { - QFont f = view()->renderer()->currentFont(); - f.setBold(true); - return f; - } - break; + case Qt::FontRole: + if (!index.column()) { + QFont f = view()->renderer()->currentFont(); + f.setBold(true); + return f; + } + break; - case Qt::ForegroundRole: - return QApplication::palette().toolTipText().color(); - case Qt::BackgroundRole: - return QApplication::palette().toolTipBase().color(); + case Qt::ForegroundRole: + return QApplication::palette().toolTipText().color(); + case Qt::BackgroundRole: + return QApplication::palette().toolTipBase().color(); } } return QVariant(); } int KateCompletionModel::contextMatchQuality(const QModelIndex &index) const { if (!index.isValid()) { return 0; } Group *g = groupOfParent(index); if (!g || g->filtered.size() < index.row()) { return 0; } return contextMatchQuality(g->filtered[index.row()].sourceRow()); } int KateCompletionModel::contextMatchQuality(const ModelRow &source) const { QModelIndex realIndex = source.second; int bestMatch = -1; // Iterate through all argument-hints and find the best match-quality for (const Item &item : qAsConst(m_argumentHints->filtered)) { const ModelRow &row(item.sourceRow()); if (realIndex.model() != row.first) { continue; // We can only match within the same source-model } QModelIndex hintIndex = row.second; QVariant depth = hintIndex.data(CodeCompletionModel::ArgumentHintDepth); if (!depth.isValid() || depth.type() != QVariant::Int || depth.toInt() != 1) { continue; // Only match completion-items to argument-hints of depth 1(the ones the item will be given to as argument) } hintIndex.data(CodeCompletionModel::SetMatchContext); QVariant matchQuality = realIndex.data(CodeCompletionModel::MatchQuality); if (matchQuality.isValid() && matchQuality.type() == QVariant::Int) { int m = matchQuality.toInt(); if (m > bestMatch) { bestMatch = m; } } } if (m_argumentHints->filtered.isEmpty()) { QVariant matchQuality = realIndex.data(CodeCompletionModel::MatchQuality); if (matchQuality.isValid() && matchQuality.type() == QVariant::Int) { int m = matchQuality.toInt(); if (m > bestMatch) { bestMatch = m; } } } return bestMatch; } Qt::ItemFlags KateCompletionModel::flags(const QModelIndex &index) const { if (!hasCompletionModel() || !index.isValid()) { return Qt::NoItemFlags; } if (!hasGroups() || groupOfParent(index)) { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } return Qt::ItemIsEnabled; } KateCompletionWidget *KateCompletionModel::widget() const { return static_cast(QObject::parent()); } KTextEditor::ViewPrivate *KateCompletionModel::view() const { return widget()->view(); } void KateCompletionModel::setMatchCaseSensitivity(Qt::CaseSensitivity match_cs) { m_matchCaseSensitivity = match_cs; Q_ASSERT(m_exactMatchCaseSensitivity == m_matchCaseSensitivity || m_matchCaseSensitivity == Qt::CaseInsensitive); } void KateCompletionModel::setMatchCaseSensitivity(Qt::CaseSensitivity match_cs, Qt::CaseSensitivity exact_match_cs) { m_matchCaseSensitivity = match_cs; m_exactMatchCaseSensitivity = exact_match_cs; Q_ASSERT(m_exactMatchCaseSensitivity == m_matchCaseSensitivity || m_matchCaseSensitivity == Qt::CaseInsensitive); } int KateCompletionModel::columnCount(const QModelIndex &) const { return isColumnMergingEnabled() && !m_columnMerges.isEmpty() ? m_columnMerges.count() : KTextEditor::CodeCompletionModel::ColumnCount; } KateCompletionModel::ModelRow KateCompletionModel::modelRowPair(const QModelIndex &index) const { return qMakePair(static_cast(const_cast(index.model())), index); } bool KateCompletionModel::hasChildren(const QModelIndex &parent) const { if (!hasCompletionModel()) { return false; } if (!parent.isValid()) { if (hasGroups()) { return true; } return !m_ungrouped->filtered.isEmpty(); } if (parent.column() != 0) { return false; } if (!hasGroups()) { return false; } if (Group *g = groupForIndex(parent)) { return !g->filtered.isEmpty(); } return false; } QModelIndex KateCompletionModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) { return QModelIndex(); } if (parent.isValid() || !hasGroups()) { if (parent.isValid() && parent.column() != 0) { return QModelIndex(); } Group *g = groupForIndex(parent); if (!g) { return QModelIndex(); } if (row >= g->filtered.count()) { // qCWarning(LOG_KTE) << "Invalid index requested: row " << row << " beyond individual range in group " << g; return QModelIndex(); } // qCDebug(LOG_KTE) << "Returning index for child " << row << " of group " << g; return createIndex(row, column, g); } if (row >= m_rowTable.count()) { // qCWarning(LOG_KTE) << "Invalid index requested: row " << row << " beyond group range."; return QModelIndex(); } // qCDebug(LOG_KTE) << "Returning index for group " << m_rowTable[row]; return createIndex(row, column, quintptr(0)); } /*QModelIndex KateCompletionModel::sibling( int row, int column, const QModelIndex & index ) const { if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) return QModelIndex(); if (!index.isValid()) { } if (Group* g = groupOfParent(index)) { if (row >= g->filtered.count()) return QModelIndex(); return createIndex(row, column, g); } if (hasGroups()) return QModelIndex(); if (row >= m_ungrouped->filtered.count()) return QModelIndex(); return createIndex(row, column, m_ungrouped); }*/ bool KateCompletionModel::hasIndex(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || column >= columnCount(QModelIndex())) { return false; } if (parent.isValid() || !hasGroups()) { if (parent.isValid() && parent.column() != 0) { return false; } Group *g = groupForIndex(parent); if (row >= g->filtered.count()) { return false; } return true; } if (row >= m_rowTable.count()) { return false; } return true; } QModelIndex KateCompletionModel::indexForRow(Group *g, int row) const { if (row < 0 || row >= g->filtered.count()) { return QModelIndex(); } return createIndex(row, 0, g); } QModelIndex KateCompletionModel::indexForGroup(Group *g) const { if (!hasGroups()) { return QModelIndex(); } int row = m_rowTable.indexOf(g); if (row == -1) { return QModelIndex(); } return createIndex(row, 0, quintptr(0)); } void KateCompletionModel::clearGroups() { clearExpanding(); m_ungrouped->clear(); m_argumentHints->clear(); m_bestMatches->clear(); // Don't bother trying to work out where it is m_rowTable.removeAll(m_ungrouped); m_emptyGroups.removeAll(m_ungrouped); m_rowTable.removeAll(m_argumentHints); m_emptyGroups.removeAll(m_argumentHints); m_rowTable.removeAll(m_bestMatches); m_emptyGroups.removeAll(m_bestMatches); qDeleteAll(m_rowTable); qDeleteAll(m_emptyGroups); m_rowTable.clear(); m_emptyGroups.clear(); m_groupHash.clear(); m_customGroupHash.clear(); m_emptyGroups.append(m_ungrouped); m_groupHash.insert(0, m_ungrouped); m_emptyGroups.append(m_argumentHints); m_groupHash.insert(-1, m_argumentHints); m_emptyGroups.append(m_bestMatches); m_groupHash.insert(BestMatchesProperty, m_bestMatches); } QSet KateCompletionModel::createItems(const HierarchicalModelHandler &_handler, const QModelIndex &i, bool notifyModel) { HierarchicalModelHandler handler(_handler); QSet ret; QAbstractItemModel *model = handler.model(); if (model->rowCount(i) == 0) { // Leaf node, create an item ret.insert(createItem(handler, i, notifyModel)); } else { // Non-leaf node, take the role from the node, and recurse to the sub-nodes handler.takeRole(i); for (int a = 0; a < model->rowCount(i); a++) { ret += createItems(handler, model->index(a, 0, i), notifyModel); } } return ret; } QSet KateCompletionModel::deleteItems(const QModelIndex &i) { QSet ret; if (i.model()->rowCount(i) == 0) { // Leaf node, delete the item Group *g = groupForIndex(mapFromSource(i)); ret.insert(g); g->removeItem(ModelRow(const_cast(static_cast(i.model())), i)); } else { // Non-leaf node for (int a = 0; a < i.model()->rowCount(i); a++) { ret += deleteItems(i.model()->index(a, 0, i)); } } return ret; } void KateCompletionModel::createGroups() { beginResetModel(); // After clearing the model, it has to be reset, else we will be in an invalid state while inserting // new groups. clearGroups(); bool has_groups = false; for (CodeCompletionModel *sourceModel : qAsConst(m_completionModels)) { has_groups |= sourceModel->hasGroups(); for (int i = 0; i < sourceModel->rowCount(); ++i) { createItems(HierarchicalModelHandler(sourceModel), sourceModel->index(i, 0)); } } m_hasGroups = has_groups; // debugStats(); for (Group *g : qAsConst(m_rowTable)) { hideOrShowGroup(g); } for (Group *g : qAsConst(m_emptyGroups)) { hideOrShowGroup(g); } makeGroupItemsUnique(); updateBestMatches(); endResetModel(); } KateCompletionModel::Group *KateCompletionModel::createItem(const HierarchicalModelHandler &handler, const QModelIndex &sourceIndex, bool notifyModel) { // QModelIndex sourceIndex = sourceModel->index(row, CodeCompletionModel::Name, QModelIndex()); int completionFlags = handler.getData(CodeCompletionModel::CompletionRole, sourceIndex).toInt(); // Scope is expensive, should not be used with big models QString scopeIfNeeded = (groupingMethod() & Scope) ? sourceIndex.sibling(sourceIndex.row(), CodeCompletionModel::Scope).data(Qt::DisplayRole).toString() : QString(); int argumentHintDepth = handler.getData(CodeCompletionModel::ArgumentHintDepth, sourceIndex).toInt(); Group *g; if (argumentHintDepth) { g = m_argumentHints; } else { QString customGroup = handler.customGroup(); if (!customGroup.isNull() && m_hasGroups) { if (m_customGroupHash.contains(customGroup)) { g = m_customGroupHash[customGroup]; } else { g = new Group(customGroup, 0, this); g->customSortingKey = handler.customGroupingKey(); m_emptyGroups.append(g); m_customGroupHash.insert(customGroup, g); } } else { g = fetchGroup(completionFlags, scopeIfNeeded, handler.hasHierarchicalRoles()); } } Item item = Item(g != m_argumentHints, this, handler, ModelRow(handler.model(), sourceIndex)); if (g != m_argumentHints) { item.match(); } g->addItem(item, notifyModel); return g; } void KateCompletionModel::slotRowsInserted(const QModelIndex &parent, int start, int end) { QSet affectedGroups; HierarchicalModelHandler handler(static_cast(sender())); if (parent.isValid()) { handler.collectRoles(parent); } for (int i = start; i <= end; ++i) { affectedGroups += createItems(handler, handler.model()->index(i, 0, parent), true); } for (Group *g : qAsConst(affectedGroups)) { hideOrShowGroup(g, true); } } void KateCompletionModel::slotRowsRemoved(const QModelIndex &parent, int start, int end) { CodeCompletionModel *source = static_cast(sender()); QSet affectedGroups; for (int i = start; i <= end; ++i) { QModelIndex index = source->index(i, 0, parent); affectedGroups += deleteItems(index); } for (Group *g : qAsConst(affectedGroups)) { hideOrShowGroup(g, true); } } KateCompletionModel::Group *KateCompletionModel::fetchGroup(int attribute, const QString &scope, bool forceGrouping) { Q_UNUSED(forceGrouping); ///@todo use forceGrouping if (!hasGroups()) { return m_ungrouped; } int groupingAttribute = groupingAttributes(attribute); // qCDebug(LOG_KTE) << attribute << " " << groupingAttribute; if (m_groupHash.contains(groupingAttribute)) { if (groupingMethod() & Scope) { for (QHash::ConstIterator it = m_groupHash.constFind(groupingAttribute); it != m_groupHash.constEnd() && it.key() == groupingAttribute; ++it) if (it.value()->scope == scope) { return it.value(); } } else { return m_groupHash.value(groupingAttribute); } } QString st, at, it; QString title; if (groupingMethod() & ScopeType) { if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) { st = QStringLiteral("Global"); } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) { st = QStringLiteral("Namespace"); } else if (attribute & KTextEditor::CodeCompletionModel::LocalScope) { st = QStringLiteral("Local"); } title = st; } if (groupingMethod() & Scope) { if (!title.isEmpty()) { title.append(QLatin1Char(' ')); } title.append(scope); } if (groupingMethod() & AccessType) { if (attribute & KTextEditor::CodeCompletionModel::Public) { at = QStringLiteral("Public"); } else if (attribute & KTextEditor::CodeCompletionModel::Protected) { at = QStringLiteral("Protected"); } else if (attribute & KTextEditor::CodeCompletionModel::Private) { at = QStringLiteral("Private"); } if (accessIncludeStatic() && attribute & KTextEditor::CodeCompletionModel::Static) { at.append(QLatin1String(" Static")); } if (accessIncludeConst() && attribute & KTextEditor::CodeCompletionModel::Const) { at.append(QLatin1String(" Const")); } if (!at.isEmpty()) { if (!title.isEmpty()) { title.append(QLatin1String(", ")); } title.append(at); } } if (groupingMethod() & ItemType) { if (attribute & CodeCompletionModel::Namespace) { it = i18n("Namespaces"); } else if (attribute & CodeCompletionModel::Class) { it = i18n("Classes"); } else if (attribute & CodeCompletionModel::Struct) { it = i18n("Structs"); } else if (attribute & CodeCompletionModel::Union) { it = i18n("Unions"); } else if (attribute & CodeCompletionModel::Function) { it = i18n("Functions"); } else if (attribute & CodeCompletionModel::Variable) { it = i18n("Variables"); } else if (attribute & CodeCompletionModel::Enum) { it = i18n("Enumerations"); } if (!it.isEmpty()) { if (!title.isEmpty()) { title.append(QLatin1Char(' ')); } title.append(it); } } Group *ret = new Group(title, attribute, this); ret->scope = scope; m_emptyGroups.append(ret); m_groupHash.insert(groupingAttribute, ret); return ret; } bool KateCompletionModel::hasGroups() const { // qCDebug(LOG_KTE) << "m_groupHash.size()"<= m_rowTable.count()) { return m_ungrouped; } return m_rowTable[index.row()]; } /*QMap< int, QVariant > KateCompletionModel::itemData( const QModelIndex & index ) const { if (!hasGroups() || groupOfParent(index)) { QModelIndex index = mapToSource(index); if (index.isValid()) return index.model()->itemData(index); } return QAbstractItemModel::itemData(index); }*/ QModelIndex KateCompletionModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } if (Group *g = groupOfParent(index)) { if (!hasGroups()) { Q_ASSERT(g == m_ungrouped); return QModelIndex(); } int row = m_rowTable.indexOf(g); if (row == -1) { qCWarning(LOG_KTE) << "Couldn't find parent for index" << index; return QModelIndex(); } return createIndex(row, 0, quintptr(0)); } return QModelIndex(); } int KateCompletionModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { if (hasGroups()) { // qCDebug(LOG_KTE) << "Returning row count for toplevel " << m_rowTable.count(); return m_rowTable.count(); } else { // qCDebug(LOG_KTE) << "Returning ungrouped row count for toplevel " << m_ungrouped->filtered.count(); return m_ungrouped->filtered.count(); } } if (parent.column() > 0) { // only the first column has children return 0; } Group *g = groupForIndex(parent); // This is not an error, seems you don't have to check hasChildren() if (!g) { return 0; } // qCDebug(LOG_KTE) << "Returning row count for group " << g << " as " << g->filtered.count(); return g->filtered.count(); } void KateCompletionModel::sort(int column, Qt::SortOrder order) { Q_UNUSED(column) Q_UNUSED(order) } QModelIndex KateCompletionModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid()) { return QModelIndex(); } if (Group *g = groupOfParent(proxyIndex)) { if (proxyIndex.row() >= 0 && proxyIndex.row() < g->filtered.count()) { ModelRow source = g->filtered[proxyIndex.row()].sourceRow(); return source.second.sibling(source.second.row(), proxyIndex.column()); } else { qCDebug(LOG_KTE) << "Invalid proxy-index"; } } return QModelIndex(); } QModelIndex KateCompletionModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) { return QModelIndex(); } if (!hasGroups()) { return index(m_ungrouped->rowOf(modelRowPair(sourceIndex)), sourceIndex.column(), QModelIndex()); } for (Group *g : qAsConst(m_rowTable)) { int row = g->rowOf(modelRowPair(sourceIndex)); if (row != -1) { return index(row, sourceIndex.column(), indexForGroup(g)); } } // Copied from above for (Group *g : qAsConst(m_emptyGroups)) { int row = g->rowOf(modelRowPair(sourceIndex)); if (row != -1) { return index(row, sourceIndex.column(), indexForGroup(g)); } } return QModelIndex(); } void KateCompletionModel::setCurrentCompletion(KTextEditor::CodeCompletionModel *model, const QString &completion) { if (m_currentMatch[model] == completion) { return; } if (!hasCompletionModel()) { m_currentMatch[model] = completion; return; } changeTypes changeType = Change; if (m_currentMatch[model].length() > completion.length() && m_currentMatch[model].startsWith(completion, m_matchCaseSensitivity)) { // Filter has been broadened changeType = Broaden; } else if (m_currentMatch[model].length() < completion.length() && completion.startsWith(m_currentMatch[model], m_matchCaseSensitivity)) { // Filter has been narrowed changeType = Narrow; } // qCDebug(LOG_KTE) << model << "Old match: " << m_currentMatch[model] << ", new: " << completion << ", type: " << changeType; m_currentMatch[model] = completion; const bool resetModel = (changeType != Narrow); if (resetModel) { beginResetModel(); } if (!hasGroups()) { changeCompletions(m_ungrouped, changeType, !resetModel); } else { for (Group *g : qAsConst(m_rowTable)) { if (g != m_argumentHints) { changeCompletions(g, changeType, !resetModel); } } for (Group *g : qAsConst(m_emptyGroups)) { if (g != m_argumentHints) { changeCompletions(g, changeType, !resetModel); } } } // NOTE: best matches are also updated in resort resort(); if (resetModel) { endResetModel(); } clearExpanding(); // We need to do this, or be aware of expanding-widgets while filtering. emit layoutChanged(); } QString KateCompletionModel::commonPrefixInternal(const QString &forcePrefix) const { QString commonPrefix; // isNull() = true QList groups = m_rowTable; groups += m_ungrouped; for (Group *g : qAsConst(groups)) { for (const Item &item : qAsConst(g->filtered)) { uint startPos = m_currentMatch[item.sourceRow().first].length(); const QString candidate = item.name().mid(startPos); if (!candidate.startsWith(forcePrefix)) { continue; } if (commonPrefix.isNull()) { commonPrefix = candidate; // Replace QString::null prefix with QString(), so we won't initialize it again if (commonPrefix.isNull()) { commonPrefix = QString(); // isEmpty() = true, isNull() = false } } else { commonPrefix.truncate(candidate.length()); for (int a = 0; a < commonPrefix.length(); ++a) { if (commonPrefix[a] != candidate[a]) { commonPrefix.truncate(a); break; } } } } } return commonPrefix; } QString KateCompletionModel::commonPrefix(QModelIndex selectedIndex) const { QString commonPrefix = commonPrefixInternal(QString()); if (commonPrefix.isEmpty() && selectedIndex.isValid()) { Group *g = m_ungrouped; if (hasGroups()) { g = groupOfParent(selectedIndex); } if (g && selectedIndex.row() < g->filtered.size()) { // Follow the path of the selected item, finding the next non-empty common prefix Item item = g->filtered[selectedIndex.row()]; int matchLength = m_currentMatch[item.sourceRow().first].length(); commonPrefix = commonPrefixInternal(item.name().mid(matchLength).left(1)); } } return commonPrefix; } void KateCompletionModel::changeCompletions(Group *g, changeTypes changeType, bool notifyModel) { if (changeType != Narrow) { g->filtered = g->prefilter; // In the "Broaden" or "Change" case, just re-filter everything, // and don't notify the model. The model is notified afterwards through a reset(). } // This code determines what of the filtered items still fit, and computes the ranges that were removed, giving // them to beginRemoveRows(..) in batches QList newFiltered; int deleteUntil = -1; // In each state, the range [currentRow+1, deleteUntil] needs to be deleted for (int currentRow = g->filtered.count() - 1; currentRow >= 0; --currentRow) { if (g->filtered[currentRow].match()) { // This row does not need to be deleted, which means that currentRow+1 to deleteUntil need to be deleted now if (deleteUntil != -1 && notifyModel) { beginRemoveRows(indexForGroup(g), currentRow + 1, deleteUntil); endRemoveRows(); } deleteUntil = -1; newFiltered.prepend(g->filtered[currentRow]); } else { if (deleteUntil == -1) { deleteUntil = currentRow; // Mark that this row needs to be deleted } } } if (deleteUntil != -1 && notifyModel) { beginRemoveRows(indexForGroup(g), 0, deleteUntil); endRemoveRows(); } g->filtered = newFiltered; hideOrShowGroup(g, notifyModel); } int KateCompletionModel::Group::orderNumber() const { if (this == model->m_ungrouped) { return 700; } if (customSortingKey != -1) { return customSortingKey; } if (attribute & BestMatchesProperty) { return 1; } if (attribute & KTextEditor::CodeCompletionModel::LocalScope) { return 100; } else if (attribute & KTextEditor::CodeCompletionModel::Public) { return 200; } else if (attribute & KTextEditor::CodeCompletionModel::Protected) { return 300; } else if (attribute & KTextEditor::CodeCompletionModel::Private) { return 400; } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) { return 500; } else if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) { return 600; } return 700; } bool KateCompletionModel::Group::orderBefore(Group *other) const { return orderNumber() < other->orderNumber(); } void KateCompletionModel::hideOrShowGroup(Group *g, bool notifyModel) { if (g == m_argumentHints) { emit argumentHintsChanged(); m_updateBestMatchesTimer->start(200); // We have new argument-hints, so we have new best matches return; // Never show argument-hints in the normal completion-list } if (!g->isEmpty) { if (g->filtered.isEmpty()) { // Move to empty group list g->isEmpty = true; int row = m_rowTable.indexOf(g); if (row != -1) { if (hasGroups() && notifyModel) { beginRemoveRows(QModelIndex(), row, row); } m_rowTable.removeAt(row); if (hasGroups() && notifyModel) { endRemoveRows(); } m_emptyGroups.append(g); } else { qCWarning(LOG_KTE) << "Group " << g << " not found in row table!!"; } } } else { if (!g->filtered.isEmpty()) { // Move off empty group list g->isEmpty = false; int row = 0; // Find row where to insert for (int a = 0; a < m_rowTable.count(); a++) { if (g->orderBefore(m_rowTable[a])) { row = a; break; } row = a + 1; } if (notifyModel) { if (hasGroups()) { beginInsertRows(QModelIndex(), row, row); } else { beginInsertRows(QModelIndex(), 0, g->filtered.count()); } } m_rowTable.insert(row, g); if (notifyModel) { endInsertRows(); } m_emptyGroups.removeAll(g); } } } bool KateCompletionModel::indexIsItem(const QModelIndex &index) const { if (!hasGroups()) { return true; } if (groupOfParent(index)) { return true; } return false; } void KateCompletionModel::slotModelReset() { createGroups(); // debugStats(); } void KateCompletionModel::debugStats() { if (!hasGroups()) { qCDebug(LOG_KTE) << "Model groupless, " << m_ungrouped->filtered.count() << " items."; } else { qCDebug(LOG_KTE) << "Model grouped (" << m_rowTable.count() << " groups):"; for (Group *g : qAsConst(m_rowTable)) { qCDebug(LOG_KTE) << "Group" << g << "count" << g->filtered.count(); } } } bool KateCompletionModel::hasCompletionModel() const { return !m_completionModels.isEmpty(); } void KateCompletionModel::setFilteringEnabled(bool enable) { if (m_filteringEnabled != enable) { m_filteringEnabled = enable; } } void KateCompletionModel::setSortingEnabled(bool enable) { if (m_sortingEnabled != enable) { m_sortingEnabled = enable; beginResetModel(); resort(); endResetModel(); } } void KateCompletionModel::setGroupingEnabled(bool enable) { if (m_groupingEnabled != enable) { m_groupingEnabled = enable; } } void KateCompletionModel::setColumnMergingEnabled(bool enable) { if (m_columnMergingEnabled != enable) { m_columnMergingEnabled = enable; } } bool KateCompletionModel::isColumnMergingEnabled() const { return m_columnMergingEnabled; } bool KateCompletionModel::isGroupingEnabled() const { return m_groupingEnabled; } bool KateCompletionModel::isFilteringEnabled() const { return m_filteringEnabled; } bool KateCompletionModel::isSortingEnabled() const { return m_sortingEnabled; } QString KateCompletionModel::columnName(int column) { switch (column) { - case KTextEditor::CodeCompletionModel::Prefix: - return i18n("Prefix"); - case KTextEditor::CodeCompletionModel::Icon: - return i18n("Icon"); - case KTextEditor::CodeCompletionModel::Scope: - return i18n("Scope"); - case KTextEditor::CodeCompletionModel::Name: - return i18n("Name"); - case KTextEditor::CodeCompletionModel::Arguments: - return i18n("Arguments"); - case KTextEditor::CodeCompletionModel::Postfix: - return i18n("Postfix"); + case KTextEditor::CodeCompletionModel::Prefix: + return i18n("Prefix"); + case KTextEditor::CodeCompletionModel::Icon: + return i18n("Icon"); + case KTextEditor::CodeCompletionModel::Scope: + return i18n("Scope"); + case KTextEditor::CodeCompletionModel::Name: + return i18n("Name"); + case KTextEditor::CodeCompletionModel::Arguments: + return i18n("Arguments"); + case KTextEditor::CodeCompletionModel::Postfix: + return i18n("Postfix"); } return QString(); } const QList> &KateCompletionModel::columnMerges() const { return m_columnMerges; } void KateCompletionModel::setColumnMerges(const QList> &columnMerges) { beginResetModel(); m_columnMerges = columnMerges; endResetModel(); } int KateCompletionModel::translateColumn(int sourceColumn) const { if (m_columnMerges.isEmpty()) { return sourceColumn; } /* Debugging - dump column merge list QString columnMerge; for (const QList &list : m_columnMerges) { columnMerge += '['; for (int column : list) { columnMerge += QString::number(column) + QLatin1Char(' '); } columnMerge += "] "; } qCDebug(LOG_KTE) << k_funcinfo << columnMerge;*/ int c = 0; for (const QList &list : m_columnMerges) { for (int column : list) { if (column == sourceColumn) { return c; } } c++; } return -1; } int KateCompletionModel::groupingAttributes(int attribute) const { int ret = 0; if (m_groupingMethod & ScopeType) { if (countBits(attribute & ScopeTypeMask) > 1) { qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one scope type modifier provided."; } if (attribute & KTextEditor::CodeCompletionModel::GlobalScope) { ret |= KTextEditor::CodeCompletionModel::GlobalScope; } else if (attribute & KTextEditor::CodeCompletionModel::NamespaceScope) { ret |= KTextEditor::CodeCompletionModel::NamespaceScope; } else if (attribute & KTextEditor::CodeCompletionModel::LocalScope) { ret |= KTextEditor::CodeCompletionModel::LocalScope; } } if (m_groupingMethod & AccessType) { if (countBits(attribute & AccessTypeMask) > 1) { qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one access type modifier provided."; } if (attribute & KTextEditor::CodeCompletionModel::Public) { ret |= KTextEditor::CodeCompletionModel::Public; } else if (attribute & KTextEditor::CodeCompletionModel::Protected) { ret |= KTextEditor::CodeCompletionModel::Protected; } else if (attribute & KTextEditor::CodeCompletionModel::Private) { ret |= KTextEditor::CodeCompletionModel::Private; } if (accessIncludeStatic() && attribute & KTextEditor::CodeCompletionModel::Static) { ret |= KTextEditor::CodeCompletionModel::Static; } if (accessIncludeConst() && attribute & KTextEditor::CodeCompletionModel::Const) { ret |= KTextEditor::CodeCompletionModel::Const; } } if (m_groupingMethod & ItemType) { if (countBits(attribute & ItemTypeMask) > 1) { qCWarning(LOG_KTE) << "Invalid completion model metadata: more than one item type modifier provided."; } if (attribute & KTextEditor::CodeCompletionModel::Namespace) { ret |= KTextEditor::CodeCompletionModel::Namespace; } else if (attribute & KTextEditor::CodeCompletionModel::Class) { ret |= KTextEditor::CodeCompletionModel::Class; } else if (attribute & KTextEditor::CodeCompletionModel::Struct) { ret |= KTextEditor::CodeCompletionModel::Struct; } else if (attribute & KTextEditor::CodeCompletionModel::Union) { ret |= KTextEditor::CodeCompletionModel::Union; } else if (attribute & KTextEditor::CodeCompletionModel::Function) { ret |= KTextEditor::CodeCompletionModel::Function; } else if (attribute & KTextEditor::CodeCompletionModel::Variable) { ret |= KTextEditor::CodeCompletionModel::Variable; } else if (attribute & KTextEditor::CodeCompletionModel::Enum) { ret |= KTextEditor::CodeCompletionModel::Enum; } /* if (itemIncludeTemplate() && attribute & KTextEditor::CodeCompletionModel::Template) ret |= KTextEditor::CodeCompletionModel::Template;*/ } return ret; } void KateCompletionModel::setGroupingMethod(GroupingMethods m) { m_groupingMethod = m; createGroups(); } bool KateCompletionModel::accessIncludeConst() const { return m_accessConst; } void KateCompletionModel::setAccessIncludeConst(bool include) { if (m_accessConst != include) { m_accessConst = include; if (groupingMethod() & AccessType) { createGroups(); } } } bool KateCompletionModel::accessIncludeStatic() const { return m_accessStatic; } void KateCompletionModel::setAccessIncludeStatic(bool include) { if (m_accessStatic != include) { m_accessStatic = include; if (groupingMethod() & AccessType) { createGroups(); } } } bool KateCompletionModel::accessIncludeSignalSlot() const { return m_accesSignalSlot; } void KateCompletionModel::setAccessIncludeSignalSlot(bool include) { if (m_accesSignalSlot != include) { m_accesSignalSlot = include; if (groupingMethod() & AccessType) { createGroups(); } } } int KateCompletionModel::countBits(int value) const { int count = 0; for (int i = 1; i; i <<= 1) if (i & value) { count++; } return count; } KateCompletionModel::GroupingMethods KateCompletionModel::groupingMethod() const { return m_groupingMethod; } bool KateCompletionModel::isSortingByInheritanceDepth() const { return m_isSortingByInheritance; } void KateCompletionModel::setSortingByInheritanceDepth(bool byInheritance) { m_isSortingByInheritance = byInheritance; } bool KateCompletionModel::isSortingAlphabetical() const { return m_sortingAlphabetical; } Qt::CaseSensitivity KateCompletionModel::sortingCaseSensitivity() const { return m_sortingCaseSensitivity; } KateCompletionModel::Item::Item(bool doInitialMatch, KateCompletionModel *m, const HierarchicalModelHandler &handler, ModelRow sr) : model(m) , m_sourceRow(sr) , matchCompletion(StartsWithMatch) , matchFilters(true) , m_haveExactMatch(false) { inheritanceDepth = handler.getData(CodeCompletionModel::InheritanceDepth, m_sourceRow.second).toInt(); m_unimportant = handler.getData(CodeCompletionModel::UnimportantItemRole, m_sourceRow.second).toBool(); QModelIndex nameSibling = sr.second.sibling(sr.second.row(), CodeCompletionModel::Name); m_nameColumn = nameSibling.data(Qt::DisplayRole).toString(); if (doInitialMatch) { filter(); match(); } } bool KateCompletionModel::Item::operator<(const Item &rhs) const { int ret = 0; // qCDebug(LOG_KTE) << c1 << " c/w " << c2 << " -> " << (model->isSortingReverse() ? ret > 0 : ret < 0) << " (" << ret << ")"; if (m_unimportant && !rhs.m_unimportant) { return false; } if (!m_unimportant && rhs.m_unimportant) { return true; } if (matchCompletion < rhs.matchCompletion) { // enums are ordered in the order items should be displayed return true; } if (matchCompletion > rhs.matchCompletion) { return false; } if (ret == 0) { const QString &filter = rhs.model->currentCompletion(rhs.m_sourceRow.first); bool thisStartWithFilter = m_nameColumn.startsWith(filter, Qt::CaseSensitive); bool rhsStartsWithFilter = rhs.m_nameColumn.startsWith(filter, Qt::CaseSensitive); if (thisStartWithFilter && !rhsStartsWithFilter) { return true; } if (rhsStartsWithFilter && !thisStartWithFilter) { return false; } } if (model->isSortingByInheritanceDepth()) { ret = inheritanceDepth - rhs.inheritanceDepth; } if (ret == 0 && model->isSortingAlphabetical()) { // Do not use localeAwareCompare, because it is simply too slow for a list of about 1000 items ret = QString::compare(m_nameColumn, rhs.m_nameColumn, model->sortingCaseSensitivity()); } if (ret == 0) { // FIXME need to define a better default ordering for multiple model display ret = m_sourceRow.second.row() - rhs.m_sourceRow.second.row(); } return ret < 0; } void KateCompletionModel::Group::addItem(const Item &i, bool notifyModel) { if (isEmpty) { notifyModel = false; } QModelIndex groupIndex; if (notifyModel) { groupIndex = model->indexForGroup(this); } if (model->isSortingEnabled()) { prefilter.insert(std::upper_bound(prefilter.begin(), prefilter.end(), i), i); if (i.isVisible()) { QList::iterator it = std::upper_bound(filtered.begin(), filtered.end(), i); uint rowNumber = it - filtered.begin(); if (notifyModel) { model->beginInsertRows(groupIndex, rowNumber, rowNumber); } filtered.insert(it, i); } } else { if (notifyModel) { model->beginInsertRows(groupIndex, prefilter.size(), prefilter.size()); } if (i.isVisible()) { prefilter.append(i); } } if (notifyModel) { model->endInsertRows(); } } bool KateCompletionModel::Group::removeItem(const ModelRow &row) { for (int pi = 0; pi < prefilter.count(); ++pi) if (prefilter[pi].sourceRow() == row) { int index = rowOf(row); if (index != -1) { model->beginRemoveRows(model->indexForGroup(this), index, index); } filtered.removeAt(index); prefilter.removeAt(pi); if (index != -1) { model->endRemoveRows(); } return index != -1; } Q_ASSERT(false); return false; } KateCompletionModel::Group::Group(const QString &title, int attribute, KateCompletionModel *m) : model(m) , attribute(attribute) // ugly hack to add some left margin , title(QLatin1Char(' ') + title) , isEmpty(true) , customSortingKey(-1) { Q_ASSERT(model); } void KateCompletionModel::setSortingAlphabetical(bool alphabetical) { if (m_sortingAlphabetical != alphabetical) { m_sortingAlphabetical = alphabetical; beginResetModel(); resort(); endResetModel(); } } void KateCompletionModel::Group::resort() { std::stable_sort(filtered.begin(), filtered.end()); model->hideOrShowGroup(this); } void KateCompletionModel::setSortingCaseSensitivity(Qt::CaseSensitivity cs) { if (m_sortingCaseSensitivity != cs) { m_sortingCaseSensitivity = cs; beginResetModel(); resort(); endResetModel(); } } void KateCompletionModel::resort() { for (Group *g : qAsConst(m_rowTable)) { g->resort(); } for (Group *g : qAsConst(m_emptyGroups)) { g->resort(); } // call updateBestMatches here, so they are moved to the top again. updateBestMatches(); } bool KateCompletionModel::Item::isValid() const { return model && m_sourceRow.first && m_sourceRow.second.row() >= 0; } void KateCompletionModel::Group::clear() { prefilter.clear(); filtered.clear(); isEmpty = true; } bool KateCompletionModel::filterContextMatchesOnly() const { return m_filterContextMatchesOnly; } void KateCompletionModel::setFilterContextMatchesOnly(bool filter) { if (m_filterContextMatchesOnly != filter) { m_filterContextMatchesOnly = filter; refilter(); } } bool KateCompletionModel::filterByAttribute() const { return m_filterByAttribute; } void KateCompletionModel::setFilterByAttribute(bool filter) { if (m_filterByAttribute == filter) { m_filterByAttribute = filter; refilter(); } } KTextEditor::CodeCompletionModel::CompletionProperties KateCompletionModel::filterAttributes() const { return m_filterAttributes; } void KateCompletionModel::setFilterAttributes(KTextEditor::CodeCompletionModel::CompletionProperties attributes) { if (m_filterAttributes == attributes) { m_filterAttributes = attributes; refilter(); } } int KateCompletionModel::maximumInheritanceDepth() const { return m_maximumInheritanceDepth; } void KateCompletionModel::setMaximumInheritanceDepth(int maxDepth) { if (m_maximumInheritanceDepth != maxDepth) { m_maximumInheritanceDepth = maxDepth; refilter(); } } void KateCompletionModel::refilter() { beginResetModel(); m_ungrouped->refilter(); for (Group *g : qAsConst(m_rowTable)) { if (g != m_argumentHints) { g->refilter(); } } for (Group *g : qAsConst(m_emptyGroups)) { if (g != m_argumentHints) { g->refilter(); } } updateBestMatches(); clearExpanding(); // We need to do this, or be aware of expanding-widgets while filtering. endResetModel(); } void KateCompletionModel::Group::refilter() { filtered.clear(); for (const Item &i : qAsConst(prefilter)) { if (!i.isFiltered()) { filtered.append(i); } } } bool KateCompletionModel::Item::filter() { matchFilters = false; if (model->isFilteringEnabled()) { QModelIndex sourceIndex = m_sourceRow.second.sibling(m_sourceRow.second.row(), CodeCompletionModel::Name); if (model->filterContextMatchesOnly()) { QVariant contextMatch = sourceIndex.data(CodeCompletionModel::MatchQuality); if (contextMatch.canConvert(QVariant::Int) && !contextMatch.toInt()) { return false; } } if (model->filterByAttribute()) { int completionFlags = sourceIndex.data(CodeCompletionModel::CompletionRole).toInt(); if (model->filterAttributes() & completionFlags) { return false; } } if (model->maximumInheritanceDepth() > 0) { int inheritanceDepth = sourceIndex.data(CodeCompletionModel::InheritanceDepth).toInt(); if (inheritanceDepth > model->maximumInheritanceDepth()) { return false; } } } matchFilters = true; return matchFilters; } uint KateCompletionModel::filteredItemCount() const { uint ret = 0; for (Group *group : m_rowTable) { ret += group->filtered.size(); } return ret; } bool KateCompletionModel::shouldMatchHideCompletionList() const { // @todo Make this faster bool doHide = false; CodeCompletionModel *hideModel = nullptr; for (Group *group : qAsConst(m_rowTable)) { for (const Item &item : qAsConst(group->filtered)) { if (item.haveExactMatch()) { KTextEditor::CodeCompletionModelControllerInterface *iface3 = dynamic_cast(item.sourceRow().first); bool hide = false; if (!iface3) { hide = true; } if (iface3 && iface3->matchingItem(item.sourceRow().second) == KTextEditor::CodeCompletionModelControllerInterface::HideListIfAutomaticInvocation) { hide = true; } if (hide) { doHide = true; hideModel = item.sourceRow().first; } } } } if (doHide) { // Check if all other visible items are from the same model for (Group *group : qAsConst(m_rowTable)) { for (const Item &item : qAsConst(group->filtered)) { if (item.sourceRow().first != hideModel) { return false; } } } } return doHide; } static inline QChar toLowerIfInsensitive(QChar c, Qt::CaseSensitivity caseSensitive) { return (caseSensitive == Qt::CaseInsensitive) ? c.toLower() : c; } static inline bool matchesAbbreviationHelper(const QString &word, const QString &typed, const QVarLengthArray &offsets, Qt::CaseSensitivity caseSensitive, int &depth, int atWord = -1, int i = 0) { int atLetter = 1; for (; i < typed.size(); i++) { const QChar c = toLowerIfInsensitive(typed.at(i), caseSensitive); bool haveNextWord = offsets.size() > atWord + 1; bool canCompare = atWord != -1 && word.size() > offsets.at(atWord) + atLetter; if (canCompare && c == toLowerIfInsensitive(word.at(offsets.at(atWord) + atLetter), caseSensitive)) { // the typed letter matches a letter after the current word beginning if (!haveNextWord || c != toLowerIfInsensitive(word.at(offsets.at(atWord + 1)), caseSensitive)) { // good, simple case, no conflict atLetter += 1; continue; } // For maliciously crafted data, the code used here theoretically can have very high // complexity. Thus ensure we don't run into this case, by limiting the amount of branches // we walk through to 128. depth++; if (depth > 128) { return false; } // the letter matches both the next word beginning and the next character in the word if (haveNextWord && matchesAbbreviationHelper(word, typed, offsets, caseSensitive, depth, atWord + 1, i + 1)) { // resolving the conflict by taking the next word's first character worked, fine return true; } // otherwise, continue by taking the next letter in the current word. atLetter += 1; continue; } else if (haveNextWord && c == toLowerIfInsensitive(word.at(offsets.at(atWord + 1)), caseSensitive)) { // the typed letter matches the next word beginning atWord++; atLetter = 1; continue; } // no match return false; } // all characters of the typed word were matched return true; } bool KateCompletionModel::matchesAbbreviation(const QString &word, const QString &typed, Qt::CaseSensitivity caseSensitive) { // A mismatch is very likely for random even for the first letter, // thus this optimization makes sense. if (toLowerIfInsensitive(word.at(0), caseSensitive) != toLowerIfInsensitive(typed.at(0), caseSensitive)) { return false; } // First, check if all letters are contained in the word in the right order. int atLetter = 0; for (const QChar c : typed) { while (toLowerIfInsensitive(c, caseSensitive) != toLowerIfInsensitive(word.at(atLetter), caseSensitive)) { atLetter += 1; if (atLetter >= word.size()) { return false; } } } bool haveUnderscore = true; QVarLengthArray offsets; // We want to make "KComplM" match "KateCompletionModel"; this means we need // to allow parts of the typed text to be not part of the actual abbreviation, // which consists only of the uppercased / underscored letters (so "KCM" in this case). // However it might be ambiguous whether a letter is part of such a word or part of // the following abbreviation, so we need to find all possible word offsets first, // then compare. for (int i = 0; i < word.size(); i++) { const QChar c = word.at(i); if (c == QLatin1Char('_')) { haveUnderscore = true; } else if (haveUnderscore || c.isUpper()) { offsets.append(i); haveUnderscore = false; } } int depth = 0; return matchesAbbreviationHelper(word, typed, offsets, caseSensitive, depth); } static inline bool containsAtWordBeginning(const QString &word, const QString &typed, Qt::CaseSensitivity caseSensitive) { for (int i = 1; i < word.size(); i++) { // The current position is a word beginning if the previous character was an underscore // or if the current character is uppercase. Subsequent uppercase characters do not count, // to handle the special case of UPPER_CASE_VARS properly. const QChar c = word.at(i); const QChar prev = word.at(i - 1); if (!(prev == QLatin1Char('_') || (c.isUpper() && !prev.isUpper()))) { continue; } if (word.midRef(i).startsWith(typed, caseSensitive)) { return true; } } return false; } KateCompletionModel::Item::MatchType KateCompletionModel::Item::match() { QString match = model->currentCompletion(m_sourceRow.first); m_haveExactMatch = false; // Hehe, everything matches nothing! (ie. everything matches a blank string) if (match.isEmpty()) { return PerfectMatch; } if (m_nameColumn.isEmpty()) { return NoMatch; } matchCompletion = (m_nameColumn.startsWith(match, model->matchCaseSensitivity()) ? StartsWithMatch : NoMatch); if (matchCompletion == NoMatch) { // if no match, try for "contains" // Only match when the occurrence is at a "word" beginning, marked by // an underscore or a capital. So Foo matches BarFoo and Bar_Foo, but not barfoo. // Starting at 1 saves looking at the beginning of the word, that was already checked above. if (containsAtWordBeginning(m_nameColumn, match, model->matchCaseSensitivity())) { matchCompletion = ContainsMatch; } } if (matchCompletion == NoMatch && !m_nameColumn.isEmpty() && !match.isEmpty()) { // if still no match, try abbreviation matching if (matchesAbbreviation(m_nameColumn, match, model->matchCaseSensitivity())) { matchCompletion = AbbreviationMatch; } } if (matchCompletion && match.length() == m_nameColumn.length()) { if (model->matchCaseSensitivity() == Qt::CaseInsensitive && model->exactMatchCaseSensitivity() == Qt::CaseSensitive && !m_nameColumn.startsWith(match, Qt::CaseSensitive)) { return matchCompletion; } matchCompletion = PerfectMatch; m_haveExactMatch = true; } return matchCompletion; } QString KateCompletionModel::propertyName(KTextEditor::CodeCompletionModel::CompletionProperty property) { switch (property) { - case CodeCompletionModel::Public: - return i18n("Public"); + case CodeCompletionModel::Public: + return i18n("Public"); - case CodeCompletionModel::Protected: - return i18n("Protected"); + case CodeCompletionModel::Protected: + return i18n("Protected"); - case CodeCompletionModel::Private: - return i18n("Private"); + case CodeCompletionModel::Private: + return i18n("Private"); - case CodeCompletionModel::Static: - return i18n("Static"); + case CodeCompletionModel::Static: + return i18n("Static"); - case CodeCompletionModel::Const: - return i18n("Constant"); + case CodeCompletionModel::Const: + return i18n("Constant"); - case CodeCompletionModel::Namespace: - return i18n("Namespace"); + case CodeCompletionModel::Namespace: + return i18n("Namespace"); - case CodeCompletionModel::Class: - return i18n("Class"); + case CodeCompletionModel::Class: + return i18n("Class"); - case CodeCompletionModel::Struct: - return i18n("Struct"); + case CodeCompletionModel::Struct: + return i18n("Struct"); - case CodeCompletionModel::Union: - return i18n("Union"); + case CodeCompletionModel::Union: + return i18n("Union"); - case CodeCompletionModel::Function: - return i18n("Function"); + case CodeCompletionModel::Function: + return i18n("Function"); - case CodeCompletionModel::Variable: - return i18n("Variable"); + case CodeCompletionModel::Variable: + return i18n("Variable"); - case CodeCompletionModel::Enum: - return i18n("Enumeration"); + case CodeCompletionModel::Enum: + return i18n("Enumeration"); - case CodeCompletionModel::Template: - return i18n("Template"); + case CodeCompletionModel::Template: + return i18n("Template"); - case CodeCompletionModel::Virtual: - return i18n("Virtual"); + case CodeCompletionModel::Virtual: + return i18n("Virtual"); - case CodeCompletionModel::Override: - return i18n("Override"); + case CodeCompletionModel::Override: + return i18n("Override"); - case CodeCompletionModel::Inline: - return i18n("Inline"); + case CodeCompletionModel::Inline: + return i18n("Inline"); - case CodeCompletionModel::Friend: - return i18n("Friend"); + case CodeCompletionModel::Friend: + return i18n("Friend"); - case CodeCompletionModel::Signal: - return i18n("Signal"); + case CodeCompletionModel::Signal: + return i18n("Signal"); - case CodeCompletionModel::Slot: - return i18n("Slot"); + case CodeCompletionModel::Slot: + return i18n("Slot"); - case CodeCompletionModel::LocalScope: - return i18n("Local Scope"); + case CodeCompletionModel::LocalScope: + return i18n("Local Scope"); - case CodeCompletionModel::NamespaceScope: - return i18n("Namespace Scope"); + case CodeCompletionModel::NamespaceScope: + return i18n("Namespace Scope"); - case CodeCompletionModel::GlobalScope: - return i18n("Global Scope"); + case CodeCompletionModel::GlobalScope: + return i18n("Global Scope"); - default: - return i18n("Unknown Property"); + default: + return i18n("Unknown Property"); } } bool KateCompletionModel::Item::isVisible() const { return matchCompletion && matchFilters; } bool KateCompletionModel::Item::isFiltered() const { return !matchFilters; } bool KateCompletionModel::Item::isMatching() const { return matchFilters; } const KateCompletionModel::ModelRow &KateCompletionModel::Item::sourceRow() const { return m_sourceRow; } QString KateCompletionModel::currentCompletion(KTextEditor::CodeCompletionModel *model) const { return m_currentMatch.value(model); } Qt::CaseSensitivity KateCompletionModel::matchCaseSensitivity() const { return m_matchCaseSensitivity; } Qt::CaseSensitivity KateCompletionModel::exactMatchCaseSensitivity() const { return m_exactMatchCaseSensitivity; } void KateCompletionModel::addCompletionModel(KTextEditor::CodeCompletionModel *model) { if (m_completionModels.contains(model)) { return; } m_completionModels.append(model); connect(model, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(slotRowsInserted(QModelIndex, int, int))); connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(slotRowsRemoved(QModelIndex, int, int))); connect(model, SIGNAL(modelReset()), SLOT(slotModelReset())); // This performs the reset createGroups(); } void KateCompletionModel::setCompletionModel(KTextEditor::CodeCompletionModel *model) { clearCompletionModels(); addCompletionModel(model); } void KateCompletionModel::setCompletionModels(const QList &models) { // if (m_completionModels == models) // return; clearCompletionModels(); m_completionModels = models; for (KTextEditor::CodeCompletionModel *model : models) { connect(model, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(slotRowsInserted(QModelIndex, int, int))); connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(slotRowsRemoved(QModelIndex, int, int))); connect(model, SIGNAL(modelReset()), SLOT(slotModelReset())); } // This performs the reset createGroups(); } QList KateCompletionModel::completionModels() const { return m_completionModels; } void KateCompletionModel::removeCompletionModel(CodeCompletionModel *model) { if (!model || !m_completionModels.contains(model)) { return; } beginResetModel(); m_currentMatch.remove(model); clearGroups(); model->disconnect(this); m_completionModels.removeAll(model); endResetModel(); if (!m_completionModels.isEmpty()) { // This performs the reset createGroups(); } } void KateCompletionModel::makeGroupItemsUnique(bool onlyFiltered) { struct FilterItems { FilterItems(KateCompletionModel &model, const QVector &needShadowing) : m_model(model) , m_needShadowing(needShadowing) { } QHash had; KateCompletionModel &m_model; const QVector m_needShadowing; void filter(QList &items) { QList temp; for (const Item &item : qAsConst(items)) { QHash::const_iterator it = had.constFind(item.name()); if (it != had.constEnd() && *it != item.sourceRow().first && m_needShadowing.contains(item.sourceRow().first)) { continue; } had.insert(item.name(), item.sourceRow().first); temp.push_back(item); } items = temp; } void filter(Group *group, bool onlyFiltered) { if (group->prefilter.size() == group->filtered.size()) { // Filter only once filter(group->filtered); if (!onlyFiltered) { group->prefilter = group->filtered; } } else { // Must filter twice filter(group->filtered); if (!onlyFiltered) { filter(group->prefilter); } } if (group->filtered.isEmpty()) { m_model.hideOrShowGroup(group); } } }; QVector needShadowing; for (KTextEditor::CodeCompletionModel *model : qAsConst(m_completionModels)) { KTextEditor::CodeCompletionModelControllerInterface *v4 = dynamic_cast(model); if (v4 && v4->shouldHideItemsWithEqualNames()) { needShadowing.push_back(model); } } if (needShadowing.isEmpty()) { return; } FilterItems filter(*this, needShadowing); filter.filter(m_ungrouped, onlyFiltered); for (Group *group : qAsConst(m_rowTable)) { filter.filter(group, onlyFiltered); } } // Updates the best-matches group void KateCompletionModel::updateBestMatches() { int maxMatches = 300; // We cannot do too many operations here, because they are all executed whenever a character is added. Would be nice if we could split the operations up somewhat using a timer. m_updateBestMatchesTimer->stop(); // Maps match-qualities to ModelRows paired together with the BestMatchesCount returned by the items. typedef QMultiMap> BestMatchMap; BestMatchMap matches; if (!hasGroups()) { // If there is no grouping, just change the order of the items, moving the best matching ones to the front QMultiMap rowsForQuality; int row = 0; for (const Item &item : qAsConst(m_ungrouped->filtered)) { ModelRow source = item.sourceRow(); QVariant v = source.second.data(CodeCompletionModel::BestMatchesCount); if (v.type() == QVariant::Int && v.toInt() > 0) { int quality = contextMatchQuality(source); if (quality > 0) { rowsForQuality.insert(quality, row); } } ++row; --maxMatches; if (maxMatches < 0) { break; } } if (!rowsForQuality.isEmpty()) { // Rewrite m_ungrouped->filtered in a new order QSet movedToFront; QList newFiltered; for (QMultiMap::const_iterator it = rowsForQuality.constBegin(); it != rowsForQuality.constEnd(); ++it) { newFiltered.prepend(m_ungrouped->filtered[it.value()]); movedToFront.insert(it.value()); } { int size = m_ungrouped->filtered.size(); for (int a = 0; a < size; ++a) if (!movedToFront.contains(a)) { newFiltered.append(m_ungrouped->filtered[a]); } } m_ungrouped->filtered = newFiltered; } return; } ///@todo Cache the CodeCompletionModel::BestMatchesCount for (Group *g : qAsConst(m_rowTable)) { if (g == m_bestMatches) { continue; } for (int a = 0; a < g->filtered.size(); a++) { ModelRow source = g->filtered[a].sourceRow(); QVariant v = source.second.data(CodeCompletionModel::BestMatchesCount); if (v.type() == QVariant::Int && v.toInt() > 0) { // Return the best match with any of the argument-hints int quality = contextMatchQuality(source); if (quality > 0) { matches.insert(quality, qMakePair(v.toInt(), g->filtered[a].sourceRow())); } --maxMatches; } if (maxMatches < 0) { break; } } if (maxMatches < 0) { break; } } // Now choose how many of the matches will be taken. This is done with the rule: // The count of shown best-matches should equal the average count of their BestMatchesCounts int cnt = 0; int matchesSum = 0; BestMatchMap::const_iterator it = matches.constEnd(); while (it != matches.constBegin()) { --it; ++cnt; matchesSum += (*it).first; if (cnt > matchesSum / cnt) { break; } } m_bestMatches->filtered.clear(); it = matches.constEnd(); while (it != matches.constBegin() && cnt > 0) { --it; --cnt; m_bestMatches->filtered.append(Item(true, this, HierarchicalModelHandler((*it).second.first), (*it).second)); } hideOrShowGroup(m_bestMatches); } void KateCompletionModel::rowSelected(const QModelIndex &row) { ExpandingWidgetModel::rowSelected(row); ///@todo delay this int rc = widget()->argumentHintModel()->rowCount(QModelIndex()); if (rc == 0) { return; } // For now, simply update the whole column 0 QModelIndex start = widget()->argumentHintModel()->index(0, 0); QModelIndex end = widget()->argumentHintModel()->index(rc - 1, 0); widget()->argumentHintModel()->emitDataChanged(start, end); } void KateCompletionModel::clearCompletionModels() { if (m_completionModels.isEmpty()) { return; } beginResetModel(); for (CodeCompletionModel *model : qAsConst(m_completionModels)) { model->disconnect(this); } m_completionModels.clear(); m_currentMatch.clear(); clearGroups(); endResetModel(); } diff --git a/src/completion/katekeywordcompletion.cpp b/src/completion/katekeywordcompletion.cpp index 612d52cb..ae211a2f 100644 --- a/src/completion/katekeywordcompletion.cpp +++ b/src/completion/katekeywordcompletion.cpp @@ -1,167 +1,167 @@ /* This file is part of the KDE libraries Copyright (C) 2014 Sven Brauch This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, or any later version, as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katekeywordcompletion.h" #include "katedocument.h" #include "katehighlight.h" #include "katetextline.h" #include #include #include KateKeywordCompletionModel::KateKeywordCompletionModel(QObject *parent) : CodeCompletionModel(parent) { setHasGroups(false); } void KateKeywordCompletionModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, KTextEditor::CodeCompletionModel::InvocationType /*invocationType*/) { KTextEditor::DocumentPrivate *doc = static_cast(view->document()); if (!doc->highlight() || doc->highlight()->noHighlighting()) { return; } m_items = doc->highlight()->keywordsForLocation(doc, range.end()); std::sort(m_items.begin(), m_items.end()); } QModelIndex KateKeywordCompletionModel::parent(const QModelIndex &index) const { if (index.internalId()) return createIndex(0, 0); else return QModelIndex(); } QModelIndex KateKeywordCompletionModel::index(int row, int column, const QModelIndex &parent) const { if (!parent.isValid()) { if (row == 0) return createIndex(row, column); else return QModelIndex(); } else if (parent.parent().isValid()) { return QModelIndex(); } if (row < 0 || row >= m_items.count() || column < 0 || column >= ColumnCount) { return QModelIndex(); } return createIndex(row, column, 1); } int KateKeywordCompletionModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid() && !m_items.isEmpty()) return 1; // One root node to define the custom group else if (parent.parent().isValid()) return 0; // Completion-items have no children else return m_items.count(); } static bool isInWord(const KTextEditor::View *view, const KTextEditor::Cursor &position, QChar c) { KTextEditor::DocumentPrivate *document = static_cast(view->document()); KateHighlighting *highlight = document->highlight(); Kate::TextLine line = document->kateTextLine(position.line()); return highlight->isInWord(c, line->attribute(position.column() - 1)); } KTextEditor::Range KateKeywordCompletionModel::completionRange(KTextEditor::View *view, const KTextEditor::Cursor &position) { const QString &text = view->document()->text(KTextEditor::Range(position, KTextEditor::Cursor(position.line(), 0))); int pos; for (pos = text.size() - 1; pos >= 0; pos--) { if (isInWord(view, position, text.at(pos))) { // This needs to be aware of what characters are word-characters in the // active language, so that languages which prefix commands with e.g. @ // or \ have properly working completion. continue; } break; } return KTextEditor::Range(KTextEditor::Cursor(position.line(), pos + 1), position); } bool KateKeywordCompletionModel::shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString ¤tCompletion) { if (view->cursorPosition() < range.start() || view->cursorPosition() > range.end()) return true; // Always abort when the completion-range has been left // Do not abort completions when the text has been empty already before and a newline has been entered for (QChar c : currentCompletion) { if (!isInWord(view, range.start(), c)) { return true; } } return false; } bool KateKeywordCompletionModel::shouldStartCompletion(KTextEditor::View * /*view*/, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor & /*position*/) { if (userInsertion && insertedText.size() > 3 && !insertedText.contains(QLatin1Char(' ')) && insertedText.at(insertedText.size() - 1).isLetter()) { return true; } return false; } bool KateKeywordCompletionModel::shouldHideItemsWithEqualNames() const { return true; } QVariant KateKeywordCompletionModel::data(const QModelIndex &index, int role) const { if (role == UnimportantItemRole) return QVariant(true); if (role == InheritanceDepth) return 9000; if (!index.parent().isValid()) { // group header switch (role) { - case Qt::DisplayRole: - return i18n("Language keywords"); - case GroupRole: - return Qt::DisplayRole; + case Qt::DisplayRole: + return i18n("Language keywords"); + case GroupRole: + return Qt::DisplayRole; } } if (index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole) return m_items.at(index.row()); if (index.column() == KTextEditor::CodeCompletionModel::Icon && role == Qt::DecorationRole) { static const QIcon icon(QIcon::fromTheme(QStringLiteral("code-variable")).pixmap(QSize(16, 16))); return icon; } return QVariant(); } KTextEditor::CodeCompletionModelControllerInterface::MatchReaction KateKeywordCompletionModel::matchingItem(const QModelIndex & /*matched*/) { return KTextEditor::CodeCompletionModelControllerInterface::HideListIfAutomaticInvocation; } // kate: indent-width 4; replace-tabs on diff --git a/src/completion/katewordcompletion.cpp b/src/completion/katewordcompletion.cpp index ab070656..65715e27 100644 --- a/src/completion/katewordcompletion.cpp +++ b/src/completion/katewordcompletion.cpp @@ -1,577 +1,577 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2003 Anders Lund * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // BEGIN includes #include "katewordcompletion.h" #include "kateconfig.h" #include "katedefaultcolors.h" #include "katedocument.h" #include "kateglobal.h" #include "katehighlight.h" #include "katepartdebug.h" #include "kateview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // END /// Amount of characters the document may have to enable automatic invocation (1MB) static const int autoInvocationMaxFilesize = 1000000; // BEGIN KateWordCompletionModel KateWordCompletionModel::KateWordCompletionModel(QObject *parent) : CodeCompletionModel(parent) , m_automatic(false) { setHasGroups(false); } KateWordCompletionModel::~KateWordCompletionModel() { } void KateWordCompletionModel::saveMatches(KTextEditor::View *view, const KTextEditor::Range &range) { m_matches = allMatches(view, range); m_matches.sort(); } QVariant KateWordCompletionModel::data(const QModelIndex &index, int role) const { if (role == UnimportantItemRole) { return QVariant(true); } if (role == InheritanceDepth) { return 10000; } if (!index.parent().isValid()) { // It is the group header switch (role) { - case Qt::DisplayRole: - return i18n("Auto Word Completion"); - case GroupRole: - return Qt::DisplayRole; + case Qt::DisplayRole: + return i18n("Auto Word Completion"); + case GroupRole: + return Qt::DisplayRole; } } if (index.column() == KTextEditor::CodeCompletionModel::Name && role == Qt::DisplayRole) { return m_matches.at(index.row()); } if (index.column() == KTextEditor::CodeCompletionModel::Icon && role == Qt::DecorationRole) { static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text")).pixmap(QSize(16, 16))); return icon; } return QVariant(); } QModelIndex KateWordCompletionModel::parent(const QModelIndex &index) const { if (index.internalId()) { return createIndex(0, 0, quintptr(0)); } else { return QModelIndex(); } } QModelIndex KateWordCompletionModel::index(int row, int column, const QModelIndex &parent) const { if (!parent.isValid()) { if (row == 0) { return createIndex(row, column, quintptr(0)); } else { return QModelIndex(); } } else if (parent.parent().isValid()) { return QModelIndex(); } if (row < 0 || row >= m_matches.count() || column < 0 || column >= ColumnCount) { return QModelIndex(); } return createIndex(row, column, 1); } int KateWordCompletionModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid() && !m_matches.isEmpty()) { return 1; // One root node to define the custom group } else if (parent.parent().isValid()) { return 0; // Completion-items have no children } else { return m_matches.count(); } } bool KateWordCompletionModel::shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) { if (!userInsertion) { return false; } if (insertedText.isEmpty()) { return false; } KTextEditor::ViewPrivate *v = qobject_cast(view); if (view->document()->totalCharacters() > autoInvocationMaxFilesize) { // Disable automatic invocation for files larger than 1MB (see benchmarks) return false; } const QString &text = view->document()->line(position.line()).left(position.column()); const uint check = v->config()->wordCompletionMinimalWordLength(); // Start completion immediately if min. word size is zero if (!check) { return true; } // Otherwise, check if user has typed long enough text... const int start = text.length(); const int end = start - check; if (end < 0) { return false; } for (int i = start - 1; i >= end; i--) { const QChar c = text.at(i); if (!(c.isLetter() || (c.isNumber()) || c == QLatin1Char('_'))) { return false; } } return true; } bool KateWordCompletionModel::shouldAbortCompletion(KTextEditor::View *view, const KTextEditor::Range &range, const QString ¤tCompletion) { if (m_automatic) { KTextEditor::ViewPrivate *v = qobject_cast(view); if (currentCompletion.length() < v->config()->wordCompletionMinimalWordLength()) { return true; } } return CodeCompletionModelControllerInterface::shouldAbortCompletion(view, range, currentCompletion); } void KateWordCompletionModel::completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) { m_automatic = it == AutomaticInvocation; saveMatches(view, range); } /** * Scan throughout the entire document for possible completions, * ignoring any dublets and words shorter than configured and/or * reasonable minimum length. */ QStringList KateWordCompletionModel::allMatches(KTextEditor::View *view, const KTextEditor::Range &range) const { QSet result; const int minWordSize = qMax(2, qobject_cast(view)->config()->wordCompletionMinimalWordLength()); const int lines = view->document()->lines(); KateHighlighting *h = qobject_cast(view)->doc()->highlight(); for (int line = 0; line < lines; line++) { const QString &text = view->document()->line(line); int wordBegin = 0; int offset = 0; const int end = text.size(); const bool cursorLine = view->cursorPosition().line() == line; while (offset < end) { const QChar c = text.at(offset); // increment offset when at line end, so we take the last character too if (!h->isInWord(c) || (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! */ if (!cursorLine || (view->cursorPosition().column() < wordBegin || view->cursorPosition().column() > offset)) { result.insert(text.mid(wordBegin, offset - wordBegin)); } } wordBegin = offset + 1; } if (c.isSpace()) { wordBegin = offset + 1; } offset += 1; } } return result.values(); } void KateWordCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const { KTextEditor::ViewPrivate *v = qobject_cast(view); KateHighlighting *h = v->doc()->highlight(); 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) { if (!h->isInWord(line[i])) { tailEnd = i; } } int sizeDiff = m_matches.at(index.row()).size() - (word.end().column() - word.start().column()); tailStart += sizeDiff; tailEnd += sizeDiff; 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()); } else { view->document()->replaceText(word, m_matches.at(index.row())); } } KTextEditor::CodeCompletionModelControllerInterface::MatchReaction KateWordCompletionModel::matchingItem(const QModelIndex & /*matched*/) { return HideListIfAutomaticInvocation; } bool KateWordCompletionModel::shouldHideItemsWithEqualNames() const { // We don't want word-completion items if the same items // are available through more sophisticated completion models return true; } // Return the range containing the word left of the cursor KTextEditor::Range KateWordCompletionModel::completionRange(KTextEditor::View *view, const KTextEditor::Cursor &position) { int line = position.line(); int col = position.column(); KTextEditor::Document *doc = view->document(); KateHighlighting *h = qobject_cast(view)->doc()->highlight(); while (col > 0) { const QChar c = (doc->characterAt(KTextEditor::Cursor(line, col - 1))); if (h->isInWord(c)) { col--; continue; } break; } return KTextEditor::Range(KTextEditor::Cursor(line, col), position); } // END KateWordCompletionModel // BEGIN KateWordCompletionView struct KateWordCompletionViewPrivate { KTextEditor::MovingRange *liRange; // range containing last inserted text KTextEditor::Range dcRange; // current range to be completed by directional completion KTextEditor::Cursor dcCursor; // directional completion search cursor QRegularExpression wordRegEx; int directionalPos; // be able to insert "" at the correct time bool isCompleting; // true when the directional completion is doing a completion }; KateWordCompletionView::KateWordCompletionView(KTextEditor::View *view, KActionCollection *ac) : QObject(view) , m_view(view) , m_dWCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()) , d(new KateWordCompletionViewPrivate) { d->isCompleting = false; d->dcRange = KTextEditor::Range::invalid(); d->liRange = static_cast(m_view->document())->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand); const KColorScheme &colors(KTextEditor::EditorPrivate::self()->defaultColors().view()); KTextEditor::Attribute::Ptr a = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); a->setBackground(colors.background(KColorScheme::ActiveBackground)); a->setForeground(colors.foreground(KColorScheme::ActiveText)); // ### this does 0 d->liRange->setAttribute(a); QAction *action; if (qobject_cast(view)) { action = new QAction(i18n("Shell Completion"), this); ac->addAction(QStringLiteral("doccomplete_sh"), action); connect(action, SIGNAL(triggered()), this, SLOT(shellComplete())); } action = new QAction(i18n("Reuse Word Above"), this); ac->addAction(QStringLiteral("doccomplete_bw"), action); ac->setDefaultShortcut(action, Qt::CTRL + Qt::Key_8); connect(action, SIGNAL(triggered()), this, SLOT(completeBackwards())); action = new QAction(i18n("Reuse Word Below"), this); ac->addAction(QStringLiteral("doccomplete_fw"), action); ac->setDefaultShortcut(action, Qt::CTRL + Qt::Key_9); connect(action, SIGNAL(triggered()), this, SLOT(completeForwards())); } KateWordCompletionView::~KateWordCompletionView() { delete d; } void KateWordCompletionView::completeBackwards() { complete(false); } void KateWordCompletionView::completeForwards() { complete(); } // Pop up the editors completion list if applicable void KateWordCompletionView::popupCompletionList() { qCDebug(LOG_KTE) << "entered ..."; KTextEditor::Range r = range(); KTextEditor::CodeCompletionInterface *cci = qobject_cast(m_view); if (!cci || cci->isCompletionActive()) { return; } m_dWCompletionModel->saveMatches(m_view, r); qCDebug(LOG_KTE) << "after save matches ..."; if (!m_dWCompletionModel->rowCount(QModelIndex())) { return; } cci->startCompletion(r, m_dWCompletionModel); } // Contributed by void KateWordCompletionView::shellComplete() { KTextEditor::Range r = range(); QStringList matches = m_dWCompletionModel->allMatches(m_view, r); if (matches.size() == 0) { return; } QString partial = findLongestUnique(matches, r.columnWidth()); if (partial.isEmpty()) { popupCompletionList(); } else { m_view->document()->insertText(r.end(), partial.mid(r.columnWidth())); d->liRange->setView(m_view); d->liRange->setRange(KTextEditor::Range(r.end(), partial.length() - r.columnWidth())); connect(m_view, SIGNAL(cursorPositionChanged(KTextEditor::View *, KTextEditor::Cursor)), this, SLOT(slotCursorMoved())); } } // Do one completion, searching in the desired direction, // if possible void KateWordCompletionView::complete(bool fw) { KTextEditor::Range r = range(); int inc = fw ? 1 : -1; KTextEditor::Document *doc = m_view->document(); if (d->dcRange.isValid()) { // qCDebug(LOG_KTE)<<"CONTINUE "<dcRange; // this is a repeated activation // if we are back to where we started, reset. if ((fw && d->directionalPos == -1) || (!fw && d->directionalPos == 1)) { const int spansColumns = d->liRange->end().column() - d->liRange->start().column(); if (spansColumns > 0) { doc->removeText(*d->liRange); } d->liRange->setRange(KTextEditor::Range::invalid()); d->dcCursor = r.end(); d->directionalPos = 0; return; } if (fw) { const int spansColumns = d->liRange->end().column() - d->liRange->start().column(); d->dcCursor.setColumn(d->dcCursor.column() + spansColumns); } d->directionalPos += inc; } else { // new completion, reset all // qCDebug(LOG_KTE)<<"RESET FOR NEW"; d->dcRange = r; d->liRange->setRange(KTextEditor::Range::invalid()); d->dcCursor = r.start(); d->directionalPos = inc; d->liRange->setView(m_view); connect(m_view, SIGNAL(cursorPositionChanged(KTextEditor::View *, KTextEditor::Cursor)), this, SLOT(slotCursorMoved())); } d->wordRegEx.setPattern(QLatin1String("\\b") + doc->text(d->dcRange) + QLatin1String("(\\w+)")); int pos(0); QString ln = doc->line(d->dcCursor.line()); while (true) { // qCDebug(LOG_KTE)<<"SEARCHING FOR "<wordRegEx.pattern()<<" "<dcCursor; QRegularExpressionMatch match; pos = fw ? ln.indexOf(d->wordRegEx, d->dcCursor.column(), &match) : ln.lastIndexOf(d->wordRegEx, d->dcCursor.column(), &match); if (match.hasMatch()) { // we matched a word // qCDebug(LOG_KTE)<<"USABLE MATCH"; const QStringRef m = match.capturedRef(1); if (m != doc->text(*d->liRange) && (d->dcCursor.line() != d->dcRange.start().line() || pos != d->dcRange.start().column())) { // we got good a match! replace text and return. d->isCompleting = true; KTextEditor::Range replaceRange(d->liRange->toRange()); if (!replaceRange.isValid()) { replaceRange.setRange(r.end(), r.end()); } doc->replaceText(replaceRange, m.toString()); d->liRange->setRange(KTextEditor::Range(d->dcRange.end(), m.length())); d->dcCursor.setColumn(pos); // for next try d->isCompleting = false; return; } // equal to last one, continue else { // qCDebug(LOG_KTE)<<"SKIPPING, EQUAL MATCH"; d->dcCursor.setColumn(pos); // for next try if (fw) { d->dcCursor.setColumn(pos + m.length()); } else { if (pos == 0) { if (d->dcCursor.line() > 0) { int l = d->dcCursor.line() + inc; ln = doc->line(l); d->dcCursor.setPosition(l, ln.length()); } else { return; } } else { d->dcCursor.setColumn(d->dcCursor.column() - 1); } } } } else { // no match // qCDebug(LOG_KTE)<<"NO MATCH"; if ((!fw && d->dcCursor.line() == 0) || (fw && d->dcCursor.line() >= doc->lines())) { return; } int l = d->dcCursor.line() + inc; ln = doc->line(l); d->dcCursor.setPosition(l, fw ? 0 : ln.length()); } } // while true } void KateWordCompletionView::slotCursorMoved() { if (d->isCompleting) { return; } d->dcRange = KTextEditor::Range::invalid(); disconnect(m_view, SIGNAL(cursorPositionChanged(KTextEditor::View *, KTextEditor::Cursor)), this, SLOT(slotCursorMoved())); d->liRange->setView(nullptr); d->liRange->setRange(KTextEditor::Range::invalid()); } // Contributed by FIXME QString KateWordCompletionView::findLongestUnique(const QStringList &matches, int lead) const { QString partial = matches.first(); for (const QString ¤t : matches) { if (!current.startsWith(partial)) { while (partial.length() > lead) { partial.remove(partial.length() - 1, 1); if (current.startsWith(partial)) { break; } } if (partial.length() == lead) { return QString(); } } } return partial; } // Return the string to complete (the letters behind the cursor) QString KateWordCompletionView::word() const { return m_view->document()->text(range()); } // Return the range containing the word behind the cursor KTextEditor::Range KateWordCompletionView::range() const { return m_dWCompletionModel->completionRange(m_view, m_view->cursorPosition()); } // END diff --git a/src/dialogs/katedialogs.cpp b/src/dialogs/katedialogs.cpp index 21a8126f..c9291bc8 100644 --- a/src/dialogs/katedialogs.cpp +++ b/src/dialogs/katedialogs.cpp @@ -1,1432 +1,1432 @@ /* This file is part of the KDE libraries Copyright (C) 2002, 2003 Anders Lund Copyright (C) 2003 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2006 Dominik Haumann Copyright (C) 2007 Mirko Stocker Copyright (C) 2009 Michel Ludwig Copyright (C) 2009 Erlend Hamberg Based on work of: Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // BEGIN Includes #include "katedialogs.h" #include #include #include "kateautoindent.h" #include "katebuffer.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katemodeconfigpage.h" #include "kateschema.h" #include "kateview.h" #include "spellcheck/spellcheck.h" // auto generated ui files #include "ui_bordersappearanceconfigwidget.h" #include "ui_completionconfigtab.h" #include "ui_editconfigwidget.h" #include "ui_indentationconfigwidget.h" #include "ui_navigationconfigwidget.h" #include "ui_opensaveconfigadvwidget.h" #include "ui_opensaveconfigwidget.h" #include "ui_spellcheckconfigwidget.h" #include "ui_textareaappearanceconfigwidget.h" #include #include #include "kateabstractinputmodefactory.h" #include "katepartdebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // END // BEGIN KateIndentConfigTab KateIndentConfigTab::KateIndentConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::IndentationConfigWidget(); ui->setupUi(newWidget); ui->cmbMode->addItems(KateAutoIndent::listModes()); // FIXME Give ui->label a more descriptive name, it's these "More..." info about tab key action ui->label->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); connect(ui->label, &QLabel::linkActivated, this, &KateIndentConfigTab::showWhatsThis); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->chkBackspaceUnindents); observeChanges(ui->chkIndentPaste); observeChanges(ui->chkKeepExtraSpaces); observeChanges(ui->cmbMode); observeChanges(ui->rbIndentMixed); observeChanges(ui->rbIndentWithSpaces); observeChanges(ui->rbIndentWithTabs); connect(ui->rbIndentWithTabs, &QAbstractButton::toggled, ui->sbIndentWidth, &QWidget::setDisabled); connect(ui->rbIndentWithTabs, &QAbstractButton::toggled, this, &KateIndentConfigTab::slotChanged); // FIXME See slot below observeChanges(ui->rbTabAdvances); observeChanges(ui->rbTabIndents); observeChanges(ui->rbTabSmart); observeChanges(ui->sbIndentWidth); observeChanges(ui->sbTabWidth); layout->addWidget(newWidget); setLayout(layout); } KateIndentConfigTab::~KateIndentConfigTab() { delete ui; } void KateIndentConfigTab::slotChanged() { // FIXME Make it working without this quirk // When the value is not copied it will silently set back to "Tabs & Spaces" if (ui->rbIndentWithTabs->isChecked()) { ui->sbIndentWidth->setValue(ui->sbTabWidth->value()); } } // NOTE Should we have more use of such info stuff, consider to make it part // of KateConfigPage and add a similar function like observeChanges(..) void KateIndentConfigTab::showWhatsThis(const QString &text) { QWhatsThis::showText(QCursor::pos(), text); } void KateIndentConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateDocumentConfig::global()->configStart(); KateDocumentConfig::global()->setBackspaceIndents(ui->chkBackspaceUnindents->isChecked()); KateDocumentConfig::global()->setIndentPastedText(ui->chkIndentPaste->isChecked()); KateDocumentConfig::global()->setIndentationMode(KateAutoIndent::modeName(ui->cmbMode->currentIndex())); KateDocumentConfig::global()->setIndentationWidth(ui->sbIndentWidth->value()); KateDocumentConfig::global()->setKeepExtraSpaces(ui->chkKeepExtraSpaces->isChecked()); KateDocumentConfig::global()->setReplaceTabsDyn(ui->rbIndentWithSpaces->isChecked()); KateDocumentConfig::global()->setTabWidth(ui->sbTabWidth->value()); if (ui->rbTabAdvances->isChecked()) { KateDocumentConfig::global()->setTabHandling(KateDocumentConfig::tabInsertsTab); } else if (ui->rbTabIndents->isChecked()) { KateDocumentConfig::global()->setTabHandling(KateDocumentConfig::tabIndents); } else { KateDocumentConfig::global()->setTabHandling(KateDocumentConfig::tabSmart); } KateDocumentConfig::global()->configEnd(); } void KateIndentConfigTab::reload() { ui->chkBackspaceUnindents->setChecked(KateDocumentConfig::global()->backspaceIndents()); ui->chkIndentPaste->setChecked(KateDocumentConfig::global()->indentPastedText()); ui->chkKeepExtraSpaces->setChecked(KateDocumentConfig::global()->keepExtraSpaces()); ui->sbIndentWidth->setSuffix(ki18np(" character", " characters")); ui->sbIndentWidth->setValue(KateDocumentConfig::global()->indentationWidth()); ui->sbTabWidth->setSuffix(ki18np(" character", " characters")); ui->sbTabWidth->setValue(KateDocumentConfig::global()->tabWidth()); ui->rbTabAdvances->setChecked(KateDocumentConfig::global()->tabHandling() == KateDocumentConfig::tabInsertsTab); ui->rbTabIndents->setChecked(KateDocumentConfig::global()->tabHandling() == KateDocumentConfig::tabIndents); ui->rbTabSmart->setChecked(KateDocumentConfig::global()->tabHandling() == KateDocumentConfig::tabSmart); ui->cmbMode->setCurrentIndex(KateAutoIndent::modeNumber(KateDocumentConfig::global()->indentationMode())); if (KateDocumentConfig::global()->replaceTabsDyn()) { ui->rbIndentWithSpaces->setChecked(true); } else { if (KateDocumentConfig::global()->indentationWidth() == KateDocumentConfig::global()->tabWidth()) { ui->rbIndentWithTabs->setChecked(true); } else { ui->rbIndentMixed->setChecked(true); } } ui->sbIndentWidth->setEnabled(!ui->rbIndentWithTabs->isChecked()); } QString KateIndentConfigTab::name() const { return i18n("Indentation"); } // END KateIndentConfigTab // BEGIN KateCompletionConfigTab KateCompletionConfigTab::KateCompletionConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::CompletionConfigTab(); ui->setupUi(newWidget); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->chkAutoCompletionEnabled); observeChanges(ui->gbKeywordCompletion); observeChanges(ui->gbWordCompletion); observeChanges(ui->minimalWordLength); observeChanges(ui->removeTail); layout->addWidget(newWidget); setLayout(layout); } KateCompletionConfigTab::~KateCompletionConfigTab() { delete ui; } void KateCompletionConfigTab::showWhatsThis(const QString &text) // NOTE Not used atm, remove? See also KateIndentConfigTab::showWhatsThis { QWhatsThis::showText(QCursor::pos(), text); } void KateCompletionConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateViewConfig::global()->setValue(KateViewConfig::AutomaticCompletionInvocation, ui->chkAutoCompletionEnabled->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::KeywordCompletion, ui->gbKeywordCompletion->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::WordCompletion, ui->gbWordCompletion->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::WordCompletionMinimalWordLength, ui->minimalWordLength->value()); KateViewConfig::global()->setValue(KateViewConfig::WordCompletionRemoveTail, ui->removeTail->isChecked()); KateViewConfig::global()->configEnd(); } void KateCompletionConfigTab::reload() { ui->chkAutoCompletionEnabled->setChecked(KateViewConfig::global()->automaticCompletionInvocation()); ui->gbKeywordCompletion->setChecked(KateViewConfig::global()->keywordCompletion()); ui->gbWordCompletion->setChecked(KateViewConfig::global()->wordCompletion()); ui->minimalWordLength->setValue(KateViewConfig::global()->wordCompletionMinimalWordLength()); ui->removeTail->setChecked(KateViewConfig::global()->wordCompletionRemoveTail()); } QString KateCompletionConfigTab::name() const { return i18n("Auto Completion"); } // END KateCompletionConfigTab // BEGIN KateSpellCheckConfigTab KateSpellCheckConfigTab::KateSpellCheckConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::SpellCheckConfigWidget(); ui->setupUi(newWidget); // "What's This?" help can be found in the ui file reload(); m_sonnetConfigWidget = new Sonnet::ConfigWidget(this); connect(m_sonnetConfigWidget, &Sonnet::ConfigWidget::configChanged, this, &KateSpellCheckConfigTab::slotChanged); layout->addWidget(m_sonnetConfigWidget); layout->addWidget(newWidget); setLayout(layout); } KateSpellCheckConfigTab::~KateSpellCheckConfigTab() { delete ui; } void KateSpellCheckConfigTab::showWhatsThis(const QString &text) // NOTE Not used atm, remove? See also KateIndentConfigTab::showWhatsThis { QWhatsThis::showText(QCursor::pos(), text); } void KateSpellCheckConfigTab::apply() { if (!hasChanged()) { // nothing changed, no need to apply stuff return; } m_changed = false; // WARNING: this is slightly hackish, but it's currently the only way to // do it, see also the KTextEdit class KateDocumentConfig::global()->configStart(); m_sonnetConfigWidget->save(); QSettings settings(QStringLiteral("KDE"), QStringLiteral("Sonnet")); KateDocumentConfig::global()->setOnTheFlySpellCheck(settings.value(QStringLiteral("checkerEnabledByDefault"), false).toBool()); KateDocumentConfig::global()->configEnd(); const auto docs = KTextEditor::EditorPrivate::self()->kateDocuments(); for (KTextEditor::DocumentPrivate *doc : docs) { doc->refreshOnTheFlyCheck(); } } void KateSpellCheckConfigTab::reload() { // does nothing } QString KateSpellCheckConfigTab::name() const { return i18n("Spellcheck"); } // END KateSpellCheckConfigTab // BEGIN KateNavigationConfigTab KateNavigationConfigTab::KateNavigationConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us having more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::NavigationConfigWidget(); ui->setupUi(newWidget); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->cbTextSelectionMode); observeChanges(ui->chkBackspaceRemoveComposed); observeChanges(ui->chkPagingMovesCursor); observeChanges(ui->chkScrollPastEnd); observeChanges(ui->chkSmartHome); observeChanges(ui->sbAutoCenterCursor); layout->addWidget(newWidget); setLayout(layout); } KateNavigationConfigTab::~KateNavigationConfigTab() { delete ui; } void KateNavigationConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); KateDocumentConfig::global()->setPageUpDownMovesCursor(ui->chkPagingMovesCursor->isChecked()); KateDocumentConfig::global()->setSmartHome(ui->chkSmartHome->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::AutoCenterLines, ui->sbAutoCenterCursor->value()); KateViewConfig::global()->setValue(KateViewConfig::BackspaceRemoveComposedCharacters, ui->chkBackspaceRemoveComposed->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::PersistentSelection, ui->cbTextSelectionMode->currentIndex() == 1); KateViewConfig::global()->setValue(KateViewConfig::ScrollPastEnd, ui->chkScrollPastEnd->isChecked()); KateDocumentConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); } void KateNavigationConfigTab::reload() { ui->cbTextSelectionMode->setCurrentIndex(KateViewConfig::global()->persistentSelection() ? 1 : 0); ui->chkBackspaceRemoveComposed->setChecked(KateViewConfig::global()->backspaceRemoveComposed()); ui->chkPagingMovesCursor->setChecked(KateDocumentConfig::global()->pageUpDownMovesCursor()); ui->chkScrollPastEnd->setChecked(KateViewConfig::global()->scrollPastEnd()); ui->chkSmartHome->setChecked(KateDocumentConfig::global()->smartHome()); ui->sbAutoCenterCursor->setValue(KateViewConfig::global()->autoCenterLines()); } QString KateNavigationConfigTab::name() const { return i18n("Text Navigation"); } // END KateNavigationConfigTab // BEGIN KateEditGeneralConfigTab KateEditGeneralConfigTab::KateEditGeneralConfigTab(QWidget *parent) : KateConfigPage(parent) { QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::EditConfigWidget(); ui->setupUi(newWidget); const QList inputModes = KTextEditor::EditorPrivate::self()->inputModeFactories(); for (KateAbstractInputModeFactory *fact : inputModes) { ui->cmbInputMode->addItem(fact->name(), static_cast(fact->inputMode())); } // "What's This?" Help is in the ui-files reload(); observeChanges(ui->chkAutoBrackets); observeChanges(ui->chkMousePasteAtCursorPosition); observeChanges(ui->chkShowStaticWordWrapMarker); observeChanges(ui->chkTextDragAndDrop); observeChanges(ui->chkSmartCopyCut); observeChanges(ui->chkStaticWordWrap); observeChanges(ui->cmbEncloseSelection); connect(ui->cmbEncloseSelection->lineEdit(), &QLineEdit::editingFinished, [=] { const int index = ui->cmbEncloseSelection->currentIndex(); const QString text = ui->cmbEncloseSelection->currentText(); // Text removed? Remove item, but don't remove default data! if (index >= UserData && text.isEmpty()) { ui->cmbEncloseSelection->removeItem(index); slotChanged(); // Not already there? Add new item! For whatever reason it isn't done automatically } else if (ui->cmbEncloseSelection->findText(text) < 0) { ui->cmbEncloseSelection->addItem(text); slotChanged(); } ui->cmbEncloseSelection->setCurrentIndex(ui->cmbEncloseSelection->findText(text)); }); observeChanges(ui->cmbInputMode); observeChanges(ui->sbWordWrap); layout->addWidget(newWidget); setLayout(layout); } KateEditGeneralConfigTab::~KateEditGeneralConfigTab() { delete ui; } void KateEditGeneralConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); KateDocumentConfig::global()->setWordWrap(ui->chkStaticWordWrap->isChecked()); KateDocumentConfig::global()->setWordWrapAt(ui->sbWordWrap->value()); KateRendererConfig::global()->setWordWrapMarker(ui->chkShowStaticWordWrapMarker->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::AutoBrackets, ui->chkAutoBrackets->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::CharsToEncloseSelection, ui->cmbEncloseSelection->currentText()); QStringList userLetters; for (int i = UserData; i < ui->cmbEncloseSelection->count(); ++i) { userLetters.append(ui->cmbEncloseSelection->itemText(i)); } KateViewConfig::global()->setValue(KateViewConfig::UserSetsOfCharsToEncloseSelection, userLetters); KateViewConfig::global()->setValue(KateViewConfig::InputMode, ui->cmbInputMode->currentData().toInt()); KateViewConfig::global()->setValue(KateViewConfig::MousePasteAtCursorPosition, ui->chkMousePasteAtCursorPosition->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::TextDragAndDrop, ui->chkTextDragAndDrop->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::SmartCopyCut, ui->chkSmartCopyCut->isChecked()); KateDocumentConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); } void KateEditGeneralConfigTab::reload() { ui->chkAutoBrackets->setChecked(KateViewConfig::global()->autoBrackets()); ui->chkMousePasteAtCursorPosition->setChecked(KateViewConfig::global()->mousePasteAtCursorPosition()); ui->chkShowStaticWordWrapMarker->setChecked(KateRendererConfig::global()->wordWrapMarker()); ui->chkTextDragAndDrop->setChecked(KateViewConfig::global()->textDragAndDrop()); ui->chkSmartCopyCut->setChecked(KateViewConfig::global()->smartCopyCut()); ui->chkStaticWordWrap->setChecked(KateDocumentConfig::global()->wordWrap()); ui->sbWordWrap->setSuffix(ki18ncp("Wrap words at (value is at 20 or larger)", " character", " characters")); ui->sbWordWrap->setValue(KateDocumentConfig::global()->wordWrapAt()); ui->cmbEncloseSelection->clear(); ui->cmbEncloseSelection->lineEdit()->setClearButtonEnabled(true); ui->cmbEncloseSelection->lineEdit()->setPlaceholderText(i18n("Feature is not active")); ui->cmbEncloseSelection->addItem(QString(), None); ui->cmbEncloseSelection->setItemData(0, i18n("Disable Feature"), Qt::ToolTipRole); ui->cmbEncloseSelection->addItem(QStringLiteral("`*_~"), MarkDown); ui->cmbEncloseSelection->setItemData(1, i18n("May be handy with Markdown"), Qt::ToolTipRole); ui->cmbEncloseSelection->addItem(QStringLiteral("<>(){}[]"), MirrorChar); ui->cmbEncloseSelection->setItemData(2, i18n("Mirror characters, similar but not exactly like auto brackets"), Qt::ToolTipRole); ui->cmbEncloseSelection->addItem(QStringLiteral("´`_.:|#@~*!?$%/=,;-+^°§&"), NonLetters); ui->cmbEncloseSelection->setItemData(3, i18n("Non letter character"), Qt::ToolTipRole); const QStringList userLetters = KateViewConfig::global()->value(KateViewConfig::UserSetsOfCharsToEncloseSelection).toStringList(); for (int i = 0; i < userLetters.size(); ++i) { ui->cmbEncloseSelection->addItem(userLetters.at(i), UserData + i); } ui->cmbEncloseSelection->setCurrentIndex(ui->cmbEncloseSelection->findText(KateViewConfig::global()->charsToEncloseSelection())); const int id = static_cast(KateViewConfig::global()->inputMode()); ui->cmbInputMode->setCurrentIndex(ui->cmbInputMode->findData(id)); } QString KateEditGeneralConfigTab::name() const { return i18n("General"); } // END KateEditGeneralConfigTab // BEGIN KateEditConfigTab KateEditConfigTab::KateEditConfigTab(QWidget *parent) : KateConfigPage(parent) , editConfigTab(new KateEditGeneralConfigTab(this)) , navigationConfigTab(new KateNavigationConfigTab(this)) , indentConfigTab(new KateIndentConfigTab(this)) , completionConfigTab(new KateCompletionConfigTab(this)) , spellCheckConfigTab(new KateSpellCheckConfigTab(this)) { QVBoxLayout *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); QTabWidget *tabWidget = new QTabWidget(this); // add all tabs tabWidget->insertTab(0, editConfigTab, editConfigTab->name()); tabWidget->insertTab(1, navigationConfigTab, navigationConfigTab->name()); tabWidget->insertTab(2, indentConfigTab, indentConfigTab->name()); tabWidget->insertTab(3, completionConfigTab, completionConfigTab->name()); tabWidget->insertTab(4, spellCheckConfigTab, spellCheckConfigTab->name()); observeChanges(editConfigTab); observeChanges(navigationConfigTab); observeChanges(indentConfigTab); observeChanges(completionConfigTab); observeChanges(spellCheckConfigTab); int i = tabWidget->count(); const auto inputModeFactories = KTextEditor::EditorPrivate::self()->inputModeFactories(); for (KateAbstractInputModeFactory *factory : inputModeFactories) { KateConfigPage *tab = factory->createConfigPage(this); if (tab) { m_inputModeConfigTabs << tab; tabWidget->insertTab(i, tab, tab->name()); observeChanges(tab); i++; } } layout->addWidget(tabWidget); setLayout(layout); } KateEditConfigTab::~KateEditConfigTab() { qDeleteAll(m_inputModeConfigTabs); } void KateEditConfigTab::apply() { // try to update the rest of tabs editConfigTab->apply(); navigationConfigTab->apply(); indentConfigTab->apply(); completionConfigTab->apply(); spellCheckConfigTab->apply(); for (KateConfigPage *tab : qAsConst(m_inputModeConfigTabs)) { tab->apply(); } } void KateEditConfigTab::reload() { editConfigTab->reload(); navigationConfigTab->reload(); indentConfigTab->reload(); completionConfigTab->reload(); spellCheckConfigTab->reload(); for (KateConfigPage *tab : qAsConst(m_inputModeConfigTabs)) { tab->reload(); } } void KateEditConfigTab::reset() { editConfigTab->reset(); navigationConfigTab->reset(); indentConfigTab->reset(); completionConfigTab->reset(); spellCheckConfigTab->reset(); for (KateConfigPage *tab : qAsConst(m_inputModeConfigTabs)) { tab->reset(); } } void KateEditConfigTab::defaults() { editConfigTab->defaults(); navigationConfigTab->defaults(); indentConfigTab->defaults(); completionConfigTab->defaults(); spellCheckConfigTab->defaults(); for (KateConfigPage *tab : qAsConst(m_inputModeConfigTabs)) { tab->defaults(); } } QString KateEditConfigTab::name() const { return i18n("Editing"); } QString KateEditConfigTab::fullName() const { return i18n("Editing Options"); } QIcon KateEditConfigTab::icon() const { return QIcon::fromTheme(QStringLiteral("accessories-text-editor")); } // END KateEditConfigTab // BEGIN KateViewDefaultsConfig KateViewDefaultsConfig::KateViewDefaultsConfig(QWidget *parent) : KateConfigPage(parent) , textareaUi(new Ui::TextareaAppearanceConfigWidget()) , bordersUi(new Ui::BordersAppearanceConfigWidget()) { QLayout *layout = new QVBoxLayout(this); QTabWidget *tabWidget = new QTabWidget(this); layout->addWidget(tabWidget); layout->setContentsMargins(0, 0, 0, 0); QWidget *textareaTab = new QWidget(tabWidget); textareaUi->setupUi(textareaTab); tabWidget->addTab(textareaTab, i18n("General")); QWidget *bordersTab = new QWidget(tabWidget); bordersUi->setupUi(bordersTab); tabWidget->addTab(bordersTab, i18n("Borders")); textareaUi->cmbDynamicWordWrapIndicator->addItem(i18n("Off")); textareaUi->cmbDynamicWordWrapIndicator->addItem(i18n("Follow Line Numbers")); textareaUi->cmbDynamicWordWrapIndicator->addItem(i18n("Always On")); // "What's This?" help is in the ui-file reload(); observeChanges(textareaUi->chkAnimateBracketMatching); observeChanges(textareaUi->chkDynWrapAtStaticMarker); observeChanges(textareaUi->chkFoldFirstLine); observeChanges(textareaUi->chkShowIndentationLines); observeChanges(textareaUi->chkShowLineCount); observeChanges(textareaUi->chkShowTabs); observeChanges(textareaUi->chkShowWholeBracketExpression); observeChanges(textareaUi->chkShowWordCount); observeChanges(textareaUi->cmbDynamicWordWrapIndicator); observeChanges(textareaUi->gbWordWrap); observeChanges(textareaUi->sbDynamicWordWrapDepth); observeChanges(textareaUi->sliSetMarkerSize); observeChanges(textareaUi->spacesComboBox); observeChanges(bordersUi->chkIconBorder); observeChanges(bordersUi->chkLineNumbers); observeChanges(bordersUi->chkScrollbarMarks); observeChanges(bordersUi->chkScrollbarMiniMap); observeChanges(bordersUi->chkScrollbarMiniMapAll); bordersUi->chkScrollbarMiniMapAll->hide(); // this is temporary until the feature is done observeChanges(bordersUi->chkScrollbarPreview); observeChanges(bordersUi->chkShowFoldingMarkers); observeChanges(bordersUi->chkShowFoldingPreview); observeChanges(bordersUi->chkShowLineModification); observeChanges(bordersUi->cmbShowScrollbars); observeChanges(bordersUi->rbSortBookmarksByCreation); observeChanges(bordersUi->rbSortBookmarksByPosition); observeChanges(bordersUi->spBoxMiniMapWidth); } KateViewDefaultsConfig::~KateViewDefaultsConfig() { delete bordersUi; delete textareaUi; } void KateViewDefaultsConfig::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateRendererConfig::global()->configStart(); KateDocumentConfig::global()->setMarkerSize(textareaUi->sliSetMarkerSize->value()); KateDocumentConfig::global()->setShowSpaces(KateDocumentConfig::WhitespaceRendering(textareaUi->spacesComboBox->currentIndex())); KateDocumentConfig::global()->setShowTabs(textareaUi->chkShowTabs->isChecked()); KateRendererConfig::global()->setAnimateBracketMatching(textareaUi->chkAnimateBracketMatching->isChecked()); KateRendererConfig::global()->setShowIndentationLines(textareaUi->chkShowIndentationLines->isChecked()); KateRendererConfig::global()->setShowWholeBracketExpression(textareaUi->chkShowWholeBracketExpression->isChecked()); KateViewConfig::global()->setDynWordWrap(textareaUi->gbWordWrap->isChecked()); KateViewConfig::global()->setShowWordCount(textareaUi->chkShowWordCount->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::BookmarkSorting, bordersUi->rbSortBookmarksByPosition->isChecked() ? 0 : 1); KateViewConfig::global()->setValue(KateViewConfig::DynWordWrapAlignIndent, textareaUi->sbDynamicWordWrapDepth->value()); KateViewConfig::global()->setValue(KateViewConfig::DynWordWrapIndicators, textareaUi->cmbDynamicWordWrapIndicator->currentIndex()); KateViewConfig::global()->setValue(KateViewConfig::DynWrapAtStaticMarker, textareaUi->chkDynWrapAtStaticMarker->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::FoldFirstLine, textareaUi->chkFoldFirstLine->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ScrollBarMiniMapWidth, bordersUi->spBoxMiniMapWidth->value()); KateViewConfig::global()->setValue(KateViewConfig::ShowFoldingBar, bordersUi->chkShowFoldingMarkers->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowFoldingPreview, bordersUi->chkShowFoldingPreview->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowIconBar, bordersUi->chkIconBorder->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowLineCount, textareaUi->chkShowLineCount->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowLineModification, bordersUi->chkShowLineModification->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowLineNumbers, bordersUi->chkLineNumbers->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarMarks, bordersUi->chkScrollbarMarks->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarMiniMap, bordersUi->chkScrollbarMiniMap->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarMiniMapAll, bordersUi->chkScrollbarMiniMapAll->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarPreview, bordersUi->chkScrollbarPreview->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollbars, bordersUi->cmbShowScrollbars->currentIndex()); KateRendererConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); } void KateViewDefaultsConfig::reload() { bordersUi->chkIconBorder->setChecked(KateViewConfig::global()->iconBar()); bordersUi->chkLineNumbers->setChecked(KateViewConfig::global()->lineNumbers()); bordersUi->chkScrollbarMarks->setChecked(KateViewConfig::global()->scrollBarMarks()); bordersUi->chkScrollbarMiniMap->setChecked(KateViewConfig::global()->scrollBarMiniMap()); bordersUi->chkScrollbarMiniMapAll->setChecked(KateViewConfig::global()->scrollBarMiniMapAll()); bordersUi->chkScrollbarPreview->setChecked(KateViewConfig::global()->scrollBarPreview()); bordersUi->chkShowFoldingMarkers->setChecked(KateViewConfig::global()->foldingBar()); bordersUi->chkShowFoldingPreview->setChecked(KateViewConfig::global()->foldingPreview()); bordersUi->chkShowLineModification->setChecked(KateViewConfig::global()->lineModification()); bordersUi->cmbShowScrollbars->setCurrentIndex(KateViewConfig::global()->showScrollbars()); bordersUi->rbSortBookmarksByCreation->setChecked(KateViewConfig::global()->bookmarkSort() == 1); bordersUi->rbSortBookmarksByPosition->setChecked(KateViewConfig::global()->bookmarkSort() == 0); bordersUi->spBoxMiniMapWidth->setValue(KateViewConfig::global()->scrollBarMiniMapWidth()); textareaUi->chkAnimateBracketMatching->setChecked(KateRendererConfig::global()->animateBracketMatching()); textareaUi->chkDynWrapAtStaticMarker->setChecked(KateViewConfig::global()->dynWrapAtStaticMarker()); textareaUi->chkFoldFirstLine->setChecked(KateViewConfig::global()->foldFirstLine()); textareaUi->chkShowIndentationLines->setChecked(KateRendererConfig::global()->showIndentationLines()); textareaUi->chkShowLineCount->setChecked(KateViewConfig::global()->showLineCount()); textareaUi->chkShowTabs->setChecked(KateDocumentConfig::global()->showTabs()); textareaUi->chkShowWholeBracketExpression->setChecked(KateRendererConfig::global()->showWholeBracketExpression()); textareaUi->chkShowWordCount->setChecked(KateViewConfig::global()->showWordCount()); textareaUi->cmbDynamicWordWrapIndicator->setCurrentIndex(KateViewConfig::global()->dynWordWrapIndicators()); textareaUi->gbWordWrap->setChecked(KateViewConfig::global()->dynWordWrap()); textareaUi->sbDynamicWordWrapDepth->setValue(KateViewConfig::global()->dynWordWrapAlignIndent()); textareaUi->sliSetMarkerSize->setValue(KateDocumentConfig::global()->markerSize()); textareaUi->spacesComboBox->setCurrentIndex(KateDocumentConfig::global()->showSpaces()); } void KateViewDefaultsConfig::reset() { ; } void KateViewDefaultsConfig::defaults() { ; } QString KateViewDefaultsConfig::name() const { return i18n("Appearance"); } QString KateViewDefaultsConfig::fullName() const { return i18n("Appearance"); } QIcon KateViewDefaultsConfig::icon() const { return QIcon::fromTheme(QStringLiteral("preferences-desktop-theme")); } // END KateViewDefaultsConfig // BEGIN KateSaveConfigTab KateSaveConfigTab::KateSaveConfigTab(QWidget *parent) : KateConfigPage(parent) , modeConfigPage(new ModeConfigPage(this)) { // FIXME: Is really needed to move all this code below to another class, // since it is another tab itself on the config dialog. This means we should // initialize, add and work with as we do with modeConfigPage (ereslibre) QVBoxLayout *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); QTabWidget *tabWidget = new QTabWidget(this); QWidget *tmpWidget = new QWidget(tabWidget); QVBoxLayout *internalLayout = new QVBoxLayout; QWidget *newWidget = new QWidget(tabWidget); ui = new Ui::OpenSaveConfigWidget(); ui->setupUi(newWidget); QWidget *tmpWidget2 = new QWidget(tabWidget); QVBoxLayout *internalLayout2 = new QVBoxLayout; QWidget *newWidget2 = new QWidget(tabWidget); uiadv = new Ui::OpenSaveConfigAdvWidget(); uiadv->setupUi(newWidget2); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->cbRemoveTrailingSpaces); observeChanges(ui->chkDetectEOL); observeChanges(ui->chkEnableBOM); observeChanges(ui->chkNewLineAtEof); observeChanges(ui->cmbEOL); observeChanges(ui->cmbEncoding); observeChanges(ui->cmbEncodingDetection); observeChanges(ui->cmbEncodingFallback); observeChanges(ui->lineLengthLimit); observeChanges(uiadv->chkBackupLocalFiles); observeChanges(uiadv->chkBackupRemoteFiles); observeChanges(uiadv->cmbSwapFileMode); connect(uiadv->cmbSwapFileMode, QOverload::of(&QComboBox::currentIndexChanged), this, &KateSaveConfigTab::swapFileModeChanged); observeChanges(uiadv->edtBackupPrefix); observeChanges(uiadv->edtBackupSuffix); observeChanges(uiadv->kurlSwapDirectory); observeChanges(uiadv->spbSwapFileSync); internalLayout->addWidget(newWidget); tmpWidget->setLayout(internalLayout); internalLayout2->addWidget(newWidget2); tmpWidget2->setLayout(internalLayout2); // add all tabs tabWidget->insertTab(0, tmpWidget, i18n("General")); tabWidget->insertTab(1, tmpWidget2, i18n("Advanced")); tabWidget->insertTab(2, modeConfigPage, modeConfigPage->name()); observeChanges(modeConfigPage); layout->addWidget(tabWidget); setLayout(layout); // support variable expansion in backup prefix/suffix KTextEditor::Editor::instance()->addVariableExpansion({uiadv->edtBackupPrefix, uiadv->edtBackupSuffix}, {QStringLiteral("Date:Locale"), QStringLiteral("Date:ISO"), QStringLiteral("Date:"), QStringLiteral("Time:Locale"), QStringLiteral("Time:ISO"), QStringLiteral("Time:"), QStringLiteral("ENV:"), QStringLiteral("JS:"), QStringLiteral("UUID")}); } KateSaveConfigTab::~KateSaveConfigTab() { delete ui; } void KateSaveConfigTab::swapFileModeChanged(int idx) { const KateDocumentConfig::SwapFileMode mode = static_cast(idx); switch (mode) { - case KateDocumentConfig::DisableSwapFile: - uiadv->lblSwapDirectory->setEnabled(false); - uiadv->kurlSwapDirectory->setEnabled(false); - uiadv->lblSwapFileSync->setEnabled(false); - uiadv->spbSwapFileSync->setEnabled(false); - break; - case KateDocumentConfig::EnableSwapFile: - uiadv->lblSwapDirectory->setEnabled(false); - uiadv->kurlSwapDirectory->setEnabled(false); - uiadv->lblSwapFileSync->setEnabled(true); - uiadv->spbSwapFileSync->setEnabled(true); - break; - case KateDocumentConfig::SwapFilePresetDirectory: - uiadv->lblSwapDirectory->setEnabled(true); - uiadv->kurlSwapDirectory->setEnabled(true); - uiadv->lblSwapFileSync->setEnabled(true); - uiadv->spbSwapFileSync->setEnabled(true); - break; + case KateDocumentConfig::DisableSwapFile: + uiadv->lblSwapDirectory->setEnabled(false); + uiadv->kurlSwapDirectory->setEnabled(false); + uiadv->lblSwapFileSync->setEnabled(false); + uiadv->spbSwapFileSync->setEnabled(false); + break; + case KateDocumentConfig::EnableSwapFile: + uiadv->lblSwapDirectory->setEnabled(false); + uiadv->kurlSwapDirectory->setEnabled(false); + uiadv->lblSwapFileSync->setEnabled(true); + uiadv->spbSwapFileSync->setEnabled(true); + break; + case KateDocumentConfig::SwapFilePresetDirectory: + uiadv->lblSwapDirectory->setEnabled(true); + uiadv->kurlSwapDirectory->setEnabled(true); + uiadv->lblSwapFileSync->setEnabled(true); + uiadv->spbSwapFileSync->setEnabled(true); + break; } } void KateSaveConfigTab::apply() { modeConfigPage->apply(); // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateGlobalConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); if (uiadv->edtBackupSuffix->text().isEmpty() && uiadv->edtBackupPrefix->text().isEmpty()) { KMessageBox::information(this, i18n("You did not provide a backup suffix or prefix. Using default suffix: '~'"), i18n("No Backup Suffix or Prefix")); uiadv->edtBackupSuffix->setText(QStringLiteral("~")); } KateDocumentConfig::global()->setBackupOnSaveLocal(uiadv->chkBackupLocalFiles->isChecked()); KateDocumentConfig::global()->setBackupOnSaveRemote(uiadv->chkBackupRemoteFiles->isChecked()); KateDocumentConfig::global()->setBackupPrefix(uiadv->edtBackupPrefix->text()); KateDocumentConfig::global()->setBackupSuffix(uiadv->edtBackupSuffix->text()); KateDocumentConfig::global()->setSwapFileMode(uiadv->cmbSwapFileMode->currentIndex()); KateDocumentConfig::global()->setSwapDirectory(uiadv->kurlSwapDirectory->url().toLocalFile()); KateDocumentConfig::global()->setSwapSyncInterval(uiadv->spbSwapFileSync->value()); KateDocumentConfig::global()->setRemoveSpaces(ui->cbRemoveTrailingSpaces->currentIndex()); KateDocumentConfig::global()->setNewLineAtEof(ui->chkNewLineAtEof->isChecked()); // set both standard and fallback encoding KateDocumentConfig::global()->setEncoding(KCharsets::charsets()->encodingForName(ui->cmbEncoding->currentText())); KateGlobalConfig::global()->setProberType((KEncodingProber::ProberType)ui->cmbEncodingDetection->currentIndex()); KateGlobalConfig::global()->setFallbackEncoding(KCharsets::charsets()->encodingForName(ui->cmbEncodingFallback->currentText())); KateDocumentConfig::global()->setEol(ui->cmbEOL->currentIndex()); KateDocumentConfig::global()->setAllowEolDetection(ui->chkDetectEOL->isChecked()); KateDocumentConfig::global()->setBom(ui->chkEnableBOM->isChecked()); KateDocumentConfig::global()->setLineLengthLimit(ui->lineLengthLimit->value()); KateDocumentConfig::global()->configEnd(); KateGlobalConfig::global()->configEnd(); } void KateSaveConfigTab::reload() { modeConfigPage->reload(); // encodings ui->cmbEncoding->clear(); ui->cmbEncodingFallback->clear(); QStringList encodings(KCharsets::charsets()->descriptiveEncodingNames()); int insert = 0; for (int i = 0; i < encodings.count(); i++) { bool found = false; QTextCodec *codecForEnc = KCharsets::charsets()->codecForName(KCharsets::charsets()->encodingForName(encodings[i]), found); if (found) { ui->cmbEncoding->addItem(encodings[i]); ui->cmbEncodingFallback->addItem(encodings[i]); if (codecForEnc == KateDocumentConfig::global()->codec()) { ui->cmbEncoding->setCurrentIndex(insert); } if (codecForEnc == KateGlobalConfig::global()->fallbackCodec()) { // adjust index for fallback config, has no default! ui->cmbEncodingFallback->setCurrentIndex(insert); } insert++; } } // encoding detection ui->cmbEncodingDetection->clear(); bool found = false; for (int i = 0; !KEncodingProber::nameForProberType((KEncodingProber::ProberType)i).isEmpty(); ++i) { ui->cmbEncodingDetection->addItem(KEncodingProber::nameForProberType((KEncodingProber::ProberType)i)); if (i == KateGlobalConfig::global()->proberType()) { ui->cmbEncodingDetection->setCurrentIndex(ui->cmbEncodingDetection->count() - 1); found = true; } } if (!found) { ui->cmbEncodingDetection->setCurrentIndex(KEncodingProber::Universal); } // eol ui->cmbEOL->setCurrentIndex(KateDocumentConfig::global()->eol()); ui->chkDetectEOL->setChecked(KateDocumentConfig::global()->allowEolDetection()); ui->chkEnableBOM->setChecked(KateDocumentConfig::global()->bom()); ui->lineLengthLimit->setValue(KateDocumentConfig::global()->lineLengthLimit()); ui->cbRemoveTrailingSpaces->setCurrentIndex(KateDocumentConfig::global()->removeSpaces()); ui->chkNewLineAtEof->setChecked(KateDocumentConfig::global()->newLineAtEof()); // other stuff uiadv->chkBackupLocalFiles->setChecked(KateDocumentConfig::global()->backupOnSaveLocal()); uiadv->chkBackupRemoteFiles->setChecked(KateDocumentConfig::global()->backupOnSaveRemote()); uiadv->edtBackupPrefix->setText(KateDocumentConfig::global()->backupPrefix()); uiadv->edtBackupSuffix->setText(KateDocumentConfig::global()->backupSuffix()); uiadv->cmbSwapFileMode->setCurrentIndex(KateDocumentConfig::global()->swapFileMode()); uiadv->kurlSwapDirectory->setUrl(QUrl::fromLocalFile(KateDocumentConfig::global()->swapDirectory())); uiadv->spbSwapFileSync->setValue(KateDocumentConfig::global()->swapSyncInterval()); swapFileModeChanged(KateDocumentConfig::global()->swapFileMode()); } void KateSaveConfigTab::reset() { modeConfigPage->reset(); } void KateSaveConfigTab::defaults() { modeConfigPage->defaults(); ui->cbRemoveTrailingSpaces->setCurrentIndex(0); uiadv->chkBackupLocalFiles->setChecked(true); uiadv->chkBackupRemoteFiles->setChecked(false); uiadv->edtBackupPrefix->setText(QString()); uiadv->edtBackupSuffix->setText(QStringLiteral("~")); uiadv->cmbSwapFileMode->setCurrentIndex(1); uiadv->kurlSwapDirectory->setDisabled(true); uiadv->lblSwapDirectory->setDisabled(true); uiadv->spbSwapFileSync->setValue(15); } QString KateSaveConfigTab::name() const { return i18n("Open/Save"); } QString KateSaveConfigTab::fullName() const { return i18n("File Opening & Saving"); } QIcon KateSaveConfigTab::icon() const { return QIcon::fromTheme(QStringLiteral("document-save")); } // END KateSaveConfigTab // BEGIN KateGotoBar KateGotoBar::KateGotoBar(KTextEditor::View *view, QWidget *parent) : KateViewBarWidget(true, parent) , m_view(view) { Q_ASSERT(m_view != nullptr); // this bar widget is pointless w/o a view QHBoxLayout *topLayout = new QHBoxLayout(centralWidget()); topLayout->setContentsMargins(0, 0, 0, 0); QToolButton *btn = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setText(i18n("&Line:")); btn->setToolTip(i18n("Go to line number from clipboard")); connect(btn, &QToolButton::clicked, this, &KateGotoBar::gotoClipboard); topLayout->addWidget(btn); m_gotoRange = new QSpinBox(this); m_gotoRange->setMinimum(1); topLayout->addWidget(m_gotoRange, 1); topLayout->setStretchFactor(m_gotoRange, 0); btn = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setText(i18n("Go to")); btn->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); connect(btn, &QToolButton::clicked, this, &KateGotoBar::gotoLine); topLayout->addWidget(btn); btn = m_modifiedUp = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setDefaultAction(m_view->action("modified_line_up")); btn->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search"))); btn->setText(QString()); btn->installEventFilter(this); topLayout->addWidget(btn); btn = m_modifiedDown = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setDefaultAction(m_view->action("modified_line_down")); btn->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); btn->setText(QString()); btn->installEventFilter(this); topLayout->addWidget(btn); topLayout->addStretch(); setFocusProxy(m_gotoRange); } void KateGotoBar::showEvent(QShowEvent *event) { Q_UNUSED(event) // Catch rare cases where the bar is visible while document is edited connect(m_view->document(), &KTextEditor::Document::textChanged, this, &KateGotoBar::updateData); } void KateGotoBar::closed() { disconnect(m_view->document(), &KTextEditor::Document::textChanged, this, &KateGotoBar::updateData); } bool KateGotoBar::eventFilter(QObject *object, QEvent *event) { if (object == m_modifiedUp || object == m_modifiedDown) { if (event->type() != QEvent::Wheel) { return false; } int delta = static_cast(event)->angleDelta().y(); // Reset m_wheelDelta when scroll direction change if (m_wheelDelta != 0 && (m_wheelDelta < 0) != (delta < 0)) { m_wheelDelta = 0; } m_wheelDelta += delta; if (m_wheelDelta >= 120) { m_wheelDelta = 0; m_modifiedUp->click(); } else if (m_wheelDelta <= -120) { m_wheelDelta = 0; m_modifiedDown->click(); } } return false; } void KateGotoBar::gotoClipboard() { QRegularExpression rx(QStringLiteral("\\d+")); int lineNo = rx.match(QApplication::clipboard()->text(QClipboard::Selection)).captured().toInt(); if (lineNo <= m_gotoRange->maximum() && lineNo >= 1) { m_gotoRange->setValue(lineNo); gotoLine(); } else { QPointer message = new KTextEditor::Message(i18n("No valid line number found in clipboard")); message->setWordWrap(true); message->setAutoHide(2000); message->setPosition(KTextEditor::Message::BottomInView); message->setView(m_view), m_view->document()->postMessage(message); } } void KateGotoBar::updateData() { m_gotoRange->setMaximum(m_view->document()->lines()); if (!isVisible()) { m_gotoRange->setValue(m_view->cursorPosition().line() + 1); m_gotoRange->adjustSize(); // ### does not respect the range :-( } m_gotoRange->selectAll(); } void KateGotoBar::keyPressEvent(QKeyEvent *event) { int key = event->key(); if (key == Qt::Key_Return || key == Qt::Key_Enter) { gotoLine(); return; } KateViewBarWidget::keyPressEvent(event); } void KateGotoBar::gotoLine() { KTextEditor::ViewPrivate *kv = qobject_cast(m_view); if (kv && kv->selection() && !kv->config()->persistentSelection()) { kv->clearSelection(); } m_view->setCursorPosition(KTextEditor::Cursor(m_gotoRange->value() - 1, 0)); m_view->setFocus(); emit hideMe(); } // END KateGotoBar // BEGIN KateDictionaryBar KateDictionaryBar::KateDictionaryBar(KTextEditor::ViewPrivate *view, QWidget *parent) : KateViewBarWidget(true, parent) , m_view(view) { Q_ASSERT(m_view != nullptr); // this bar widget is pointless w/o a view QHBoxLayout *topLayout = new QHBoxLayout(centralWidget()); topLayout->setContentsMargins(0, 0, 0, 0); // topLayout->setSpacing(spacingHint()); m_dictionaryComboBox = new Sonnet::DictionaryComboBox(centralWidget()); connect(m_dictionaryComboBox, SIGNAL(dictionaryChanged(QString)), this, SLOT(dictionaryChanged(QString))); connect(view->doc(), SIGNAL(defaultDictionaryChanged(KTextEditor::DocumentPrivate *)), this, SLOT(updateData())); QLabel *label = new QLabel(i18n("Dictionary:"), centralWidget()); label->setBuddy(m_dictionaryComboBox); topLayout->addWidget(label); topLayout->addWidget(m_dictionaryComboBox, 1); topLayout->setStretchFactor(m_dictionaryComboBox, 0); topLayout->addStretch(); } KateDictionaryBar::~KateDictionaryBar() { } void KateDictionaryBar::updateData() { KTextEditor::DocumentPrivate *document = m_view->doc(); QString dictionary = document->defaultDictionary(); if (dictionary.isEmpty()) { dictionary = Sonnet::Speller().defaultLanguage(); } m_dictionaryComboBox->setCurrentByDictionary(dictionary); } void KateDictionaryBar::dictionaryChanged(const QString &dictionary) { const KTextEditor::Range selection = m_view->selectionRange(); if (selection.isValid() && !selection.isEmpty()) { const bool blockmode = m_view->blockSelection(); m_view->doc()->setDictionary(dictionary, selection, blockmode); } else { m_view->doc()->setDefaultDictionary(dictionary); } } // END KateGotoBar // BEGIN KateModOnHdPrompt KateModOnHdPrompt::KateModOnHdPrompt(KTextEditor::DocumentPrivate *doc, KTextEditor::ModificationInterface::ModifiedOnDiskReason modtype, const QString &reason) : QObject(doc) , m_doc(doc) , m_modtype(modtype) , m_proc(nullptr) , m_diffFile(nullptr) , m_diffAction(nullptr) { m_message = new KTextEditor::Message(reason, KTextEditor::Message::Information); m_message->setPosition(KTextEditor::Message::AboveView); m_message->setWordWrap(true); // If the file isn't deleted, present a diff button const bool onDiskDeleted = modtype == KTextEditor::ModificationInterface::OnDiskDeleted; if (!onDiskDeleted) { QAction *aAutoReload = new QAction(i18n("Enable Auto Reload"), this); aAutoReload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); aAutoReload->setToolTip(i18n("Will never again warn about on disk changes but always reload.")); m_message->addAction(aAutoReload, false); connect(aAutoReload, &QAction::triggered, this, &KateModOnHdPrompt::autoReloadTriggered); if (!QStandardPaths::findExecutable(QStringLiteral("diff")).isEmpty()) { m_diffAction = new QAction(i18n("View &Difference"), this); m_diffAction->setToolTip(i18n("Shows a diff of the changes")); m_message->addAction(m_diffAction, false); connect(m_diffAction, SIGNAL(triggered()), this, SLOT(slotDiff())); } QAction *aReload = new QAction(i18n("&Reload"), this); aReload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); aReload->setToolTip(i18n("Reload the file from disk. Unsaved changes will be lost.")); m_message->addAction(aReload); connect(aReload, SIGNAL(triggered()), this, SIGNAL(reloadTriggered())); } else { QAction *closeFile = new QAction(i18nc("@action:button closes the opened file", "&Close File"), this); closeFile->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); closeFile->setToolTip(i18n("Close the file, discarding its content.")); m_message->addAction(closeFile, false); connect(closeFile, &QAction::triggered, this, &KateModOnHdPrompt::closeTriggered); QAction *aSaveAs = new QAction(i18n("&Save As..."), this); aSaveAs->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); aSaveAs->setToolTip(i18n("Lets you select a location and save the file again.")); m_message->addAction(aSaveAs, false); connect(aSaveAs, SIGNAL(triggered()), this, SIGNAL(saveAsTriggered())); } QAction *aIgnore = new QAction(i18n("&Ignore"), this); aIgnore->setToolTip(i18n("Ignores the changes on disk without any action.")); aIgnore->setIcon(KStandardGuiItem::overwrite().icon()); m_message->addAction(aIgnore); connect(aIgnore, SIGNAL(triggered()), this, SIGNAL(ignoreTriggered())); m_doc->postMessage(m_message); } KateModOnHdPrompt::~KateModOnHdPrompt() { delete m_proc; m_proc = nullptr; if (m_diffFile) { m_diffFile->setAutoRemove(true); delete m_diffFile; m_diffFile = nullptr; } delete m_message; } void KateModOnHdPrompt::slotDiff() { if (m_diffFile) { return; } m_diffFile = new QTemporaryFile(); m_diffFile->open(); // Start a KProcess that creates a diff m_proc = new KProcess(this); m_proc->setOutputChannelMode(KProcess::MergedChannels); *m_proc << QStringLiteral("diff") << QStringLiteral("-u") << QStringLiteral("-") << m_doc->url().toLocalFile(); connect(m_proc, SIGNAL(readyRead()), this, SLOT(slotDataAvailable())); connect(m_proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(slotPDone())); // disable the diff button, to hinder the user to run it twice. m_diffAction->setEnabled(false); m_proc->start(); QTextStream ts(m_proc); int lastln = m_doc->lines() - 1; for (int l = 0; l < lastln; ++l) { ts << m_doc->line(l) << '\n'; } ts << m_doc->line(lastln); ts.flush(); m_proc->closeWriteChannel(); } void KateModOnHdPrompt::slotDataAvailable() { m_diffFile->write(m_proc->readAll()); } void KateModOnHdPrompt::slotPDone() { m_diffAction->setEnabled(true); const QProcess::ExitStatus es = m_proc->exitStatus(); delete m_proc; m_proc = nullptr; if (es != QProcess::NormalExit) { KMessageBox::sorry(nullptr, i18n("The diff command failed. Please make sure that " "diff(1) is installed and in your PATH."), i18n("Error Creating Diff")); delete m_diffFile; m_diffFile = nullptr; return; } if (m_diffFile->size() == 0) { KMessageBox::information(nullptr, i18n("The files are identical."), i18n("Diff Output")); delete m_diffFile; m_diffFile = nullptr; return; } m_diffFile->setAutoRemove(false); QUrl url = QUrl::fromLocalFile(m_diffFile->fileName()); delete m_diffFile; m_diffFile = nullptr; // KRun::runUrl should delete the file, once the client exits KRun::runUrl(url, QStringLiteral("text/x-patch"), nullptr, KRun::RunFlags(KRun::DeleteTemporaryFiles)); } // END KateModOnHdPrompt diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp index 2940df38..60b5a3bd 100644 --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -1,6072 +1,6072 @@ /* This file is part of the KDE libraries Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2006 Hamish Rodda Copyright (C) 2007 Mirko Stocker Copyright (C) 2009-2010 Michel Ludwig Copyright (C) 2013 Gerald Senarclens de Grancy Copyright (C) 2013 Andrey Matveyakin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-13020, USA. */ // BEGIN includes #include "katedocument.h" #include "config.h" #include "kateabstractinputmode.h" #include "kateautoindent.h" #include "katebuffer.h" #include "kateconfig.h" #include "katedialogs.h" #include "kateglobal.h" #include "katehighlight.h" #include "katemodemanager.h" #include "katepartdebug.h" #include "kateplaintextsearch.h" #include "kateregexp.h" #include "kateregexpsearch.h" #include "katerenderer.h" #include "kateschema.h" #include "katescriptmanager.h" #include "kateswapfile.h" #include "katetemplatehandler.h" #include "katetextline.h" #include "kateundomanager.h" #include "katevariableexpansionmanager.h" #include "kateview.h" #include "printing/kateprinter.h" #include "spellcheck/ontheflycheck.h" #include "spellcheck/prefixstore.h" #include "spellcheck/spellcheck.h" #if EDITORCONFIG_FOUND #include "editorconfig.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if LIBGIT2_FOUND #include #include #include #endif // END includes #if 0 #define EDIT_DEBUG qCDebug(LOG_KTE) #else #define EDIT_DEBUG \ if (0) \ qCDebug(LOG_KTE) #endif -template static int indexOf(const std::initializer_list &list, const E &entry) +template static int indexOf(const std::initializer_list &list, const E &entry) { auto it = std::find(list.begin(), list.end(), entry); return it == list.end() ? -1 : std::distance(list.begin(), it); } -template static bool contains(const std::initializer_list &list, const E &entry) +template static bool contains(const std::initializer_list &list, const E &entry) { return indexOf(list, entry) >= 0; } static inline QChar matchingStartBracket(const QChar c) { switch (c.toLatin1()) { - case '}': - return QLatin1Char('{'); - case ']': - return QLatin1Char('['); - case ')': - return QLatin1Char('('); + case '}': + return QLatin1Char('{'); + case ']': + return QLatin1Char('['); + case ')': + return QLatin1Char('('); } return QChar(); } static inline QChar matchingEndBracket(const QChar c, bool withQuotes = true) { switch (c.toLatin1()) { - case '{': - return QLatin1Char('}'); - case '[': - return QLatin1Char(']'); - case '(': - return QLatin1Char(')'); - case '\'': - return withQuotes ? QLatin1Char('\'') : QChar(); - case '"': - return withQuotes ? QLatin1Char('"') : QChar(); + case '{': + return QLatin1Char('}'); + case '[': + return QLatin1Char(']'); + case '(': + return QLatin1Char(')'); + case '\'': + return withQuotes ? QLatin1Char('\'') : QChar(); + case '"': + return withQuotes ? QLatin1Char('"') : QChar(); } return QChar(); } static inline QChar matchingBracket(const QChar c) { QChar bracket = matchingStartBracket(c); if (bracket.isNull()) { bracket = matchingEndBracket(c, /*withQuotes=*/false); } return bracket; } static inline bool isStartBracket(const QChar c) { return !matchingEndBracket(c, /*withQuotes=*/false).isNull(); } static inline bool isEndBracket(const QChar c) { return !matchingStartBracket(c).isNull(); } static inline bool isBracket(const QChar c) { return isStartBracket(c) || isEndBracket(c); } /** * normalize given url * @param url input url * @return normalized url */ static QUrl normalizeUrl(const QUrl &url) { /** * only normalize local urls */ if (url.isEmpty() || !url.isLocalFile()) return url; /** * don't normalize if not existing! * canonicalFilePath won't work! */ const QString normalizedUrl(QFileInfo(url.toLocalFile()).canonicalFilePath()); if (normalizedUrl.isEmpty()) return url; /** * else: use canonicalFilePath to normalize */ return QUrl::fromLocalFile(normalizedUrl); } // BEGIN d'tor, c'tor // // KTextEditor::DocumentPrivate Constructor // KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode, bool bReadOnly, QWidget *parentWidget, QObject *parent) : KTextEditor::Document(this, parent) , m_bSingleViewMode(bSingleViewMode) , m_bReadOnly(bReadOnly) , m_undoManager(new KateUndoManager(this)) , m_buffer(new KateBuffer(this)) , m_indenter(new KateAutoIndent(this)) , m_docName(QStringLiteral("need init")) , m_fileType(QStringLiteral("Normal")) , m_config(new KateDocumentConfig(this)) { /** * no plugins from kparts here */ setPluginLoadingMode(DoNotLoadPlugins); /** * pass on our component data, do this after plugin loading is off */ setComponentData(KTextEditor::EditorPrivate::self()->aboutData()); /** * avoid spamming plasma and other window managers with progress dialogs * we show such stuff inline in the views! */ setProgressInfoEnabled(false); // register doc at factory KTextEditor::EditorPrivate::self()->registerDocument(this); // normal hl m_buffer->setHighlight(0); // swap file m_swapfile = (config()->swapFileMode() == KateDocumentConfig::DisableSwapFile) ? nullptr : new Kate::SwapFile(this); // some nice signals from the buffer connect(m_buffer, SIGNAL(tagLines(int, int)), this, SLOT(tagLines(int, int))); // if the user changes the highlight with the dialog, notify the doc connect(KateHlManager::self(), SIGNAL(changed()), SLOT(internalHlChanged())); // signals for mod on hd connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(dirty(QString)), this, SLOT(slotModOnHdDirty(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(created(QString)), this, SLOT(slotModOnHdCreated(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(deleted(QString)), this, SLOT(slotModOnHdDeleted(QString))); /** * singleshot timer to handle updates of mod on hd state delayed */ m_modOnHdTimer.setSingleShot(true); m_modOnHdTimer.setInterval(200); connect(&m_modOnHdTimer, SIGNAL(timeout()), this, SLOT(slotDelayedHandleModOnHd())); // Setup auto reload stuff m_autoReloadMode = new KToggleAction(i18n("Auto Reload Document"), this); m_autoReloadMode->setWhatsThis(i18n("Automatic reload the document when it was changed on disk")); connect(m_autoReloadMode, &KToggleAction::triggered, this, &DocumentPrivate::autoReloadToggled); // Prepare some reload amok protector... m_autoReloadThrottle.setSingleShot(true); //...but keep the value small in unit tests m_autoReloadThrottle.setInterval(KTextEditor::EditorPrivate::self()->unitTestMode() ? 50 : 3000); connect(&m_autoReloadThrottle, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); /** * load handling * this is needed to ensure we signal the user if a file ist still loading * and to disallow him to edit in that time */ connect(this, SIGNAL(started(KIO::Job *)), this, SLOT(slotStarted(KIO::Job *))); connect(this, SIGNAL(completed()), this, SLOT(slotCompleted())); connect(this, SIGNAL(canceled(QString)), this, SLOT(slotCanceled())); connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(slotUrlChanged(QUrl))); // update doc name updateDocName(); // if single view mode, like in the konqui embedding, create a default view ;) // be lazy, only create it now, if any parentWidget is given, otherwise widget() // will create it on demand... if (m_bSingleViewMode && parentWidget) { KTextEditor::View *view = (KTextEditor::View *)createView(parentWidget); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); } connect(m_undoManager, SIGNAL(undoChanged()), this, SIGNAL(undoChanged())); connect(m_undoManager, SIGNAL(undoStart(KTextEditor::Document *)), this, SIGNAL(editingStarted(KTextEditor::Document *))); connect(m_undoManager, SIGNAL(undoEnd(KTextEditor::Document *)), this, SIGNAL(editingFinished(KTextEditor::Document *))); connect(m_undoManager, SIGNAL(redoStart(KTextEditor::Document *)), this, SIGNAL(editingStarted(KTextEditor::Document *))); connect(m_undoManager, SIGNAL(redoEnd(KTextEditor::Document *)), this, SIGNAL(editingFinished(KTextEditor::Document *))); connect(this, SIGNAL(sigQueryClose(bool *, bool *)), this, SLOT(slotQueryClose_save(bool *, bool *))); connect(this, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearEditingPosStack())); onTheFlySpellCheckingEnabled(config()->onTheFlySpellCheck()); // make sure correct defaults are set (indenter, ...) updateConfig(); } // // KTextEditor::DocumentPrivate Destructor // KTextEditor::DocumentPrivate::~DocumentPrivate() { // delete pending mod-on-hd message, if applicable delete m_modOnHdHandler; /** * we are about to delete cursors/ranges/... */ emit aboutToDeleteMovingInterfaceContent(this); // kill it early, it has ranges! delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; clearDictionaryRanges(); // Tell the world that we're about to close (== destruct) // Apps must receive this in a direct signal-slot connection, and prevent // any further use of interfaces once they return. emit aboutToClose(this); // remove file from dirwatch deactivateDirWatch(); // thanks for offering, KPart, but we're already self-destructing setAutoDeleteWidget(false); setAutoDeletePart(false); // clean up remaining views qDeleteAll(m_views.keys()); m_views.clear(); // cu marks for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { delete i.value(); } m_marks.clear(); delete m_config; KTextEditor::EditorPrivate::self()->deregisterDocument(this); } // END void KTextEditor::DocumentPrivate::saveEditingPositions(const KTextEditor::Cursor &cursor) { if (m_editingStackPosition != m_editingStack.size() - 1) { m_editingStack.resize(m_editingStackPosition); } // try to be clever: reuse existing cursors if possible QSharedPointer mc; // we might pop last one: reuse that if (!m_editingStack.isEmpty() && cursor.line() == m_editingStack.top()->line()) { mc = m_editingStack.pop(); } // we might expire oldest one, reuse that one, if not already one there // we prefer the other one for reuse, as already on the right line aka in the right block! const int editingStackSizeLimit = 32; if (m_editingStack.size() >= editingStackSizeLimit) { if (mc) { m_editingStack.removeFirst(); } else { mc = m_editingStack.takeFirst(); } } // new cursor needed? or adjust existing one? if (mc) { mc->setPosition(cursor); } else { mc = QSharedPointer(newMovingCursor(cursor)); } // add new one as top of stack m_editingStack.push(mc); m_editingStackPosition = m_editingStack.size() - 1; } KTextEditor::Cursor KTextEditor::DocumentPrivate::lastEditingPosition(EditingPositionKind nextOrPrev, KTextEditor::Cursor currentCursor) { if (m_editingStack.isEmpty()) { return KTextEditor::Cursor::invalid(); } auto targetPos = m_editingStack.at(m_editingStackPosition)->toCursor(); if (targetPos == currentCursor) { if (nextOrPrev == Previous) { m_editingStackPosition--; } else { m_editingStackPosition++; } m_editingStackPosition = qBound(0, m_editingStackPosition, m_editingStack.size() - 1); } return m_editingStack.at(m_editingStackPosition)->toCursor(); } void KTextEditor::DocumentPrivate::clearEditingPosStack() { m_editingStack.clear(); m_editingStackPosition = -1; } // on-demand view creation QWidget *KTextEditor::DocumentPrivate::widget() { // no singleViewMode -> no widget()... if (!singleViewMode()) { return nullptr; } // does a widget exist already? use it! if (KTextEditor::Document::widget()) { return KTextEditor::Document::widget(); } // create and return one... KTextEditor::View *view = (KTextEditor::View *)createView(nullptr); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); return view; } // BEGIN KTextEditor::Document stuff KTextEditor::View *KTextEditor::DocumentPrivate::createView(QWidget *parent, KTextEditor::MainWindow *mainWindow) { KTextEditor::ViewPrivate *newView = new KTextEditor::ViewPrivate(this, parent, mainWindow); if (m_fileChangedDialogsActivated) { connect(newView, SIGNAL(focusIn(KTextEditor::View *)), this, SLOT(slotModifiedOnDisk())); } emit viewCreated(this, newView); // post existing messages to the new view, if no specific view is given const auto keys = m_messageHash.keys(); for (KTextEditor::Message *message : keys) { if (!message->view()) { newView->postMessage(message, m_messageHash[message]); } } return newView; } KTextEditor::Range KTextEditor::DocumentPrivate::rangeOnLine(KTextEditor::Range range, int line) const { const int col1 = toVirtualColumn(range.start()); const int col2 = toVirtualColumn(range.end()); return KTextEditor::Range(line, fromVirtualColumn(line, col1), line, fromVirtualColumn(line, col2)); } // BEGIN KTextEditor::EditInterface stuff bool KTextEditor::DocumentPrivate::isEditingTransactionRunning() const { return editSessionNumber > 0; } QString KTextEditor::DocumentPrivate::text() const { return m_buffer->text(); } QString KTextEditor::DocumentPrivate::text(const KTextEditor::Range &range, bool blockwise) const { if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return QString(); } QString s; if (range.start().line() == range.end().line()) { if (range.start().column() > range.end().column()) { return QString(); } Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return QString(); } return textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { s.append(textLine->string(range.start().column(), textLine->length() - range.start().column())); } else if (i == range.end().line()) { s.append(textLine->string(0, range.end().column())); } else { s.append(textLine->string()); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); s.append(textLine->string(subRange.start().column(), subRange.columnWidth())); } if (i < range.end().line()) { s.append(QLatin1Char('\n')); } } } return s; } QChar KTextEditor::DocumentPrivate::characterAt(const KTextEditor::Cursor &position) const { Kate::TextLine textLine = m_buffer->plainLine(position.line()); if (!textLine) { return QChar(); } return textLine->at(position.column()); } QString KTextEditor::DocumentPrivate::wordAt(const KTextEditor::Cursor &cursor) const { return text(wordRangeAt(cursor)); } KTextEditor::Range KTextEditor::DocumentPrivate::wordRangeAt(const KTextEditor::Cursor &cursor) const { // get text line const int line = cursor.line(); Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { return KTextEditor::Range::invalid(); } // make sure the cursor is const int lineLenth = textLine->length(); if (cursor.column() > lineLenth) { return KTextEditor::Range::invalid(); } int start = cursor.column(); int end = start; while (start > 0 && highlight()->isInWord(textLine->at(start - 1), textLine->attribute(start - 1))) { start--; } while (end < lineLenth && highlight()->isInWord(textLine->at(end), textLine->attribute(end))) { end++; } return KTextEditor::Range(line, start, line, end); } bool KTextEditor::DocumentPrivate::isValidTextPosition(const KTextEditor::Cursor &cursor) const { const int ln = cursor.line(); const int col = cursor.column(); // cursor in document range? if (ln < 0 || col < 0 || ln >= lines() || col > lineLength(ln)) { return false; } const QString str = line(ln); Q_ASSERT(str.length() >= col); // cursor at end of line? const int len = lineLength(ln); if (col == 0 || col == len) { return true; } // cursor in the middle of a valid utf32-surrogate? return (!str.at(col).isLowSurrogate()) || (!str.at(col - 1).isHighSurrogate()); } QStringList KTextEditor::DocumentPrivate::textLines(const KTextEditor::Range &range, bool blockwise) const { QStringList ret; if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return ret; } if (blockwise && (range.start().column() > range.end().column())) { return ret; } if (range.start().line() == range.end().line()) { Q_ASSERT(range.start() <= range.end()); Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return ret; } ret << textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { ret << textLine->string(range.start().column(), textLine->length() - range.start().column()); } else if (i == range.end().line()) { ret << textLine->string(0, range.end().column()); } else { ret << textLine->string(); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); ret << textLine->string(subRange.start().column(), subRange.columnWidth()); } } } return ret; } QString KTextEditor::DocumentPrivate::line(int line) const { Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return QString(); } return l->string(); } bool KTextEditor::DocumentPrivate::setText(const QString &s) { if (!isReadWrite()) { return false; } QList msave; msave.reserve(m_marks.size()); for (KTextEditor::Mark *mark : qAsConst(m_marks)) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor(), s); editEnd(); for (KTextEditor::Mark mark : qAsConst(msave)) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::setText(const QStringList &text) { if (!isReadWrite()) { return false; } QList msave; msave.reserve(m_marks.size()); for (KTextEditor::Mark *mark : qAsConst(m_marks)) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor::start(), text); editEnd(); for (KTextEditor::Mark mark : qAsConst(msave)) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::clear() { if (!isReadWrite()) { return false; } for (auto view : qAsConst(m_views)) { view->clear(); view->tagAll(); view->update(); } clearMarks(); emit aboutToInvalidateMovingInterfaceContent(this); m_buffer->invalidateRanges(); emit aboutToRemoveText(documentRange()); return editRemoveLines(0, lastLine()); } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QString &text, bool block) { if (!isReadWrite()) { return false; } if (text.isEmpty()) { return true; } editStart(); int currentLine = position.line(); int currentLineStart = 0; const int totalLength = text.length(); int insertColumn = position.column(); // pad with empty lines, if insert position is after last line if (position.line() > lines()) { int line = lines(); while (line <= position.line()) { editInsertLine(line, QString()); line++; } } // compute expanded column for block mode int positionColumnExpanded = insertColumn; const int tabWidth = config()->tabWidth(); if (block) { if (auto l = plainKateTextLine(currentLine)) { positionColumnExpanded = l->toVirtualColumn(insertColumn, tabWidth); } } int pos = 0; for (; pos < totalLength; pos++) { const QChar &ch = text.at(pos); if (ch == QLatin1Char('\n')) { // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } if (!block) { editWrapLine(currentLine, insertColumn + pos - currentLineStart); insertColumn = 0; } currentLine++; if (block) { auto l = plainKateTextLine(currentLine); if (currentLine == lastLine() + 1) { editInsertLine(currentLine, QString()); } insertColumn = positionColumnExpanded; if (l) { insertColumn = l->fromVirtualColumn(insertColumn, tabWidth); } } currentLineStart = pos + 1; } } // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QStringList &textLines, bool block) { if (!isReadWrite()) { return false; } // just reuse normal function return insertText(position, textLines.join(QLatin1Char('\n')), block); } bool KTextEditor::DocumentPrivate::removeText(const KTextEditor::Range &_range, bool block) { KTextEditor::Range range = _range; if (!isReadWrite()) { return false; } // Should now be impossible to trigger with the new Range class Q_ASSERT(range.start().line() <= range.end().line()); if (range.start().line() > lastLine()) { return false; } if (!block) { emit aboutToRemoveText(range); } editStart(); if (!block) { if (range.end().line() > lastLine()) { range.setEnd(KTextEditor::Cursor(lastLine() + 1, 0)); } if (range.onSingleLine()) { editRemoveText(range.start().line(), range.start().column(), range.columnWidth()); } else { int from = range.start().line(); int to = range.end().line(); // remove last line if (to <= lastLine()) { editRemoveText(to, 0, range.end().column()); } // editRemoveLines() will be called on first line (to remove bookmark) if (range.start().column() == 0 && from > 0) { --from; } // remove middle lines editRemoveLines(from + 1, to - 1); // remove first line if not already removed by editRemoveLines() if (range.start().column() > 0 || range.start().line() == 0) { editRemoveText(from, range.start().column(), m_buffer->plainLine(from)->length() - range.start().column()); editUnWrapLine(from); } } } // if ( ! block ) else { int startLine = qMax(0, range.start().line()); int vc1 = toVirtualColumn(range.start()); int vc2 = toVirtualColumn(range.end()); for (int line = qMin(range.end().line(), lastLine()); line >= startLine; --line) { int col1 = fromVirtualColumn(line, vc1); int col2 = fromVirtualColumn(line, vc2); editRemoveText(line, qMin(col1, col2), qAbs(col2 - col1)); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertLine(int l, const QString &str) { if (!isReadWrite()) { return false; } if (l < 0 || l > lines()) { return false; } return editInsertLine(l, str); } bool KTextEditor::DocumentPrivate::insertLines(int line, const QStringList &text) { if (!isReadWrite()) { return false; } if (line < 0 || line > lines()) { return false; } bool success = true; for (const QString &string : text) { success &= editInsertLine(line++, string); } return success; } bool KTextEditor::DocumentPrivate::removeLine(int line) { if (!isReadWrite()) { return false; } if (line < 0 || line > lastLine()) { return false; } return editRemoveLine(line); } int KTextEditor::DocumentPrivate::totalCharacters() const { int l = 0; for (int i = 0; i < m_buffer->count(); ++i) { Kate::TextLine line = m_buffer->plainLine(i); if (line) { l += line->length(); } } return l; } int KTextEditor::DocumentPrivate::lines() const { return m_buffer->count(); } int KTextEditor::DocumentPrivate::lineLength(int line) const { if (line < 0 || line > lastLine()) { return -1; } Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return -1; } return l->length(); } bool KTextEditor::DocumentPrivate::isLineModified(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified(); } bool KTextEditor::DocumentPrivate::isLineSaved(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsSavedOnDisk(); } bool KTextEditor::DocumentPrivate::isLineTouched(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified() || l->markedAsSavedOnDisk(); } // END // BEGIN KTextEditor::EditInterface internal stuff // // Starts an edit session with (or without) undo, update of view disabled during session // bool KTextEditor::DocumentPrivate::editStart() { editSessionNumber++; if (editSessionNumber > 1) { return false; } editIsRunning = true; // no last change cursor at start m_editLastChangeStartCursor = KTextEditor::Cursor::invalid(); m_undoManager->editStart(); for (auto view : qAsConst(m_views)) { view->editStart(); } m_buffer->editStart(); return true; } // // End edit session and update Views // bool KTextEditor::DocumentPrivate::editEnd() { if (editSessionNumber == 0) { Q_ASSERT(0); return false; } // wrap the new/changed text, if something really changed! if (m_buffer->editChanged() && (editSessionNumber == 1)) if (m_undoManager->isActive() && config()->wordWrap()) { wrapText(m_buffer->editTagStart(), m_buffer->editTagEnd()); } editSessionNumber--; if (editSessionNumber > 0) { return false; } // end buffer edit, will trigger hl update // this will cause some possible adjustment of tagline start/end m_buffer->editEnd(); m_undoManager->editEnd(); // edit end for all views !!!!!!!!! for (auto view : qAsConst(m_views)) { view->editEnd(m_buffer->editTagStart(), m_buffer->editTagEnd(), m_buffer->editTagFrom()); } if (m_buffer->editChanged()) { setModified(true); emit textChanged(this); } // remember last change position in the stack, if any // this avoid costly updates for longer editing transactions // before we did that on textInsert/Removed if (m_editLastChangeStartCursor.isValid()) saveEditingPositions(m_editLastChangeStartCursor); editIsRunning = false; return true; } void KTextEditor::DocumentPrivate::pushEditState() { editStateStack.push(editSessionNumber); } void KTextEditor::DocumentPrivate::popEditState() { if (editStateStack.isEmpty()) { return; } int count = editStateStack.pop() - editSessionNumber; while (count < 0) { ++count; editEnd(); } while (count > 0) { --count; editStart(); } } void KTextEditor::DocumentPrivate::inputMethodStart() { m_undoManager->inputMethodStart(); } void KTextEditor::DocumentPrivate::inputMethodEnd() { m_undoManager->inputMethodEnd(); } bool KTextEditor::DocumentPrivate::wrapText(int startLine, int endLine) { if (startLine < 0 || endLine < 0) { return false; } if (!isReadWrite()) { return false; } int col = config()->wordWrapAt(); if (col == 0) { return false; } editStart(); for (int line = startLine; (line <= endLine) && (line < lines()); line++) { Kate::TextLine l = kateTextLine(line); if (!l) { break; } // qCDebug(LOG_KTE) << "try wrap line: " << line; if (l->virtualLength(m_buffer->tabWidth()) > col) { Kate::TextLine nextl = kateTextLine(line + 1); // qCDebug(LOG_KTE) << "do wrap line: " << line; int eolPosition = l->length() - 1; // take tabs into account here, too int x = 0; const QString &t = l->string(); int z2 = 0; for (; z2 < l->length(); z2++) { static const QChar tabChar(QLatin1Char('\t')); if (t.at(z2) == tabChar) { x += m_buffer->tabWidth() - (x % m_buffer->tabWidth()); } else { x++; } if (x > col) { break; } } const int colInChars = qMin(z2, l->length() - 1); int searchStart = colInChars; // If where we are wrapping is an end of line and is a space we don't // want to wrap there if (searchStart == eolPosition && t.at(searchStart).isSpace()) { searchStart--; } // Scan backwards looking for a place to break the line // We are not interested in breaking at the first char // of the line (if it is a space), but we are at the second // anders: if we can't find a space, try breaking on a word // boundary, using KateHighlight::canBreakAt(). // This could be a priority (setting) in the hl/filetype/document int z = -1; int nw = -1; // alternative position, a non word character for (z = searchStart; z >= 0; z--) { if (t.at(z).isSpace()) { break; } if ((nw < 0) && highlight()->canBreakAt(t.at(z), l->attribute(z))) { nw = z; } } if (z >= 0) { // So why don't we just remove the trailing space right away? // Well, the (view's) cursor may be directly in front of that space // (user typing text before the last word on the line), and if that // happens, the cursor would be moved to the next line, which is not // what we want (bug #106261) z++; } else { // There was no space to break at so break at a nonword character if // found, or at the wrapcolumn ( that needs be configurable ) // Don't try and add any white space for the break if ((nw >= 0) && nw < colInChars) { nw++; // break on the right side of the character } z = (nw >= 0) ? nw : colInChars; } if (nextl && !nextl->isAutoWrapped()) { editWrapLine(line, z, true); editMarkLineAutoWrapped(line + 1, true); endLine++; } else { if (nextl && (nextl->length() > 0) && !nextl->at(0).isSpace() && ((l->length() < 1) || !l->at(l->length() - 1).isSpace())) { editInsertText(line + 1, 0, QStringLiteral(" ")); } bool newLineAdded = false; editWrapLine(line, z, false, &newLineAdded); editMarkLineAutoWrapped(line + 1, true); endLine++; } } } editEnd(); 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 EDIT_DEBUG << "editInsertText" << line << col << s; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (s.isEmpty()) { return true; } editStart(); QString s2 = s; int col2 = col; if (col2 > l->length()) { s2 = QString(col2 - l->length(), QLatin1Char(' ')) + s; col2 = l->length(); } m_undoManager->slotTextInserted(line, col2, s2); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col2); // insert text into line m_buffer->insertText(m_editLastChangeStartCursor, s2); emit textInserted(this, KTextEditor::Range(line, col2, line, col2 + s2.length())); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveText(int line, int col, int len) { // verbose debug EDIT_DEBUG << "editRemoveText" << line << col << len; if (line < 0 || col < 0 || len < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (len == 0) { return true; } // wrong column if (col >= l->text().size()) { return false; } // don't try to remove what's not there len = qMin(len, l->text().size() - col); editStart(); QString oldText = l->string().mid(col, len); m_undoManager->slotTextRemoved(line, col, oldText); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); // remove text from line m_buffer->removeText(KTextEditor::Range(m_editLastChangeStartCursor, KTextEditor::Cursor(line, col + len))); emit textRemoved(this, KTextEditor::Range(line, col, line, col + len), oldText); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editMarkLineAutoWrapped(int line, bool autowrapped) { // verbose debug EDIT_DEBUG << "editMarkLineAutoWrapped" << line << autowrapped; if (line < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); m_undoManager->slotMarkLineAutoWrapped(line, autowrapped); l->setAutoWrapped(autowrapped); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editWrapLine(int line, int col, bool newLine, bool *newLineAdded) { // verbose debug EDIT_DEBUG << "editWrapLine" << line << col << newLine; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); Kate::TextLine nextLine = kateTextLine(line + 1); const int length = l->length(); m_undoManager->slotLineWrapped(line, col, length - col, (!nextLine || newLine)); if (!nextLine || newLine) { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { if ((col == 0) || (i.value()->line > line)) { list.append(i.value()); } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // yes, we added a new line ! if (newLineAdded) { (*newLineAdded) = true; } } else { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); m_buffer->unwrapLine(line + 2); // no, no new line added ! if (newLineAdded) { (*newLineAdded) = false; } } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textInserted(this, KTextEditor::Range(line, col, line + 1, 0)); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editUnWrapLine(int line, bool removeLine, int length) { // verbose debug EDIT_DEBUG << "editUnWrapLine" << line << removeLine << length; if (line < 0 || length < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); Kate::TextLine nextLine = kateTextLine(line + 1); if (!l || !nextLine) { return false; } editStart(); int col = l->length(); m_undoManager->slotLineUnWrapped(line, col, length, removeLine); if (removeLine) { m_buffer->unwrapLine(line + 1); } else { m_buffer->wrapLine(KTextEditor::Cursor(line + 1, length)); m_buffer->unwrapLine(line + 1); } QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line + 1) { list.append(i.value()); } if (i.value()->line == line + 1) { KTextEditor::Mark *mark = m_marks.take(line); if (mark) { i.value()->type |= mark->type; } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line--; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textRemoved(this, KTextEditor::Range(line, col, line + 1, 0), QStringLiteral("\n")); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertLine(int line, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertLine" << line << s; if (line < 0) { return false; } if (!isReadWrite()) { return false; } if (line > lines()) { return false; } editStart(); m_undoManager->slotLineInserted(line, s); // wrap line if (line > 0) { Kate::TextLine previousLine = m_buffer->line(line - 1); m_buffer->wrapLine(KTextEditor::Cursor(line - 1, previousLine->text().size())); } else { m_buffer->wrapLine(KTextEditor::Cursor(0, 0)); } // insert text m_buffer->insertText(KTextEditor::Cursor(line, 0), s); Kate::TextLine tl = m_buffer->line(line); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { list.append(i.value()); } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeInserted(line, 0, line, tl->length()); if (line) { Kate::TextLine prevLine = plainKateTextLine(line - 1); rangeInserted.setStart(KTextEditor::Cursor(line - 1, prevLine->length())); } else { rangeInserted.setEnd(KTextEditor::Cursor(line + 1, 0)); } // remember last change cursor m_editLastChangeStartCursor = rangeInserted.start(); emit textInserted(this, rangeInserted); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveLine(int line) { return editRemoveLines(line, line); } bool KTextEditor::DocumentPrivate::editRemoveLines(int from, int to) { // verbose debug EDIT_DEBUG << "editRemoveLines" << from << to; if (to < from || from < 0 || to > lastLine()) { return false; } if (!isReadWrite()) { return false; } if (lines() == 1) { return editRemoveText(0, 0, kateTextLine(0)->length()); } editStart(); QStringList oldText; /** * first remove text */ for (int line = to; line >= from; --line) { Kate::TextLine tl = m_buffer->line(line); oldText.prepend(this->line(line)); m_undoManager->slotLineRemoved(line, this->line(line)); m_buffer->removeText(KTextEditor::Range(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, tl->text().size()))); } /** * then collapse lines */ for (int line = to; line >= from; --line) { /** * unwrap all lines, prefer to unwrap line behind, skip to wrap line 0 */ if (line + 1 < m_buffer->lines()) { m_buffer->unwrapLine(line + 1); } else if (line) { m_buffer->unwrapLine(line); } } QList rmark; QList list; for (KTextEditor::Mark *mark : qAsConst(m_marks)) { int line = mark->line; if (line > to) { list << line; } else if (line >= from) { rmark << line; } } for (int line : qAsConst(rmark)) { delete m_marks.take(line); } for (int line : qAsConst(list)) { KTextEditor::Mark *mark = m_marks.take(line); mark->line -= to - from + 1; m_marks.insert(mark->line, mark); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeRemoved(from, 0, to + 1, 0); if (to == lastLine() + to - from + 1) { rangeRemoved.setEnd(KTextEditor::Cursor(to, oldText.last().length())); if (from > 0) { Kate::TextLine prevLine = plainKateTextLine(from - 1); rangeRemoved.setStart(KTextEditor::Cursor(from - 1, prevLine->length())); } } // remember last change cursor m_editLastChangeStartCursor = rangeRemoved.start(); emit textRemoved(this, rangeRemoved, oldText.join(QLatin1Char('\n')) + QLatin1Char('\n')); editEnd(); return true; } // END // BEGIN KTextEditor::UndoInterface stuff uint KTextEditor::DocumentPrivate::undoCount() const { return m_undoManager->undoCount(); } uint KTextEditor::DocumentPrivate::redoCount() const { return m_undoManager->redoCount(); } void KTextEditor::DocumentPrivate::undo() { m_undoManager->undo(); } void KTextEditor::DocumentPrivate::redo() { m_undoManager->redo(); } // END // BEGIN KTextEditor::SearchInterface stuff QVector KTextEditor::DocumentPrivate::searchText(const KTextEditor::Range &range, const QString &pattern, const KTextEditor::SearchOptions options) const { const bool escapeSequences = options.testFlag(KTextEditor::EscapeSequences); const bool regexMode = options.testFlag(KTextEditor::Regex); const bool backwards = options.testFlag(KTextEditor::Backwards); const bool wholeWords = options.testFlag(KTextEditor::WholeWords); const Qt::CaseSensitivity caseSensitivity = options.testFlag(KTextEditor::CaseInsensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive; if (regexMode) { // regexp search // escape sequences are supported by definition KateRegExpSearch searcher(this, caseSensitivity); return searcher.search(pattern, range, backwards); } if (escapeSequences) { // escaped search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(KateRegExpSearch::escapePlaintext(pattern), range, backwards); QVector result; result.append(match); return result; } // plaintext search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(pattern, range, backwards); QVector result; result.append(match); return result; } // END QWidget *KTextEditor::DocumentPrivate::dialogParent() { QWidget *w = widget(); if (!w) { w = activeView(); if (!w) { w = QApplication::activeWindow(); } } return w; } // BEGIN KTextEditor::HighlightingInterface stuff bool KTextEditor::DocumentPrivate::setMode(const QString &name) { updateFileType(name); return true; } KTextEditor::DefaultStyle KTextEditor::DocumentPrivate::defaultStyleAt(const KTextEditor::Cursor &position) const { // TODO, FIXME KDE5: in surrogate, use 2 bytes before if (!isValidTextPosition(position)) { return dsNormal; } int ds = const_cast(this)->defStyleNum(position.line(), position.column()); if (ds < 0 || ds > static_cast(dsError)) { return dsNormal; } return static_cast(ds); } QString KTextEditor::DocumentPrivate::mode() const { return m_fileType; } QStringList KTextEditor::DocumentPrivate::modes() const { QStringList m; const QList &modeList = KTextEditor::EditorPrivate::self()->modeManager()->list(); m.reserve(modeList.size()); for (KateFileType *type : modeList) { m << type->name; } return m; } bool KTextEditor::DocumentPrivate::setHighlightingMode(const QString &name) { int mode = KateHlManager::self()->nameFind(name); if (mode == -1) { return false; } m_buffer->setHighlight(mode); return true; } QString KTextEditor::DocumentPrivate::highlightingMode() const { return highlight()->name(); } QStringList KTextEditor::DocumentPrivate::highlightingModes() const { QStringList hls; for (const auto &hl : KateHlManager::self()->modeList()) { hls << hl.name(); } return hls; } QString KTextEditor::DocumentPrivate::highlightingModeSection(int index) const { return KateHlManager::self()->modeList().at(index).section(); } QString KTextEditor::DocumentPrivate::modeSection(int index) const { return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section; } void KTextEditor::DocumentPrivate::bufferHlChanged() { // update all views makeAttribs(false); // deactivate indenter if necessary m_indenter->checkRequiredStyle(); emit highlightingModeChanged(this); } void KTextEditor::DocumentPrivate::setDontChangeHlOnSave() { m_hlSetByUser = true; } void KTextEditor::DocumentPrivate::bomSetByUser() { m_bomSetByUser = true; } // END // BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff void KTextEditor::DocumentPrivate::readSessionConfig(const KConfigGroup &kconfig, const QSet &flags) { if (!flags.contains(QStringLiteral("SkipEncoding"))) { // get the encoding QString tmpenc = kconfig.readEntry("Encoding"); if (!tmpenc.isEmpty() && (tmpenc != encoding())) { setEncoding(tmpenc); } } if (!flags.contains(QStringLiteral("SkipUrl"))) { // restore the url QUrl url(kconfig.readEntry("URL")); // open the file if url valid if (!url.isEmpty() && url.isValid()) { openUrl(url); } else { completed(); // perhaps this should be emitted at the end of this function } } else { completed(); // perhaps this should be emitted at the end of this function } if (!flags.contains(QStringLiteral("SkipMode"))) { // restore the filetype if (kconfig.hasKey("Mode")) { updateFileType(kconfig.readEntry("Mode", fileType())); // restore if set by user, too! m_fileTypeSetByUser = kconfig.readEntry("Mode Set By User", false); } } if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // restore the hl stuff if (kconfig.hasKey("Highlighting")) { const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting")); if (mode >= 0) { m_buffer->setHighlight(mode); // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save m_hlSetByUser = kconfig.readEntry("Highlighting Set By User", false); } } } // indent mode config()->setIndentationMode(kconfig.readEntry("Indentation Mode", config()->indentationMode())); // Restore Bookmarks const QList marks = kconfig.readEntry("Bookmarks", QList()); for (int i = 0; i < marks.count(); i++) { addMark(marks.at(i), KTextEditor::DocumentPrivate::markType01); } } void KTextEditor::DocumentPrivate::writeSessionConfig(KConfigGroup &kconfig, const QSet &flags) { if (this->url().isLocalFile()) { const QString path = this->url().toLocalFile(); if (path.startsWith(QDir::tempPath())) { return; // inside tmp resource, do not save } } if (!flags.contains(QStringLiteral("SkipUrl"))) { // save url kconfig.writeEntry("URL", this->url().toString()); } if (!flags.contains(QStringLiteral("SkipEncoding"))) { // save encoding kconfig.writeEntry("Encoding", encoding()); } if (!flags.contains(QStringLiteral("SkipMode"))) { // save file type kconfig.writeEntry("Mode", m_fileType); // save if set by user, too! kconfig.writeEntry("Mode Set By User", m_fileTypeSetByUser); } if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // save hl kconfig.writeEntry("Highlighting", highlight()->name()); // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser); } // indent mode kconfig.writeEntry("Indentation Mode", config()->indentationMode()); // Save Bookmarks QList marks; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) if (i.value()->type & KTextEditor::MarkInterface::markType01) { marks << i.value()->line; } kconfig.writeEntry("Bookmarks", marks); } // END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff uint KTextEditor::DocumentPrivate::mark(int line) { KTextEditor::Mark *m = m_marks.value(line); if (!m) { return 0; } return m->type; } void KTextEditor::DocumentPrivate::setMark(int line, uint markType) { clearMark(line); addMark(line, markType); } void KTextEditor::DocumentPrivate::clearMark(int line) { if (line < 0 || line > lastLine()) { return; } if (!m_marks.value(line)) { return; } KTextEditor::Mark *mark = m_marks.take(line); emit markChanged(this, *mark, MarkRemoved); emit marksChanged(this); delete mark; tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::addMark(int line, uint markType) { KTextEditor::Mark *mark; if (line < 0 || line > lastLine()) { return; } if (markType == 0) { return; } if ((mark = m_marks.value(line))) { // Remove bits already set markType &= ~mark->type; if (markType == 0) { return; } // Add bits mark->type |= markType; } else { mark = new KTextEditor::Mark; mark->line = line; mark->type = markType; m_marks.insert(line, mark); } // Emit with a mark having only the types added. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkAdded); emit marksChanged(this); tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::removeMark(int line, uint markType) { if (line < 0 || line > lastLine()) { return; } KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } // Remove bits not set markType &= mark->type; if (markType == 0) { return; } // Subtract bits mark->type &= ~markType; // Emit with a mark having only the types removed. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkRemoved); if (mark->type == 0) { m_marks.remove(line); delete mark; } emit marksChanged(this); tagLines(line, line); repaintViews(true); } const QHash &KTextEditor::DocumentPrivate::marks() { return m_marks; } void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position) { KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } bool handled = false; emit markToolTipRequested(this, *mark, position, handled); } bool KTextEditor::DocumentPrivate::handleMarkClick(int line) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markClicked(this, KTextEditor::Mark {line, 0}, handled); } else { emit markClicked(this, *mark, handled); } return handled; } bool KTextEditor::DocumentPrivate::handleMarkContextMenu(int line, QPoint position) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markContextMenuRequested(this, KTextEditor::Mark {line, 0}, position, handled); } else { emit markContextMenuRequested(this, *mark, position, handled); } return handled; } void KTextEditor::DocumentPrivate::clearMarks() { while (!m_marks.isEmpty()) { QHash::iterator it = m_marks.begin(); KTextEditor::Mark mark = *it.value(); delete it.value(); m_marks.erase(it); emit markChanged(this, mark, MarkRemoved); tagLines(mark.line, mark.line); } m_marks.clear(); emit marksChanged(this); repaintViews(true); } void KTextEditor::DocumentPrivate::setMarkPixmap(MarkInterface::MarkTypes type, const QPixmap &pixmap) { m_markPixmaps.insert(type, pixmap); } void KTextEditor::DocumentPrivate::setMarkDescription(MarkInterface::MarkTypes type, const QString &description) { m_markDescriptions.insert(type, description); } QPixmap KTextEditor::DocumentPrivate::markPixmap(MarkInterface::MarkTypes type) const { return m_markPixmaps.value(type, QPixmap()); } QColor KTextEditor::DocumentPrivate::markColor(MarkInterface::MarkTypes type) const { uint reserved = (0x1 << KTextEditor::MarkInterface::reservedMarkersCount()) - 1; if ((uint)type >= (uint)markType01 && (uint)type <= reserved) { return KateRendererConfig::global()->lineMarkerColor(type); } else { return QColor(); } } QString KTextEditor::DocumentPrivate::markDescription(MarkInterface::MarkTypes type) const { return m_markDescriptions.value(type, QString()); } void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask) { m_editableMarks = markMask; } uint KTextEditor::DocumentPrivate::editableMarks() const { return m_editableMarks; } // END // BEGIN KTextEditor::PrintInterface stuff bool KTextEditor::DocumentPrivate::print() { return KatePrinter::print(this); } void KTextEditor::DocumentPrivate::printPreview() { KatePrinter::printPreview(this); } // END KTextEditor::PrintInterface stuff // BEGIN KTextEditor::DocumentInfoInterface (### unfinished) QString KTextEditor::DocumentPrivate::mimeType() { /** * collect first 4k of text * only heuristic */ QByteArray buf; for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) { buf.append(line(i).toUtf8()); buf.append('\n'); } // use path of url, too, if set if (!url().path().isEmpty()) { return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name(); } // else only use the content return QMimeDatabase().mimeTypeForData(buf).name(); } // END KTextEditor::DocumentInfoInterface // BEGIN: error void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess() { QPointer message = new KTextEditor::Message( i18n("The file %1 could not be loaded, as it was not possible to read from it.
Check if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)), KTextEditor::Message::Error); message->setWordWrap(true); QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"), nullptr); connect(tryAgainAction, SIGNAL(triggered()), SLOT(documentReload()), Qt::QueuedConnection); QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); // add try again and close actions message->addAction(tryAgainAction); message->addAction(closeAction); // finally post message postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 could not be loaded, as it was not possible to read from it.\n\nCheck if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)); } // END: error void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride() { // raise line length limit to the next power of 2 const int longestLine = m_buffer->longestLineLoaded(); int newLimit = pow(2, ceil(log2(longestLine))); if (newLimit <= longestLine) { newLimit *= 2; } // do the raise config()->setLineLengthLimit(newLimit); // just reload m_buffer->clear(); openFile(); if (!m_openingError) { setReadWrite(true); m_readWriteStateBeforeLoading = true; } } int KTextEditor::DocumentPrivate::lineLengthLimit() const { return config()->lineLengthLimit(); } // BEGIN KParts::ReadWrite stuff bool KTextEditor::DocumentPrivate::openFile() { /** * we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // no open errors until now... m_openingError = false; m_openingErrorMessage.clear(); // add new m_file to dirwatch activateDirWatch(); // remember current encoding QString currentEncoding = encoding(); // // mime type magic to get encoding right // QString mimeType = arguments().mimeType(); int pos = mimeType.indexOf(QLatin1Char(';')); if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) { setEncoding(mimeType.mid(pos + 1)); } // update file type, we do this here PRE-LOAD, therefore pass file name for reading from updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath())); // read dir config (if possible and wanted) // do this PRE-LOAD to get encoding info! readDirConfig(); // perhaps we need to re-set again the user encoding if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) { setEncoding(currentEncoding); } bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload)); // // yeah, success // read variables // if (success) { readVariables(); } // // update views // for (auto view : qAsConst(m_views)) { // This is needed here because inserting the text moves the view's start position (it is a MovingCursor) view->setCursorPosition(KTextEditor::Cursor()); view->updateView(true); } // Inform that the text has changed (required as we're not inside the usual editStart/End stuff) emit textChanged(this); emit loaded(this); // // to houston, we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // // display errors // if (!success) { showAndSetOpeningErrorAccess(); } // warn: broken encoding if (m_buffer->brokenEncoding()) { // this file can't be saved again without killing it setReadWrite(false); m_readWriteStateBeforeLoading = false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened with %2 encoding but contained invalid characters.
" "It is set to read-only mode, as saving might destroy its content.
" "Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())), KTextEditor::Message::Warning); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n( "The file %1 was opened with %2 encoding but contained invalid characters." " It is set to read-only mode, as saving might destroy its content." " Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())); } // warn: too long lines if (m_buffer->tooLongLinesWrapped()) { // this file can't be saved again without modifications setReadWrite(false); m_readWriteStateBeforeLoading = false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(), m_buffer->longestLineLoaded()), KTextEditor::Message::Warning); QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message); connect(increaseAndReload, SIGNAL(triggered()), this, SLOT(openWithLineLengthLimitOverride())); message->addAction(increaseAndReload, true); message->addAction(new QAction(i18n("Close"), message), true); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n( "The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(), m_buffer->longestLineLoaded()); } // // return the success // return success; } bool KTextEditor::DocumentPrivate::saveFile() { // delete pending mod-on-hd message if applicable. delete m_modOnHdHandler; // some warnings, if file was changed by the outside! if (!url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { QString str = reasonedMOHString() + QLatin1String("\n\n"); if (!isModified()) { if (KMessageBox::warningContinueCancel( dialogParent(), str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."), i18n("Trying to Save Unmodified File"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } else { if (KMessageBox::warningContinueCancel(dialogParent(), str + i18n("Do you really want to save this file? Both your open file and the file on disk were changed. There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } } } // // can we encode it if we want to save it ? // if (!m_buffer->canEncode() && (KMessageBox::warningContinueCancel(dialogParent(), i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save it? There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue)) { return false; } /** * create a backup file or abort if that fails! * if no backup file wanted, this routine will just return true */ if (!createBackupFile()) return false; // update file type, pass no file path, read file type content from this document QString oldPath = m_dirWatchFile; // only update file type if path has changed so that variables are not overridden on normal save if (oldPath != localFilePath()) { updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString())); if (url().isLocalFile()) { // if file is local then read dir config for new path readDirConfig(); } } // read our vars readVariables(); // remove file from dirwatch deactivateDirWatch(); // remove all trailing spaces in the document (as edit actions) // NOTE: we need this as edit actions, since otherwise the edit actions // in the swap file recovery may happen at invalid cursor positions removeTrailingSpaces(); // // try to save // if (!m_buffer->saveFile(localFilePath())) { // add m_file again to dirwatch activateDirWatch(oldPath); KMessageBox::error(dialogParent(), i18n("The document could not be saved, as it was not possible to write to %1.\nCheck that you have write access to this file or that enough disk space is available.\nThe original file may be lost or damaged. " "Don't quit the application until the file is successfully written.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // update the checksum createDigest(); // add m_file again to dirwatch activateDirWatch(); // // we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // (dominik) mark last undo group as not mergeable, otherwise the next // edit action might be merged and undo will never stop at the saved state m_undoManager->undoSafePoint(); m_undoManager->updateLineModifications(); // // return success // return true; } bool KTextEditor::DocumentPrivate::createBackupFile() { /** * backup for local or remote files wanted? */ const bool backupLocalFiles = config()->backupOnSaveLocal(); const bool backupRemoteFiles = config()->backupOnSaveRemote(); /** * early out, before mount check: backup wanted at all? * => if not, all fine, just return */ if (!backupLocalFiles && !backupRemoteFiles) { return true; } /** * decide if we need backup based on locality * skip that, if we always want backups, as currentMountPoints is not that fast */ QUrl u(url()); bool needBackup = backupLocalFiles && backupRemoteFiles; if (!needBackup) { bool slowOrRemoteFile = !u.isLocalFile(); if (!slowOrRemoteFile) { // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs) // we have the early out above to skip this, if we want no backup, which is the default KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile()); slowOrRemoteFile = (mountPoint && mountPoint->probablySlow()); } needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles); } /** * no backup needed? be done */ if (!needBackup) { return true; } /** * else: try to backup */ const auto backupPrefix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupPrefix(), nullptr); const auto backupSuffix = KTextEditor::EditorPrivate::self()->variableExpansionManager()->expandText(config()->backupSuffix(), nullptr); if (backupPrefix.isEmpty() && backupSuffix.isEmpty()) { // no sane backup possible return true; } if (backupPrefix.contains(QDir::separator())) { /** * replace complete path, as prefix is a path! */ u.setPath(backupPrefix + u.fileName() + backupSuffix); } else { /** * replace filename in url */ const QString fileName = u.fileName(); u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + backupPrefix + fileName + backupSuffix); } qCDebug(LOG_KTE) << "backup src file name: " << url(); qCDebug(LOG_KTE) << "backup dst file name: " << u; // handle the backup... bool backupSuccess = false; // local file mode, no kio if (u.isLocalFile()) { if (QFile::exists(url().toLocalFile())) { // first: check if backupFile is already there, if true, unlink it QFile backupFile(u.toLocalFile()); if (backupFile.exists()) { backupFile.remove(); } backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile()); } else { backupSuccess = true; } } else { // remote file mode, kio // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); if (statJob->exec()) { // do a evil copy which will overwrite target if possible KFileItem item(statJob->statResult(), url()); KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); backupSuccess = job->exec(); } else { backupSuccess = true; } } // backup has failed, ask user how to proceed if (!backupSuccess && (KMessageBox::warningContinueCancel(dialogParent(), i18n("For file %1 no backup copy could be created before saving." " If an error occurs while saving, you might lose the data of this file." " A reason could be that the media you write to is full or the directory of the file is read-only for you.", url().toDisplayString(QUrl::PreferLocalFile)), i18n("Failed to create backup copy."), KGuiItem(i18n("Try to Save Nevertheless")), KStandardGuiItem::cancel(), QStringLiteral("Backup Failed Warning")) != KMessageBox::Continue)) { return false; } return true; } void KTextEditor::DocumentPrivate::readDirConfig() { if (!url().isLocalFile()) { return; } /** * first search .kateconfig upwards * with recursion guard */ QSet seenDirectories; QDir dir(QFileInfo(localFilePath()).absolutePath()); while (!seenDirectories.contains(dir.absolutePath())) { /** * fill recursion guard */ seenDirectories.insert(dir.absolutePath()); // try to open config file in this dir QFile f(dir.absolutePath() + QLatin1String("/.kateconfig")); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); uint linesRead = 0; QString line = stream.readLine(); while ((linesRead < 32) && !line.isNull()) { readVariableLine(line); line = stream.readLine(); linesRead++; } return; } /** * else: cd up, if possible or abort */ if (!dir.cdUp()) { break; } } #if EDITORCONFIG_FOUND // if there wasn’t any .kateconfig file and KTextEditor was compiled with // EditorConfig support, try to load document config from a .editorconfig // file, if such is provided EditorConfig editorConfig(this); editorConfig.parse(); #endif } void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName) { QString fileToUse = useFileName; if (fileToUse.isEmpty()) { fileToUse = localFilePath(); } QFileInfo fileInfo = QFileInfo(fileToUse); if (fileInfo.isSymLink()) { // Monitor the actual data and not the symlink fileToUse = fileInfo.canonicalFilePath(); } // same file as we are monitoring, return if (fileToUse == m_dirWatchFile) { return; } // remove the old watched file deactivateDirWatch(); // add new file if needed if (url().isLocalFile() && !fileToUse.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->addFile(fileToUse); m_dirWatchFile = fileToUse; } } void KTextEditor::DocumentPrivate::deactivateDirWatch() { if (!m_dirWatchFile.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->removeFile(m_dirWatchFile); } m_dirWatchFile.clear(); } bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url) { if (!m_reloading) { // Reset filetype when opening url m_fileTypeSetByUser = false; } bool res = KTextEditor::Document::openUrl(normalizeUrl(url)); updateDocName(); return res; } bool KTextEditor::DocumentPrivate::closeUrl() { // // file mod on hd // if (!m_reloading && !url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { // make sure to not forget pending mod-on-hd handler delete m_modOnHdHandler; QWidget *parentWidget(dialogParent()); if (!(KMessageBox::warningContinueCancel(parentWidget, reasonedMOHString() + QLatin1String("\n\n") + i18n("Do you really want to continue to close this file? Data loss may occur."), i18n("Possible Data Loss"), KGuiItem(i18n("Close Nevertheless")), KStandardGuiItem::cancel(), QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason)) == KMessageBox::Continue)) { /** * reset reloading */ m_reloading = false; return false; } } } // // first call the normal kparts implementation // if (!KParts::ReadWritePart::closeUrl()) { /** * reset reloading */ m_reloading = false; return false; } // Tell the world that we're about to go ahead with the close if (!m_reloading) { emit aboutToClose(this); } /** * delete all KTE::Messages */ if (!m_messageHash.isEmpty()) { const auto keys = m_messageHash.keys(); for (KTextEditor::Message *message : keys) { delete message; } } /** * we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // remove file from dirwatch deactivateDirWatch(); // // empty url + fileName // setUrl(QUrl()); setLocalFilePath(QString()); // we are not modified if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // remove all marks clearMarks(); // clear the buffer m_buffer->clear(); // clear undo/redo history m_undoManager->clearUndo(); m_undoManager->clearRedo(); // no, we are no longer modified setModified(false); // we have no longer any hl m_buffer->setHighlight(0); // update all our views for (auto view : qAsConst(m_views)) { view->clearSelection(); // fix bug #118588 view->clear(); } // purge swap file if (m_swapfile) { m_swapfile->fileClosed(); } // success return true; } bool KTextEditor::DocumentPrivate::isDataRecoveryAvailable() const { return m_swapfile && m_swapfile->shouldRecover(); } void KTextEditor::DocumentPrivate::recoverData() { if (isDataRecoveryAvailable()) { m_swapfile->recover(); } } void KTextEditor::DocumentPrivate::discardDataRecovery() { if (isDataRecoveryAvailable()) { m_swapfile->discard(); } } void KTextEditor::DocumentPrivate::setReadWrite(bool rw) { if (isReadWrite() == rw) { return; } KParts::ReadWritePart::setReadWrite(rw); for (auto view : qAsConst(m_views)) { view->slotUpdateUndo(); view->slotReadWriteChanged(); } emit readWriteChanged(this); } void KTextEditor::DocumentPrivate::setModified(bool m) { if (isModified() != m) { KParts::ReadWritePart::setModified(m); for (auto view : qAsConst(m_views)) { view->slotUpdateUndo(); } emit modifiedChanged(this); } m_undoManager->setModified(m); } // END // BEGIN Kate specific stuff ;) void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate) { for (auto view : qAsConst(m_views)) { view->renderer()->updateAttributes(); } if (needInvalidate) { m_buffer->invalidateHighlighting(); } for (auto view : qAsConst(m_views)) { view->tagAll(); view->updateView(true); } } // the attributes of a hl have changed, update void KTextEditor::DocumentPrivate::internalHlChanged() { makeAttribs(); } void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view) { Q_ASSERT(!m_views.contains(view)); m_views.insert(view, static_cast(view)); m_viewsCache.append(view); // apply the view & renderer vars from the file type if (!m_fileType.isEmpty()) { readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, true); } // apply the view & renderer vars from the file readVariables(true); setActiveView(view); } void KTextEditor::DocumentPrivate::removeView(KTextEditor::View *view) { Q_ASSERT(m_views.contains(view)); m_views.remove(view); m_viewsCache.removeAll(view); if (activeView() == view) { setActiveView(nullptr); } } void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view) { if (m_activeView == view) { return; } m_activeView = static_cast(view); } bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view) { // do we own the given view? return (m_views.contains(view)); } int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->toVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor &cursor) const { return toVirtualColumn(cursor.line(), cursor.column()); } int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->fromVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor &cursor) const { return fromVirtualColumn(cursor.line(), cursor.column()); } void KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, QString chars) { // nop for empty chars if (chars.isEmpty()) { return; } // auto bracket handling QChar closingBracket; if (view->config()->autoBrackets()) { // Check if entered closing bracket is already balanced const QChar typedChar = chars.at(0); const QChar openBracket = matchingStartBracket(typedChar); if (!openBracket.isNull()) { KTextEditor::Cursor curPos = view->cursorPosition(); if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123 /*Which value may best?*/).isValid()) { // Do nothing view->cursorRight(); return; } } // for newly inserted text: remember if we should auto add some bracket if (chars.size() == 1) { // we inserted a bracket? => remember the matching closing one closingBracket = matchingEndBracket(typedChar); // closing bracket for the autobracket we inserted earlier? if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) { // do nothing m_currentAutobraceRange.reset(nullptr); view->cursorRight(); return; } } } // Treat some char also as "auto bracket" only when we have a selection if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) { const QChar typedChar = chars.at(0); if (view->config()->charsToEncloseSelection().contains(typedChar)) { // The unconditional mirroring cause no harm, but allows funny brackets closingBracket = typedChar.mirroredChar(); } } editStart(); /** * special handling if we want to add auto brackets to a selection */ if (view->selection() && !closingBracket.isNull()) { std::unique_ptr selectionRange(newMovingRange(view->selectionRange())); const int startLine = qMax(0, selectionRange->start().line()); const int endLine = qMin(selectionRange->end().line(), lastLine()); const bool blockMode = view->blockSelection() && (startLine != endLine); if (blockMode) { if (selectionRange->start().column() > selectionRange->end().column()) { // Selection was done from right->left, requires special setting to ensure the new // added brackets will not be part of the selection selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight); } // Add brackets to each line of the block const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column()); const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column()); const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn); for (int line = startLine; line <= endLine; ++line) { const KTextEditor::Range r(rangeOnLine(workingRange, line)); insertText(r.end(), QString(closingBracket)); view->slotTextInserted(view, r.end(), QString(closingBracket)); insertText(r.start(), chars); view->slotTextInserted(view, r.start(), chars); } } else { // No block, just add to start & end of selection insertText(selectionRange->end(), QString(closingBracket)); view->slotTextInserted(view, selectionRange->end(), QString(closingBracket)); insertText(selectionRange->start(), chars); view->slotTextInserted(view, selectionRange->start(), chars); } // Refresh selection view->setSelection(selectionRange->toRange()); view->setCursorPosition(selectionRange->end()); editEnd(); return; } /** * normal handling */ if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } const KTextEditor::Cursor oldCur(view->cursorPosition()); const bool multiLineBlockMode = view->blockSelection() && view->selection(); if (view->currentInputMode()->overwrite()) { // blockmode multiline selection case: remove chars in every line const KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line(); const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine; const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition()); for (int line = endLine; line >= startLine; --line) { Kate::TextLine textLine = m_buffer->plainLine(line); Q_ASSERT(textLine); const int column = fromVirtualColumn(line, virtualColumn); KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine->length() - column)); // replace mode needs to know what was removed so it can be restored with backspace if (oldCur.column() < lineLength(line)) { QChar removed = characterAt(KTextEditor::Cursor(line, column)); view->currentInputMode()->overwrittenChar(removed); } removeText(r); } } chars = eventuallyReplaceTabs(view->cursorPosition(), chars); 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); } else { insertText(view->cursorPosition(), chars); } /** * auto bracket handling for newly inserted text * we inserted a bracket? * => add the matching closing one to the view + input chars * try to preserve the cursor position */ bool skipAutobrace = closingBracket == QLatin1Char('\''); if (highlight() && skipAutobrace) { // skip adding ' in spellchecked areas, because those are text skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, view->cursorPosition() - Cursor {0, 1}); } const auto cursorPos(view->cursorPosition()); if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) { // skip auto quotes when these looks already balanced, bug 405089 Kate::TextLine textLine = m_buffer->plainLine(cursorPos.line()); // RegEx match quote, but not excaped quote, thanks to https://stackoverflow.com/a/11819111 const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?plainLine(cursorPos.line()); const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?document()->text({cursorPos, cursorPos + Cursor {0, 1}}).trimmed(); if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) { insertText(view->cursorPosition(), QString(closingBracket)); const auto insertedAt(view->cursorPosition()); view->setCursorPosition(cursorPos); m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor {0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand)); connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection); // add bracket to chars inserted! needed for correct signals + indent chars.append(closingBracket); } m_currentAutobraceClosingChar = closingBracket; } // end edit session here, to have updated HL in userTypedChar! editEnd(); // trigger indentation KTextEditor::Cursor b(view->cursorPosition()); m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1)); /** * inform the view about the original inserted chars */ view->slotTextInserted(view, oldCur, chars); } void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View *, const KTextEditor::Cursor &newPos) { if (m_currentAutobraceRange && !m_currentAutobraceRange->toRange().contains(newPos)) { m_currentAutobraceRange.clear(); } } void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent) { editStart(); if (!v->config()->persistentSelection() && v->selection()) { v->removeSelectedText(); v->clearSelection(); } // query cursor position KTextEditor::Cursor c = v->cursorPosition(); if (c.line() > lastLine()) { c.setLine(lastLine()); } if (c.line() < 0) { c.setLine(0); } int ln = c.line(); Kate::TextLine textLine = plainKateTextLine(ln); if (c.column() > textLine->length()) { c.setColumn(textLine->length()); } // first: wrap line editWrapLine(c.line(), c.column()); // end edit session here, to have updated HL in userTypedChar! editEnd(); // second: if "indent" is true, indent the new line, if needed... if (indent == KTextEditor::DocumentPrivate::Indent) { m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n')); } } void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor &cursor) { Kate::TextLine textLine = m_buffer->plainLine(cursor.line()); if (!textLine || (textLine->length() < 2)) { return; } uint col = cursor.column(); if (col > 0) { col--; } if ((textLine->length() - col) < 2) { return; } uint line = cursor.line(); QString s; // clever swap code if first character on the line swap right&left // otherwise left & right s.append(textLine->at(col + 1)); s.append(textLine->at(col)); // do the swap // do it right, never ever manipulate a textline editStart(); editRemoveText(line, col, 2); editInsertText(line, col, s); editEnd(); } void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character before vertical selection line by expanding the selection range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } uint col = qMax(c.column(), 0); uint line = qMax(c.line(), 0); if ((col == 0) && (line == 0)) { return; } const Kate::TextLine textLine = m_buffer->plainLine(line); // don't forget this check!!!! really!!!! if (!textLine) { return; } if (col > 0) { bool useNextBlock = false; if (config()->backspaceIndents()) { // backspace indents: erase to next indent position int colX = textLine->toVirtualColumn(col, config()->tabWidth()); int pos = textLine->firstChar(); if (pos > 0) { pos = textLine->toVirtualColumn(pos, config()->tabWidth()); } if (pos < 0 || pos >= (int)colX) { // only spaces on left side of cursor indent(KTextEditor::Range(line, 0, line, 0), -1); } else { useNextBlock = true; } } if (!config()->backspaceIndents() || useNextBlock) { KTextEditor::Cursor beginCursor(line, 0); KTextEditor::Cursor endCursor(line, col); if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior beginCursor.setColumn(col - 1); // move to left of surrogate pair if (!isValidTextPosition(beginCursor)) { Q_ASSERT(col >= 2); beginCursor.setColumn(col - 2); } } else { beginCursor.setColumn(view->textLayout(c)->previousCursorPosition(c.column())); } removeText(KTextEditor::Range(beginCursor, endCursor)); // in most cases cursor is moved by removeText, but we should do it manually // for past-end-of-line cursors in block mode view->setCursorPosition(beginCursor); } } else { // col == 0: wrap to previous line const Kate::TextLine textLine = m_buffer->plainLine(line - 1); if (line > 0 && textLine) { if (config()->wordWrap() && textLine->endsWith(QStringLiteral(" "))) { // gg: in hard wordwrap mode, backspace must also eat the trailing space removeText(KTextEditor::Range(line - 1, textLine->length() - 1, line, 0)); } else { removeText(KTextEditor::Range(line - 1, textLine->length(), line, 0)); } } } if (m_currentAutobraceRange) { const auto r = m_currentAutobraceRange->toRange(); if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) { // start parenthesis removed and range length is 1, remove end as well del(view, view->cursorPosition()); m_currentAutobraceRange.clear(); } } } void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character after vertical selection line by expanding the selection range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } if (c.column() < (int)m_buffer->plainLine(c.line())->length()) { KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column())); removeText(KTextEditor::Range(c, endCursor)); } else if (c.line() < lastLine()) { removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0)); } } void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text) { // nop if nothing to paste if (text.isEmpty()) { return; } // normalize line endings, to e.g. catch issues with \r\n in paste buffer // see bug 410951 QString s = text; s.replace(QRegularExpression(QStringLiteral("(\r\n|\r|\n)")), QStringLiteral("\n")); int lines = s.count(QLatin1Char('\n')); m_undoManager->undoSafePoint(); editStart(); KTextEditor::Cursor pos = view->cursorPosition(); if (!view->config()->persistentSelection() && view->selection()) { pos = view->selectionRange().start(); if (view->blockSelection()) { pos = rangeOnLine(view->selectionRange(), pos.line()).start(); if (lines == 0) { s += QLatin1Char('\n'); s = s.repeated(view->selectionRange().numberOfLines() + 1); s.chop(1); } } view->removeSelectedText(); } if (config()->ovr()) { const auto pasteLines = s.splitRef(QLatin1Char('\n')); if (!view->blockSelection()) { int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length(); removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn)); } else { int maxi = qMin(pos.line() + pasteLines.count(), this->lines()); for (int i = pos.line(); i < maxi; ++i) { int pasteLength = pasteLines.at(i - pos.line()).length(); removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i)))); } } } insertText(pos, s, view->blockSelection()); editEnd(); // move cursor right for block select, as the user is moved right internal // even in that case, but user expects other behavior in block selection // mode ! // just let cursor stay, that was it before I changed to moving ranges! if (view->blockSelection()) { view->setCursorPositionInternal(pos); } if (config()->indentPastedText()) { KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0)); m_indenter->indent(view, range); } if (!view->blockSelection()) { emit charactersSemiInteractivelyInserted(pos, s); } m_undoManager->undoSafePoint(); } void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change) { if (!isReadWrite()) { return; } editStart(); m_indenter->changeIndent(range, change); editEnd(); } void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { m_indenter->indent(view, range); } void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &) { if (!isReadWrite()) { return; } int lineLen = line(view->cursorPosition().line()).length(); KTextEditor::Cursor c = view->cursorPosition(); editStart(); if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) { KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1); // replace mode needs to know what was removed so it can be restored with backspace QChar removed = line(view->cursorPosition().line()).at(r.start().column()); view->currentInputMode()->overwrittenChar(removed); removeText(r); } c = view->cursorPosition(); editInsertText(c.line(), c.column(), QStringLiteral("\t")); editEnd(); } /* Remove a given string at the beginning of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->startsWith(str); if (!there) { cursor.setColumn(textline->firstChar()); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Remove a given string at the end of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->endsWith(str); if (there) { cursor.setColumn(textline->length() - str.length()); } else { cursor.setColumn(textline->lastChar() - str.length() + 1); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Replace tabs by spaces in the given string, if enabled. */ QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor &cursorPos, const QString &str) const { const bool replacetabs = config()->replaceTabsDyn(); if (!replacetabs) { return str; } const int indentWidth = config()->indentationWidth(); static const QLatin1Char tabChar('\t'); int column = cursorPos.column(); // The result will always be at least as long as the input QString result; result.reserve(str.size()); for (const QChar ch : str) { if (ch == tabChar) { // Insert only enough spaces to align to the next indentWidth column // This fixes bug #340212 int spacesToInsert = indentWidth - (column % indentWidth); result += QString(spacesToInsert, QLatin1Char(' ')); column += spacesToInsert; } else { // Just keep all other typed characters as-is result += ch; ++column; } } return result; } /* Add to the current line a comment line mark at the beginning. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib) { QString commentLineMark = highlight()->getCommentSingleLineStart(attrib); int pos = -1; if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::StartOfLine) { pos = 0; commentLineMark += QLatin1Char(' '); } else { const Kate::TextLine l = kateTextLine(line); pos = l->firstChar(); } if (pos >= 0) { insertText(KTextEditor::Cursor(line, pos), commentLineMark); } } /* Remove from the current line a comment line mark at the beginning if there is one. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); editStart(); // Try to remove the long comment mark first bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark)); editEnd(); return removed; } /* Add to the current line a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib) { const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' '); const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib); editStart(); // Add the start comment mark insertText(KTextEditor::Cursor(line, 0), startCommentMark); // Go to the end of the line const int col = m_buffer->plainLine(line)->length(); // Add the stop comment mark insertText(KTextEditor::Cursor(line, col), stopCommentMark); editEnd(); } /* Remove from the current line a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib) { const QString shortStartCommentMark = highlight()->getCommentStart(attrib); const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' '); const QString shortStopCommentMark = highlight()->getCommentEnd(attrib); const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark; editStart(); // Try to remove the long start comment mark first const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark)); // Try to remove the long stop comment mark first const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark)); editEnd(); return (removedStart || removedStop); } /* Add to the current selection a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); KTextEditor::Range range = view->selectionRange(); if ((range.end().column() == 0) && (range.end().line() > 0)) { range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1))); } editStart(); if (!view->blockSelection()) { insertText(range.end(), endComment); insertText(range.start(), startComment); } else { for (int line = range.start().line(); line <= range.end().line(); line++) { KTextEditor::Range subRange = rangeOnLine(range, line); insertText(subRange.end(), endComment); insertText(subRange.start(), startComment); } } editEnd(); // selection automatically updated (MovingRange) } /* Add to the current selection a comment line mark at the beginning of each line. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { // const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); // if end of selection is in column 0 in last line, omit the last line if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { // insertText (z, 0, commentLineMark); addStartLineCommentToSingleLine(z, attrib); } editEnd(); // selection automatically updated (MovingRange) } bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col) { for (; line < (int)m_buffer->count(); line++) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->nextNonSpaceChar(col); if (col != -1) { return true; // Next non-space char found } col = 0; } // No non-space char found line = -1; col = -1; return false; } bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col) { while (true) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->previousNonSpaceChar(col); if (col != -1) { return true; } if (line == 0) { return false; } --line; col = textLine->length(); } // No non-space char found line = -1; col = -1; return false; } /* Remove from the selection a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); int sl = qMax(0, view->selectionRange().start().line()); int el = qMin(view->selectionRange().end().line(), lastLine()); int sc = view->selectionRange().start().column(); int ec = view->selectionRange().end().column(); // The selection ends on the char before selectEnd if (ec != 0) { --ec; } else if (el > 0) { --el; ec = m_buffer->plainLine(el)->length() - 1; } const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/ bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl)->matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec) && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el)->matchesAt(ec - endCommentLen + 1, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1)); removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen)); editEnd(); // selection automatically updated (MovingRange) } return remove; } bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor &start, const KTextEditor::Cursor &end, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); const bool remove = m_buffer->plainLine(start.line())->matchesAt(start.column(), startComment) && m_buffer->plainLine(end.line())->matchesAt(end.column() - endCommentLen, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column())); removeText(KTextEditor::Range(start, startCommentLen)); editEnd(); } return remove; } /* Remove from the beginning of each line of the selection a start comment line mark. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } bool removed = false; editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { // Try to remove the long comment mark first removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed); } editEnd(); // selection automatically updated (MovingRange) return removed; } /* Comment or uncomment the selection or the current line if there is no selection. */ void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, int change) { // skip word wrap bug #105373 const bool skipWordWrap = wordWrap(); if (skipWordWrap) { setWordWrap(false); } bool hassel = v->selection(); int c = 0; if (hassel) { c = v->selectionRange().start().column(); } int startAttrib = 0; Kate::TextLine ln = kateTextLine(line); if (c < ln->length()) { startAttrib = ln->attribute(c); } else if (!ln->attributesList().isEmpty()) { startAttrib = ln->attributesList().back().attributeValue; } bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty()); bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty())); if (change > 0) { // comment if (!hassel) { if (hasStartLineCommentMark) { addStartLineCommentToSingleLine(line, startAttrib); } else if (hasStartStopCommentMark) { addStartStopCommentToSingleLine(line, startAttrib); } } else { // anders: prefer single line comment to avoid nesting probs // If the selection starts after first char in the first line // or ends before the last char of the last line, we may use // multiline comment markers. // TODO We should try to detect nesting. // - if selection ends at col 0, most likely she wanted that // line ignored const KTextEditor::Range sel = v->selectionRange(); if (hasStartStopCommentMark && (!hasStartLineCommentMark || ((sel.start().column() > m_buffer->plainLine(sel.start().line())->firstChar()) || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line())->length()))))) { addStartStopCommentToSelection(v, startAttrib); } else if (hasStartLineCommentMark) { addStartLineCommentToSelection(v, startAttrib); } } } else { // uncomment bool removed = false; if (!hassel) { removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib)) || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib)); } else { // anders: this seems like it will work with above changes :) removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(v, startAttrib)) || (hasStartLineCommentMark && removeStartLineCommentFromSelection(v, startAttrib)); } // recursive call for toggle comment if (!removed && change == 0) { comment(v, line, column, 1); } } if (skipWordWrap) { setWordWrap(true); // see begin of function ::comment (bug #105373) } } void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor &c, KTextEditor::DocumentPrivate::TextTransform t) { if (v->selection()) { editStart(); // cache the selection and cursor, so we can be sure to restore. KTextEditor::Range selection = v->selectionRange(); KTextEditor::Range range(selection.start(), 0); while (range.start().line() <= selection.end().line()) { int start = 0; int end = lineLength(range.start().line()); if (range.start().line() == selection.start().line() || v->blockSelection()) { start = selection.start().column(); } if (range.start().line() == selection.end().line() || v->blockSelection()) { end = selection.end().column(); } if (start > end) { int swapCol = start; start = end; end = swapCol; } range.setStart(KTextEditor::Cursor(range.start().line(), start)); range.setEnd(KTextEditor::Cursor(range.end().line(), end)); QString s = text(range); QString old = s; if (t == Uppercase) { s = s.toUpper(); } else if (t == Lowercase) { s = s.toLower(); } else { // Capitalize Kate::TextLine l = m_buffer->plainLine(range.start().line()); int p(0); while (p < s.length()) { // If bol or the character before is not in a word, up this one: // 1. if both start and p is 0, upper char. // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper // 3. if p-1 is not in a word, upper. if ((!range.start().column() && !p) || ((range.start().line() == selection.start().line() || v->blockSelection()) && !p && !highlight()->isInWord(l->at(range.start().column() - 1))) || (p && !highlight()->isInWord(s.at(p - 1)))) { s[p] = s.at(p).toUpper(); } p++; } } if (s != old) { removeText(range); insertText(range.start(), s); } range.setBothLines(range.start().line() + 1); } editEnd(); // restore selection & cursor v->setSelection(selection); v->setCursorPosition(c); } else { // no selection editStart(); // get cursor KTextEditor::Cursor cursor = c; QString old = text(KTextEditor::Range(cursor, 1)); QString s; switch (t) { - case Uppercase: - s = old.toUpper(); - break; - case Lowercase: - s = old.toLower(); - break; - case Capitalize: { - Kate::TextLine l = m_buffer->plainLine(cursor.line()); - while (cursor.column() > 0 && highlight()->isInWord(l->at(cursor.column() - 1), l->attribute(cursor.column() - 1))) { - cursor.setColumn(cursor.column() - 1); - } - old = text(KTextEditor::Range(cursor, 1)); - s = old.toUpper(); - } break; - default: - break; + case Uppercase: + s = old.toUpper(); + break; + case Lowercase: + s = old.toLower(); + break; + case Capitalize: { + Kate::TextLine l = m_buffer->plainLine(cursor.line()); + while (cursor.column() > 0 && highlight()->isInWord(l->at(cursor.column() - 1), l->attribute(cursor.column() - 1))) { + cursor.setColumn(cursor.column() - 1); + } + old = text(KTextEditor::Range(cursor, 1)); + s = old.toUpper(); + } break; + default: + break; } removeText(KTextEditor::Range(cursor, 1)); insertText(cursor, s); editEnd(); } } void KTextEditor::DocumentPrivate::joinLines(uint first, uint last) { // if ( first == last ) last += 1; editStart(); int line(first); while (first < last) { // Normalize the whitespace in the joined lines by making sure there's // always exactly one space between the joined lines // This cannot be done in editUnwrapLine, because we do NOT want this // behavior when deleting from the start of a line, just when explicitly // calling the join command Kate::TextLine l = kateTextLine(line); Kate::TextLine tl = kateTextLine(line + 1); if (!l || !tl) { editEnd(); return; } int pos = tl->firstChar(); if (pos >= 0) { if (pos != 0) { editRemoveText(line + 1, 0, pos); } if (!(l->length() == 0 || l->at(l->length() - 1).isSpace())) { editInsertText(line + 1, 0, QStringLiteral(" ")); } } else { // Just remove the whitespace and let Kate handle the rest editRemoveText(line + 1, 0, tl->length()); } editUnWrapLine(line); first++; } editEnd(); } void KTextEditor::DocumentPrivate::tagLines(int start, int end) { for (auto view : qAsConst(m_views)) { view->tagLines(start, end, true); } } void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty) { for (auto view : qAsConst(m_views)) { view->repaintText(paintOnlyDirty); } } /* Bracket matching uses the following algorithm: If in overwrite mode, match the bracket currently underneath the cursor. Otherwise, if the character to the left is a bracket, match it. Otherwise if the character to the right of the cursor is a bracket, match it. Otherwise, don't match anything. */ KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor &start, int maxLines) { if (maxLines < 0) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = m_buffer->plainLine(start.line()); if (!textLine) { return KTextEditor::Range::invalid(); } KTextEditor::Range range(start, start); const QChar right = textLine->at(range.start().column()); const QChar left = textLine->at(range.start().column() - 1); QChar bracket; if (config()->ovr()) { if (isBracket(right)) { bracket = right; } else { return KTextEditor::Range::invalid(); } } else if (isBracket(right)) { bracket = right; } else if (isBracket(left)) { range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); bracket = left; } else { return KTextEditor::Range::invalid(); } const QChar opposite = matchingBracket(bracket); if (opposite.isNull()) { return KTextEditor::Range::invalid(); } const int searchDir = isStartBracket(bracket) ? 1 : -1; uint nesting = 0; const int minLine = qMax(range.start().line() - maxLines, 0); const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line()); range.setEnd(range.start()); KTextEditor::DocumentCursor cursor(this); cursor.setPosition(range.start()); int validAttr = kateTextLine(cursor.line())->attribute(cursor.column()); while (cursor.line() >= minLine && cursor.line() <= maxLine) { if (!cursor.move(searchDir)) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = kateTextLine(cursor.line()); if (textLine->attribute(cursor.column()) == validAttr) { // Check for match QChar c = textLine->at(cursor.column()); if (c == opposite) { if (nesting == 0) { if (searchDir > 0) { // forward range.setEnd(cursor.toCursor()); } else { range.setStart(cursor.toCursor()); } return range; } nesting--; } else if (c == bracket) { nesting++; } } } return KTextEditor::Range::invalid(); } // helper: remove \r and \n from visible document name (bug #170876) inline static QString removeNewLines(const QString &str) { QString tmp(str); return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")).replace(QLatin1Char('\r'), QLatin1Char(' ')).replace(QLatin1Char('\n'), QLatin1Char(' ')); } void KTextEditor::DocumentPrivate::updateDocName() { // if the name is set, and starts with FILENAME, it should not be changed! if (!url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) { return; } int count = -1; const auto docs = KTextEditor::EditorPrivate::self()->kateDocuments(); for (KTextEditor::DocumentPrivate *doc : docs) { if ((doc != this) && (doc->url().fileName() == url().fileName())) if (doc->m_docNameNumber > count) { count = doc->m_docNameNumber; } } m_docNameNumber = count + 1; QString oldName = m_docName; m_docName = removeNewLines(url().fileName()); m_isUntitled = m_docName.isEmpty(); if (m_isUntitled) { m_docName = i18n("Untitled"); } if (m_docNameNumber > 0) { m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1); } /** * avoid to emit this, if name doesn't change! */ if (oldName != m_docName) { emit documentNameChanged(this); } } void KTextEditor::DocumentPrivate::slotModifiedOnDisk(KTextEditor::View * /*v*/) { if (url().isEmpty() || !m_modOnHd) { return; } if (!isModified() && isAutoReload()) { onModOnHdAutoReload(); return; } if (!m_fileChangedDialogsActivated || m_modOnHdHandler) { return; } // don't ask the user again and again the same thing if (m_modOnHdReason == m_prevModOnHdReason) { return; } m_prevModOnHdReason = m_modOnHdReason; m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString()); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore); } void KTextEditor::DocumentPrivate::onModOnHdSaveAs() { m_modOnHd = false; QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File"), url()); if (!res.isEmpty()) { if (!saveAs(res)) { KMessageBox::error(parentWidget, i18n("Save failed")); m_modOnHd = true; } else { delete m_modOnHdHandler; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); } } else { // the save as dialog was canceled, we are still modified on disk m_modOnHd = true; } } void KTextEditor::DocumentPrivate::onModOnHdClose() { // avoid prompt in closeUrl() m_fileChangedDialogsActivated = false; // close the file without prompt confirmation closeUrl(); // Useful for kate only closeDocumentInApplication(); } void KTextEditor::DocumentPrivate::onModOnHdReload() { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::autoReloadToggled(bool b) { m_autoReloadMode->setChecked(b); if (b) { connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } else { disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } } bool KTextEditor::DocumentPrivate::isAutoReload() { return m_autoReloadMode->isChecked(); } void KTextEditor::DocumentPrivate::delayAutoReload() { if (isAutoReload()) { m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdAutoReload() { if (m_modOnHdHandler) { delete m_modOnHdHandler; autoReloadToggled(true); } if (!isAutoReload()) { return; } if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdIgnore() { // ignore as long as m_prevModOnHdReason == m_modOnHdReason delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::setModifiedOnDisk(ModifiedOnDiskReason reason) { m_modOnHdReason = reason; m_modOnHd = (reason != OnDiskUnmodified); emit modifiedOnDisk(this, (reason != OnDiskUnmodified), reason); } class KateDocumentTmpMark { public: QString line; KTextEditor::Mark mark; }; void KTextEditor::DocumentPrivate::setModifiedOnDiskWarning(bool on) { m_fileChangedDialogsActivated = on; } bool KTextEditor::DocumentPrivate::documentReload() { if (url().isEmpty()) { return false; } // typically, the message for externally modified files is visible. Since it // does not make sense showing an additional dialog, just hide the message. delete m_modOnHdHandler; emit aboutToReload(this); QList tmp; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { KateDocumentTmpMark m; m.line = line(i.value()->line); m.mark = *i.value(); tmp.append(m); } const QString oldMode = mode(); const bool byUser = m_fileTypeSetByUser; const QString hl_mode = highlightingMode(); m_storedVariables.clear(); // save cursor positions for all views QHash cursorPositions; for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); cursorPositions.insert(v, v->cursorPosition()); } m_reloading = true; KTextEditor::DocumentPrivate::openUrl(url()); // reset some flags only valid for one reload! m_userSetEncodingForNextReload = false; // restore cursor positions for all views for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); setActiveView(v); v->setCursorPosition(cursorPositions.value(v)); if (v->isVisible()) { v->repaintText(false); } } for (int z = 0; z < tmp.size(); z++) { if (z < lines()) { if (line(tmp.at(z).mark.line) == tmp.at(z).line) { setMark(tmp.at(z).mark.line, tmp.at(z).mark.type); } } } if (byUser) { setMode(oldMode); } setHighlightingMode(hl_mode); emit reloaded(this); return true; } bool KTextEditor::DocumentPrivate::documentSave() { if (!url().isValid() || !isReadWrite()) { return documentSaveAs(); } return save(); } bool KTextEditor::DocumentPrivate::documentSaveAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding) { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } setEncoding(encoding); return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveCopyAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save Copy of File"), url()); if (saveUrl.isEmpty()) { return false; } QTemporaryFile file; if (!file.open()) { return false; } if (!m_buffer->saveFile(file.fileName())) { KMessageBox::error( dialogParent(), i18n("The document could not be saved, as it was not possible to write to %1.\n\nCheck that you have write access to this file or that enough disk space is available.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); int permissions = -1; if (statJob->exec()) { permissions = KFileItem(statJob->statResult(), url()).permissions(); } // KIO move, important: allow overwrite, we checked above! KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file.fileName()), saveUrl, permissions, KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); return job->exec(); } void KTextEditor::DocumentPrivate::setWordWrap(bool on) { config()->setWordWrap(on); } bool KTextEditor::DocumentPrivate::wordWrap() const { return config()->wordWrap(); } void KTextEditor::DocumentPrivate::setWordWrapAt(uint col) { config()->setWordWrapAt(col); } unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const { return config()->wordWrapAt(); } void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on) { config()->setPageUpDownMovesCursor(on); } bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const { return config()->pageUpDownMovesCursor(); } // END bool KTextEditor::DocumentPrivate::setEncoding(const QString &e) { return m_config->setEncoding(e); } QString KTextEditor::DocumentPrivate::encoding() const { return m_config->encoding(); } void KTextEditor::DocumentPrivate::updateConfig() { m_undoManager->updateConfig(); // switch indenter if needed and update config.... m_indenter->setMode(m_config->indentationMode()); m_indenter->updateConfig(); // set tab width there, too m_buffer->setTabWidth(config()->tabWidth()); // update all views, does tagAll and updateView... for (auto view : qAsConst(m_views)) { view->updateDocumentConfig(); } // update on-the-fly spell checking as spell checking defaults might have changes if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); } emit configChanged(); } // BEGIN Variable reader // "local variable" feature by anders, 2003 /* TODO add config options (how many lines to read, on/off) add interface for plugins/apps to set/get variables add view stuff */ void KTextEditor::DocumentPrivate::readVariables(bool onlyViewAndRenderer) { if (!onlyViewAndRenderer) { m_config->configStart(); } // views! for (auto v : qAsConst(m_views)) { v->config()->configStart(); v->renderer()->config()->configStart(); } // read a number of lines in the top/bottom of the document for (int i = 0; i < qMin(9, lines()); ++i) { readVariableLine(line(i), onlyViewAndRenderer); } if (lines() > 10) { for (int i = qMax(10, lines() - 10); i < lines(); i++) { readVariableLine(line(i), onlyViewAndRenderer); } } if (!onlyViewAndRenderer) { m_config->configEnd(); } for (auto v : qAsConst(m_views)) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } void KTextEditor::DocumentPrivate::readVariableLine(const QString &t, bool onlyViewAndRenderer) { static const QRegularExpression kvLine(QStringLiteral("kate:(.*)")); static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)")); static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)")); static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)")); // simple check first, no regex // no kate inside, no vars, simple... if (!t.contains(QLatin1String("kate"))) { return; } // found vars, if any QString s; // now, try first the normal ones auto match = kvLine.match(t); if (match.hasMatch()) { s = match.captured(1); // qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s; } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); const QString nameOfFile = url().fileName(); bool found = false; for (const QString &pattern : wildcards) { QRegExp wildcard(pattern, Qt::CaseSensitive, QRegExp::Wildcard); found = wildcard.exactMatch(nameOfFile); // Qt 5.12, no ifdef, more to test // QRegularExpression wildcard(QLatin1Char('^') + QRegularExpression::wildcardToRegularExpression(pattern) + QLatin1Char('$')); // found = wildcard.match(nameOfFile).hasMatch(); if (found) { break; } } // nothing usable found... if (!found) { return; } s = match.captured(2); // qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s; } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given const QStringList types(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); // no matching type found if (!types.contains(mimeType())) { return; } s = match.captured(2); // qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s; } else { // nothing found return; } // view variable names static const auto vvl = {QLatin1String("dynamic-word-wrap"), QLatin1String("dynamic-word-wrap-indicators"), QLatin1String("line-numbers"), QLatin1String("icon-border"), QLatin1String("folding-markers"), QLatin1String("folding-preview"), QLatin1String("bookmark-sorting"), QLatin1String("auto-center-lines"), QLatin1String("icon-bar-color"), QLatin1String("scrollbar-minimap"), QLatin1String("scrollbar-preview") // renderer , QLatin1String("background-color"), QLatin1String("selection-color"), QLatin1String("current-line-color"), QLatin1String("bracket-highlight-color"), QLatin1String("word-wrap-marker-color"), QLatin1String("font"), QLatin1String("font-size"), QLatin1String("scheme")}; int spaceIndent = -1; // for backward compatibility; see below bool replaceTabsSet = false; int startPos(0); QString var, val; while ((match = kvVar.match(s, startPos)).hasMatch()) { startPos = match.capturedEnd(0); var = match.captured(1); val = match.captured(2).trimmed(); bool state; // store booleans here int n; // store ints here // only apply view & renderer config stuff if (onlyViewAndRenderer) { if (contains(vvl, var)) { // FIXME define above setViewVariable(var, val); } } else { // BOOL SETTINGS if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) { setWordWrap(state); // ??? FIXME CHECK } // KateConfig::configFlags // FIXME should this be optimized to only a few calls? how? else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) { m_config->setBackspaceIndents(state); } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) { m_config->setIndentPastedText(state); } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) { m_config->setReplaceTabsDyn(state); replaceTabsSet = true; // for backward compatibility; see below } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n( "Using deprecated modeline 'remove-trailing-space'. " "Please replace with 'remove-trailing-spaces modified;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 1 : 0); } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n( "Using deprecated modeline 'replace-trailing-space-save'. " "Please replace with 'remove-trailing-spaces all;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 2 : 0); } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) { m_config->setOvr(state); } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) { m_config->setKeepExtraSpaces(state); } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) { m_config->setTabIndents(state); } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) { m_config->setShowTabs(state); } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) { m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None); } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) { // this is for backward compatibility; see below spaceIndent = state; } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) { m_config->setSmartHome(state); } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) { m_config->setNewLineAtEof(state); } // INTEGER SETTINGS else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) { m_config->setTabWidth(n); } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) { m_config->setIndentationWidth(n); } else if (var == QLatin1String("indent-mode")) { m_config->setIndentationMode(val); } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;) m_config->setWordWrapAt(n); } // STRING SETTINGS else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) { const auto l = {QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac")}; if ((n = indexOf(l, val.toLower())) != -1) { /** * set eol + avoid that it is overwritten by auto-detection again! * this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705 */ m_config->setEol(n); m_config->setAllowEolDetection(false); } } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) { if (checkBoolValue(val, &state)) { m_config->setBom(state); } } else if (var == QLatin1String("remove-trailing-spaces")) { val = val.toLower(); if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) { m_config->setRemoveSpaces(1); } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) { m_config->setRemoveSpaces(2); } else { m_config->setRemoveSpaces(0); } } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) { setHighlightingMode(val); } else if (var == QLatin1String("mode")) { setMode(val); } else if (var == QLatin1String("encoding")) { setEncoding(val); } else if (var == QLatin1String("default-dictionary")) { setDefaultDictionary(val); } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) { onTheFlySpellCheckingEnabled(state); } // VIEW SETTINGS else if (contains(vvl, var)) { setViewVariable(var, val); } else { m_storedVariables.insert(var, val); } } } // Backward compatibility // If space-indent was set, but replace-tabs was not set, we assume // that the user wants to replace tabulators and set that flag. // If both were set, replace-tabs has precedence. // At this point spaceIndent is -1 if it was never set, // 0 if it was set to off, and 1 if it was set to on. // Note that if onlyViewAndRenderer was requested, spaceIndent is -1. if (!replaceTabsSet && spaceIndent >= 0) { m_config->setReplaceTabsDyn(spaceIndent > 0); } } void KTextEditor::DocumentPrivate::setViewVariable(const QString &var, const QString &val) { bool state; int n; QColor c; for (auto v : qAsConst(m_views)) { // First, try the new config interface QVariant help(val); // Special treatment to catch "on"/"off" if (checkBoolValue(val, &state)) { help = state; } if (v->config()->setValue(var, help)) { } else if (v->renderer()->config()->setValue(var, help)) { // No success? Go the old way } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) { v->config()->setDynWordWrap(state); } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) { v->setBlockSelection(state); // else if ( var = "dynamic-word-wrap-indicators" ) } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) { v->renderer()->config()->setIconBarColor(c); } // RENDERER else if (var == QLatin1String("background-color") && checkColorValue(val, c)) { v->renderer()->config()->setBackgroundColor(c); } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) { v->renderer()->config()->setSelectionColor(c); } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedLineColor(c); } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedBracketColor(c); } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) { v->renderer()->config()->setWordWrapMarkerColor(c); } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && var == QLatin1String("font-size"))) { QFont _f(v->renderer()->currentFont()); if (var == QLatin1String("font")) { _f.setFamily(val); _f.setFixedPitch(QFont(val).fixedPitch()); } else { _f.setPointSize(n); } v->renderer()->config()->setFont(_f); } else if (var == QLatin1String("scheme")) { v->renderer()->config()->setSchema(val); } } } bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result) { val = val.trimmed().toLower(); static const auto trueValues = {QLatin1String("1"), QLatin1String("on"), QLatin1String("true")}; if (contains(trueValues, val)) { *result = true; return true; } static const auto falseValues = {QLatin1String("0"), QLatin1String("off"), QLatin1String("false")}; if (contains(falseValues, val)) { *result = false; return true; } return false; } bool KTextEditor::DocumentPrivate::checkIntValue(const QString &val, int *result) { bool ret(false); *result = val.toInt(&ret); return ret; } bool KTextEditor::DocumentPrivate::checkColorValue(const QString &val, QColor &c) { c.setNamedColor(val); return c.isValid(); } // KTextEditor::variable QString KTextEditor::DocumentPrivate::variable(const QString &name) const { return m_storedVariables.value(name, QString()); } void KTextEditor::DocumentPrivate::setVariable(const QString &name, const QString &value) { QString s = QStringLiteral("kate: "); s.append(name); s.append(QLatin1Char(' ')); s.append(value); readVariableLine(s); } // END void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) { m_modOnHd = true; m_modOnHdReason = OnDiskModified; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) { m_modOnHd = true; m_modOnHdReason = OnDiskCreated; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) { m_modOnHd = true; m_modOnHdReason = OnDiskDeleted; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd() { // compare git hash with the one we have (if we have one) const QByteArray oldDigest = checksum(); if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) { /** * if current checksum == checksum of new file => unmodified */ if (m_modOnHdReason != OnDiskDeleted && createDigest() && oldDigest == checksum()) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; } #if LIBGIT2_FOUND /** * if still modified, try to take a look at git * skip that, if document is modified! * only do that, if the file is still there, else reload makes no sense! */ if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())) { /** * try to discover the git repo of this file * libgit2 docs state that UTF-8 is the right encoding, even on windows * I hope that is correct! */ git_repository *repository = nullptr; const QByteArray utf8Path = url().toLocalFile().toUtf8(); if (git_repository_open_ext(&repository, utf8Path.constData(), 0, nullptr) == 0) { /** * if we have repo, convert the git hash to an OID */ git_oid oid; if (git_oid_fromstr(&oid, oldDigest.toHex().data()) == 0) { /** * finally: is there a blob for this git hash? */ git_blob *blob = nullptr; if (git_blob_lookup(&blob, repository, &oid) == 0) { /** * this hash exists still in git => just reload */ m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; documentReload(); } git_blob_free(blob); } } git_repository_free(repository); } #endif } /** * emit our signal to the outside! */ emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } QByteArray KTextEditor::DocumentPrivate::checksum() const { return m_buffer->digest(); } bool KTextEditor::DocumentPrivate::createDigest() { QByteArray digest; if (url().isLocalFile()) { QFile f(url().toLocalFile()); if (f.open(QIODevice::ReadOnly)) { // init the hash with the git header QCryptographicHash crypto(QCryptographicHash::Sha1); const QString header = QStringLiteral("blob %1").arg(f.size()); crypto.addData(header.toLatin1() + '\0'); while (!f.atEnd()) { crypto.addData(f.read(256 * 1024)); } digest = crypto.result(); } } /** * set new digest */ m_buffer->setDigest(digest); return !digest.isEmpty(); } QString KTextEditor::DocumentPrivate::reasonedMOHString() const { // squeeze path const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile)); switch (m_modOnHdReason) { - case OnDiskModified: - return i18n("The file '%1' was modified by another program.", str); - break; - case OnDiskCreated: - return i18n("The file '%1' was created by another program.", str); - break; - case OnDiskDeleted: - return i18n("The file '%1' was deleted by another program.", str); - break; - default: - return QString(); + case OnDiskModified: + return i18n("The file '%1' was modified by another program.", str); + break; + case OnDiskCreated: + return i18n("The file '%1' was created by another program.", str); + break; + case OnDiskDeleted: + return i18n("The file '%1' was deleted by another program.", str); + break; + default: + return QString(); } Q_UNREACHABLE(); return QString(); } void KTextEditor::DocumentPrivate::removeTrailingSpaces() { const int remove = config()->removeSpaces(); if (remove == 0) { return; } // temporarily disable static word wrap (see bug #328900) const bool wordWrapEnabled = config()->wordWrap(); if (wordWrapEnabled) { setWordWrap(false); } editStart(); for (int line = 0; line < lines(); ++line) { Kate::TextLine textline = plainKateTextLine(line); // remove trailing spaces in entire document, remove = 2 // remove trailing spaces of touched lines, remove = 1 // remove trailing spaces of lines saved on disk, remove = 1 if (remove == 2 || textline->markedAsModified() || textline->markedAsSavedOnDisk()) { const int p = textline->lastChar() + 1; const int l = textline->length() - p; if (l > 0) { editRemoveText(line, p, l); } } } editEnd(); // enable word wrap again, if it was enabled (see bug #328900) if (wordWrapEnabled) { setWordWrap(true); // see begin of this function } } void KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user) { if (user || !m_fileTypeSetByUser) { if (!newType.isEmpty()) { // remember that we got set by user m_fileTypeSetByUser = user; m_fileType = newType; m_config->configStart(); // NOTE: if the user changes the Mode, the Highlighting also changes. // m_hlSetByUser avoids resetting the highlight when saving the document, if // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763) if ((user || !m_hlSetByUser) && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl.isEmpty()) { int hl(KateHlManager::self()->nameFind(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl)); if (hl >= 0) { m_buffer->setHighlight(hl); } } /** * set the indentation mode, if any in the mode... * and user did not set it before! * NOTE: KateBuffer::setHighlight() also sets the indentation. */ if (!m_indenterSetByUser && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter.isEmpty()) { config()->setIndentationMode(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter); } // views! for (auto v : qAsConst(m_views)) { v->config()->configStart(); v->renderer()->config()->configStart(); } bool bom_settings = false; if (m_bomSetByUser) { bom_settings = m_config->bom(); } readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).varLine); if (m_bomSetByUser) { m_config->setBom(bom_settings); } m_config->configEnd(); for (auto v : qAsConst(m_views)) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } } // fixme, make this better... emit modeChanged(this); } void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing) { *handled = true; *abortClosing = true; if (this->url().isEmpty()) { QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File")); if (res.isEmpty()) { *abortClosing = true; return; } saveAs(res); *abortClosing = false; } else { save(); *abortClosing = false; } } // BEGIN KTextEditor::ConfigInterface // BEGIN ConfigInterface stff QStringList KTextEditor::DocumentPrivate::configKeys() const { /** * expose all internally registered keys of the KateDocumentConfig */ return m_config->configKeys(); } QVariant KTextEditor::DocumentPrivate::configValue(const QString &key) { /** * just dispatch to internal key => value lookup */ return m_config->value(key); } void KTextEditor::DocumentPrivate::setConfigValue(const QString &key, const QVariant &value) { /** * just dispatch to internal key + value set */ m_config->setValue(key, value); } // END KTextEditor::ConfigInterface KTextEditor::Cursor KTextEditor::DocumentPrivate::documentEnd() const { return KTextEditor::Cursor(lastLine(), lineLength(lastLine())); } bool KTextEditor::DocumentPrivate::replaceText(const KTextEditor::Range &range, const QString &s, bool block) { // TODO more efficient? editStart(); bool changed = removeText(range, block); changed |= insertText(range.start(), s, block); editEnd(); return changed; } KateHighlighting *KTextEditor::DocumentPrivate::highlight() const { return m_buffer->highlight(); } Kate::TextLine KTextEditor::DocumentPrivate::kateTextLine(int i) { m_buffer->ensureHighlighted(i); return m_buffer->plainLine(i); } Kate::TextLine KTextEditor::DocumentPrivate::plainKateTextLine(int i) { return m_buffer->plainLine(i); } bool KTextEditor::DocumentPrivate::isEditRunning() const { return editIsRunning; } void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge) { if (merge && m_undoMergeAllEdits) { // Don't add another undo safe point: it will override our current one, // meaning we'll need two undo's to get back there - which defeats the object! return; } m_undoManager->undoSafePoint(); m_undoManager->setAllowComplexMerge(merge); m_undoMergeAllEdits = merge; } // BEGIN KTextEditor::MovingInterface KTextEditor::MovingCursor *KTextEditor::DocumentPrivate::newMovingCursor(const KTextEditor::Cursor &position, KTextEditor::MovingCursor::InsertBehavior insertBehavior) { return new Kate::TextCursor(buffer(), position, insertBehavior); } KTextEditor::MovingRange *KTextEditor::DocumentPrivate::newMovingRange(const KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior) { return new Kate::TextRange(buffer(), range, insertBehaviors, emptyBehavior); } qint64 KTextEditor::DocumentPrivate::revision() const { return m_buffer->history().revision(); } qint64 KTextEditor::DocumentPrivate::lastSavedRevision() const { return m_buffer->history().lastSavedRevision(); } void KTextEditor::DocumentPrivate::lockRevision(qint64 revision) { m_buffer->history().lockRevision(revision); } void KTextEditor::DocumentPrivate::unlockRevision(qint64 revision) { m_buffer->history().unlockRevision(revision); } void KTextEditor::DocumentPrivate::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); } void KTextEditor::DocumentPrivate::transformCursor(KTextEditor::Cursor &cursor, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { int line = cursor.line(), column = cursor.column(); m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); cursor.setLine(line); cursor.setColumn(column); } void KTextEditor::DocumentPrivate::transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision); } // END // BEGIN KTextEditor::AnnotationInterface void KTextEditor::DocumentPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model) { KTextEditor::AnnotationModel *oldmodel = m_annotationModel; m_annotationModel = model; emit annotationModelChanged(oldmodel, m_annotationModel); } KTextEditor::AnnotationModel *KTextEditor::DocumentPrivate::annotationModel() const { return m_annotationModel; } // END KTextEditor::AnnotationInterface // TAKEN FROM kparts.h bool KTextEditor::DocumentPrivate::queryClose() { if (!isReadWrite() || !isModified()) { return true; } QString docName = documentName(); int res = KMessageBox::warningYesNoCancel(dialogParent(), i18n("The document \"%1\" has been modified.\n" "Do you want to save your changes or discard them?", docName), i18n("Close Document"), KStandardGuiItem::save(), KStandardGuiItem::discard()); bool abortClose = false; bool handled = false; switch (res) { - case KMessageBox::Yes: - sigQueryClose(&handled, &abortClose); - if (!handled) { - if (url().isEmpty()) { - QUrl url = QFileDialog::getSaveFileUrl(dialogParent()); - if (url.isEmpty()) { - return false; - } - - saveAs(url); - } else { - save(); + case KMessageBox::Yes: + sigQueryClose(&handled, &abortClose); + if (!handled) { + if (url().isEmpty()) { + QUrl url = QFileDialog::getSaveFileUrl(dialogParent()); + if (url.isEmpty()) { + return false; } - } else if (abortClose) { - return false; + + saveAs(url); + } else { + save(); } - return waitSaveComplete(); - case KMessageBox::No: - return true; - default: // case KMessageBox::Cancel : + } else if (abortClose) { return false; + } + return waitSaveComplete(); + case KMessageBox::No: + return true; + default: // case KMessageBox::Cancel : + return false; } } void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job) { /** * if we are idle before, we are now loading! */ if (m_documentState == DocumentIdle) { m_documentState = DocumentLoading; } /** * if loading: * - remember pre loading read-write mode * if remote load: * - set to read-only * - trigger possible message */ if (m_documentState == DocumentLoading) { /** * remember state */ m_readWriteStateBeforeLoading = isReadWrite(); /** * perhaps show loading message, but wait one second */ if (job) { /** * only read only if really remote file! */ setReadWrite(false); /** * perhaps some message about loading in one second! * remember job pointer, we want to be able to kill it! */ m_loadingJob = job; QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage())); } } } void KTextEditor::DocumentPrivate::slotCompleted() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; } /** * Emit signal that we saved the document, if needed */ if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) { emit documentSavedOrUploaded(this, m_documentState == DocumentSavingAs); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotCanceled() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; showAndSetOpeningErrorAccess(); updateDocName(); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage() { /** * no longer loading? * no message needed! */ if (m_documentState != DocumentLoading) { return; } /** * create message about file loading in progress */ delete m_loadingMessage; m_loadingMessage = new KTextEditor::Message(i18n("The file %2 is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName())); m_loadingMessage->setPosition(KTextEditor::Message::TopInView); /** * if around job: add cancel action */ if (m_loadingJob) { QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr); connect(cancel, SIGNAL(triggered()), this, SLOT(slotAbortLoading())); m_loadingMessage->addAction(cancel); } /** * really post message */ postMessage(m_loadingMessage); } void KTextEditor::DocumentPrivate::slotAbortLoading() { /** * no job, no work */ if (!m_loadingJob) { return; } /** * abort loading if any job * signal results! */ m_loadingJob->kill(KJob::EmitResult); m_loadingJob = nullptr; } void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url) { if (m_reloading) { // the URL is temporarily unset and then reset to the previous URL during reload // we do not want to notify the outside about this return; } Q_UNUSED(url); updateDocName(); emit documentUrlChanged(this); } bool KTextEditor::DocumentPrivate::save() { /** * no double save/load * we need to allow DocumentPreSavingAs here as state, as save is called in saveAs! */ if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) { return false; } /** * if we are idle, we are now saving */ if (m_documentState == DocumentIdle) { m_documentState = DocumentSaving; } else { m_documentState = DocumentSavingAs; } /** * call back implementation for real work */ return KTextEditor::Document::save(); } bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url) { /** * abort on bad URL * that is done in saveAs below, too * but we must check it here already to avoid messing up * as no signals will be send, then */ if (!url.isValid()) { return false; } /** * no double save/load */ if (m_documentState != DocumentIdle) { return false; } /** * we enter the pre save as phase */ m_documentState = DocumentPreSavingAs; /** * call base implementation for real work */ return KTextEditor::Document::saveAs(normalizeUrl(url)); } QString KTextEditor::DocumentPrivate::defaultDictionary() const { return m_defaultDictionary; } QList> KTextEditor::DocumentPrivate::dictionaryRanges() const { return m_dictionaryRanges; } void KTextEditor::DocumentPrivate::clearDictionaryRanges() { for (QList>::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end(); ++i) { delete (*i).first; } m_dictionaryRanges.clear(); if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(); } emit dictionaryRangesPresent(false); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range, bool blockmode) { if (blockmode) { for (int i = range.start().line(); i <= range.end().line(); ++i) { setDictionary(newDictionary, rangeOnLine(range, i)); } } else { setDictionary(newDictionary, range); } emit dictionaryRangesPresent(!m_dictionaryRanges.isEmpty()); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range) { KTextEditor::Range newDictionaryRange = range; if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) { return; } QList> newRanges; // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint for (QList>::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) { qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange; if (newDictionaryRange.isEmpty()) { break; } QPair pair = *i; QString dictionarySet = pair.second; KTextEditor::MovingRange *dictionaryRange = pair.first; qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet; if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) { qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange"; return; } if (newDictionaryRange.contains(*dictionaryRange)) { delete dictionaryRange; i = m_dictionaryRanges.erase(i); qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange"; continue; } KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange); if (!intersection.isEmpty() && intersection.isValid()) { if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection' // except cut off the intersection QList remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection); Q_ASSERT(remainingRanges.size() == 1); newDictionaryRange = remainingRanges.first(); ++i; qCDebug(LOG_KTE) << "dictionarySet == newDictionary"; continue; } QList remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection); for (QList::iterator j = remainingRanges.begin(); j != remainingRanges.end(); ++j) { KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); remainingRange->setFeedback(this); newRanges.push_back(QPair(remainingRange, dictionarySet)); } i = m_dictionaryRanges.erase(i); delete dictionaryRange; } else { ++i; } } m_dictionaryRanges += newRanges; if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary KTextEditor::MovingRange *newDictionaryMovingRange = newMovingRange(newDictionaryRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); newDictionaryMovingRange->setFeedback(this); m_dictionaryRanges.push_back(QPair(newDictionaryMovingRange, newDictionary)); } if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) { m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange); } } void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict) { if (m_defaultDictionary == dict) { return; } m_defaultDictionary = dict; if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); refreshOnTheFlyCheck(); } emit defaultDictionaryChanged(this); } void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable) { if (isOnTheFlySpellCheckingEnabled() == enable) { return; } if (enable) { Q_ASSERT(m_onTheFlyChecker == nullptr); m_onTheFlyChecker = new KateOnTheFlyChecker(this); } else { delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; } for (auto view : qAsConst(m_views)) { view->reflectOnTheFlySpellCheckStatus(enable); } } bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const { return m_onTheFlyChecker != nullptr; } QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(const KTextEditor::Range &range) const { if (!m_onTheFlyChecker) { return QString(); } else { return m_onTheFlyChecker->dictionaryForMisspelledRange(range); } } void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word) { if (m_onTheFlyChecker) { m_onTheFlyChecker->clearMisspellingForWord(word); } } void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(const KTextEditor::Range &range) { if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(range); } } void KTextEditor::DocumentPrivate::rangeInvalid(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::rangeEmpty(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange) { qCDebug(LOG_KTE) << "deleting" << movingRange; auto finder = [=](const QPair &item) -> bool { return item.first == movingRange; }; auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder); if (it != m_dictionaryRanges.end()) { m_dictionaryRanges.erase(it); delete movingRange; } Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end()); } bool KTextEditor::DocumentPrivate::containsCharacterEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn; ++col) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); if (!prefixStore.findPrefix(textLine, col).isEmpty()) { return true; } } } return false; } int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos) { int previousOffset = 0; for (OffsetList::const_iterator i = offsetList.begin(); i != offsetList.end(); ++i) { if ((*i).first > pos) { break; } previousOffset = (*i).second; } return pos + previousOffset; } QString KTextEditor::DocumentPrivate::decodeCharacters(const KTextEditor::Range &range, KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList, KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList) { QString toReturn; KTextEditor::Cursor previous = range.start(); int decToEncCurrentOffset = 0, encToDecCurrentOffset = 0; int i = 0; int newI = 0; KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); const QHash &characterEncodingsHash = highlighting->getCharacterEncodings(attr); QString matchingPrefix = prefixStore.findPrefix(textLine, col); if (!matchingPrefix.isEmpty()) { toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col))); const QChar &c = characterEncodingsHash.value(matchingPrefix); const bool isNullChar = c.isNull(); if (!c.isNull()) { toReturn += c; } i += matchingPrefix.length(); col += matchingPrefix.length(); previous = KTextEditor::Cursor(line, col); decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length(); encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1); newI += (isNullChar ? 0 : 1); decToEncOffsetList.push_back(QPair(newI, decToEncCurrentOffset)); encToDecOffsetList.push_back(QPair(i, encToDecCurrentOffset)); continue; } ++col; ++i; ++newI; } ++i; ++newI; } if (previous < range.end()) { toReturn += text(KTextEditor::Range(previous, range.end())); } return toReturn; } void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const QHash &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr); QHash::const_iterator it = reverseCharacterEncodingsHash.find(textLine->at(col)); if (it != reverseCharacterEncodingsHash.end()) { replaceText(KTextEditor::Range(line, col, line, col + 1), *it); col += (*it).length(); continue; } ++col; } } } // // Highlighting information // KTextEditor::Attribute::Ptr KTextEditor::DocumentPrivate::attributeAt(const KTextEditor::Cursor &position) { KTextEditor::Attribute::Ptr attrib(new KTextEditor::Attribute()); KTextEditor::ViewPrivate *view = m_views.empty() ? nullptr : m_views.begin().value(); if (!view) { qCWarning(LOG_KTE) << "ATTENTION: cannot access lineAttributes() without any View (will be fixed eventually)"; return attrib; } Kate::TextLine kateLine = kateTextLine(position.line()); if (!kateLine) { return attrib; } *attrib = *view->renderer()->attribute(kateLine->attribute(position.column())); return attrib; } QStringList KTextEditor::DocumentPrivate::embeddedHighlightingModes() const { return highlight()->getEmbeddedHighlightingModes(); } QString KTextEditor::DocumentPrivate::highlightingModeAt(const KTextEditor::Cursor &position) { return highlight()->higlightingModeForLocation(this, position); } Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile() { return m_swapfile; } /** * \return \c -1 if \c line or \c column invalid, otherwise one of * standard style attribute number */ int KTextEditor::DocumentPrivate::defStyleNum(int line, int column) { // Validate parameters to prevent out of range access if (line < 0 || line >= lines() || column < 0) { return -1; } // get highlighted line Kate::TextLine tl = kateTextLine(line); // make sure the textline is a valid pointer if (!tl) { return -1; } /** * either get char attribute or attribute of context still active at end of line */ int attribute = 0; if (column < tl->length()) { attribute = tl->attribute(column); } else if (column == tl->length()) { if (!tl->attributesList().isEmpty()) { attribute = tl->attributesList().back().attributeValue; } else { return -1; } } else { return -1; } return highlight()->defaultStyleForAttribute(attribute); } bool KTextEditor::DocumentPrivate::isComment(int line, int column) { const int defaultStyle = defStyleNum(line, column); return defaultStyle == KTextEditor::dsComment; } int KTextEditor::DocumentPrivate::findTouchedLine(int startLine, bool down) { const int offset = down ? 1 : -1; const int lineCount = lines(); while (startLine >= 0 && startLine < lineCount) { Kate::TextLine tl = m_buffer->plainLine(startLine); if (tl && (tl->markedAsModified() || tl->markedAsSavedOnDisk())) { return startLine; } startLine += offset; } return -1; } void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler *handler) { // delete any active template handler delete m_activeTemplateHandler.data(); m_activeTemplateHandler = handler; } // BEGIN KTextEditor::MessageInterface bool KTextEditor::DocumentPrivate::postMessage(KTextEditor::Message *message) { // no message -> cancel if (!message) { return false; } // make sure the desired view belongs to this document if (message->view() && message->view()->document() != this) { qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text(); return false; } message->setParent(this); message->setDocument(this); // if there are no actions, add a close action by default if widget does not auto-hide if (message->actions().count() == 0 && message->autoHide() < 0) { QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); message->addAction(closeAction); } // make sure the message is registered even if no actions and no views exist m_messageHash[message] = QList>(); // reparent actions, as we want full control over when they are deleted const auto messageActions = message->actions(); for (QAction *action : messageActions) { action->setParent(nullptr); m_messageHash[message].append(QSharedPointer(action)); } // post message to requested view, or to all views if (KTextEditor::ViewPrivate *view = qobject_cast(message->view())) { view->postMessage(message, m_messageHash[message]); } else { for (auto view : qAsConst(m_views)) { view->postMessage(message, m_messageHash[message]); } } // also catch if the user manually calls delete message connect(message, SIGNAL(closed(KTextEditor::Message *)), SLOT(messageDestroyed(KTextEditor::Message *))); return true; } void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message) { // KTE:Message is already in destructor Q_ASSERT(m_messageHash.contains(message)); m_messageHash.remove(message); } // END KTextEditor::MessageInterface void KTextEditor::DocumentPrivate::closeDocumentInApplication() { KTextEditor::EditorPrivate::self()->application()->closeDocument(this); } diff --git a/src/include/ktexteditor/cursor.h b/src/include/ktexteditor/cursor.h index 173138dc..42b58ce8 100644 --- a/src/include/ktexteditor/cursor.h +++ b/src/include/ktexteditor/cursor.h @@ -1,430 +1,430 @@ /* This file is part of the KDE project Copyright (C) 2003-2005 Hamish Rodda Copyright (C) 2001-2005 Christoph Cullmann Copyright (C) 2014 Dominik Haumann Copyright (C) 2002 Christian Couder Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KTEXTEDITOR_CURSOR_H #define KTEXTEDITOR_CURSOR_H #include #include #include namespace KTextEditor { class Document; class Range; /** * \short The Cursor represents a position in a Document. * * \section kte_cursor_intro Introduction * A Cursor represents a position in a Document through a tuple * of two int%s, namely the line() and column(). A Cursor maintains * no affiliation with a particular Document, meaning that it remains * constant if not changed through the Cursor API. * * \section kte_cursor_notes Important Notes * * Working with a cursor, one should be aware of the following notes: * - Lines and columns start a 0. * - The Cursor class is designed to be passed by value (only 8 Bytes). * - Think of cursors as having their position at the start of a character, * not in the middle of one. * - invalid() Cursor%s are located at (-1, -1). In addition, a Cursor * is invalid(), if either its line() and/or its column() is arbitrarily * negative, i.e. < 0. * - All Cursor%s with line() >= 0 and column() >= 0 are valid. In this case * isValid() returns \e true. * - A Cursor has a non-virtual destructor. Hence, you cannot derive from Cursor. * * \section kte_cursor_properties Cursor Efficiency * * The Cursor consists of just two int%s, the line() and the column(). * Therefore, a Cursor instance takes 8 Bytes of memory. Further, a Cursor * is a non-virtual class, turning it into a primitive old data type (POD). * Thus, it can be moved and copied very efficiently. * * \section kte_cursor_more Additional Concepts * * In addition to the Cursor, the KTextEditor API provides advanced concepts: * - The DocumentCursor is a Cursor bound to a specific Document. In addition * to the Cursor API, it provides convenience functions like * DocumentCursor::isValidTextPosition() or DocumentCursor::move(). * The DocumentCursor does not maintain its position, though. * - The MovingCursor is also bound to a specific Document. In addition to the * DocumentCursor, the MovingCursor maintains its position, meaning that * whenever the Document changes, the MovingCursor moves, too. * - The Cursor forms the basis for the Range. * * \sa DocumentCursor, MovingCursor, Range */ class KTEXTEDITOR_EXPORT Cursor { public: /** * The default constructor creates a cursor at position (0, 0). */ Q_DECL_CONSTEXPR Cursor() Q_DECL_NOEXCEPT { } /** * This constructor creates a cursor initialized with \p line * and \p column. * \param line line for cursor * \param column column for cursor */ Q_DECL_CONSTEXPR Cursor(int line, int column) Q_DECL_NOEXCEPT : m_line(line), m_column(column) { } /** * Returns whether the current position of this cursor is a valid position * (line + column must both be >= 0). * * @note If you want to check, whether a cursor position is a valid * \e text-position, use DocumentCursor::isValidTextPosition(), * or Document::isValidTextPosition(). */ Q_DECL_CONSTEXPR inline bool isValid() const Q_DECL_NOEXCEPT { return m_line >= 0 && m_column >= 0; } /** * Returns an invalid cursor. * The returned cursor position is set to (-1, -1). * \see isValid() */ Q_DECL_CONSTEXPR static Cursor invalid() Q_DECL_NOEXCEPT { return Cursor(-1, -1); } /** * Returns a cursor representing the start of any document - i.e., line 0, column 0. */ Q_DECL_CONSTEXPR static Cursor start() Q_DECL_NOEXCEPT { return Cursor(); } /** * Returns the cursor position as string in the format "(line, column)". * \see fromString() */ QString toString() const { return QLatin1Char('(') + QString::number(m_line) + QLatin1String(", ") + QString::number(m_column) + QLatin1Char(')'); } /** * Returns a Cursor created from the string \p str containing the format * "(line, column)". In case the string cannot be parsed, Cursor::invalid() * is returned. * \see toString() */ static Cursor fromString(const QString &str) Q_DECL_NOEXCEPT { return fromString(str.leftRef(-1)); } /** * Returns a Cursor created from the string \p str containing the format * "(line, column)". In case the string cannot be parsed, Cursor::invalid() * is returned. * \see toString() */ static Cursor fromString(const QStringRef &str) Q_DECL_NOEXCEPT; /** * \name Position * * The following functions provide access to, and manipulation of, the cursor's position. * \{ */ /** * Set the current cursor position to \e position. * * \param position new cursor position */ inline void setPosition(const Cursor &position) Q_DECL_NOEXCEPT { m_line = position.m_line; m_column = position.m_column; } /** * \overload * * Set the cursor position to \e line and \e column. * * \param line new cursor line * \param column new cursor column */ inline void setPosition(int line, int column) Q_DECL_NOEXCEPT { m_line = line; m_column = column; } /** * Retrieve the line on which this cursor is situated. * \return line number, where 0 is the first line. */ Q_DECL_CONSTEXPR inline int line() const Q_DECL_NOEXCEPT { return m_line; } /** * Set the cursor line to \e line. * \param line new cursor line */ inline void setLine(int line) Q_DECL_NOEXCEPT { m_line = line; } /** * Retrieve the column on which this cursor is situated. * \return column number, where 0 is the first column. */ Q_DECL_CONSTEXPR inline int column() const Q_DECL_NOEXCEPT { return m_column; } /** * Set the cursor column to \e column. * \param column new cursor column */ inline void setColumn(int column) Q_DECL_NOEXCEPT { m_column = column; } /** * Determine if this cursor is located at the start of a line (= at column 0). * \return \e true if the cursor is situated at the start of the line, \e false if it isn't. */ Q_DECL_CONSTEXPR inline bool atStartOfLine() const Q_DECL_NOEXCEPT { return m_column == 0; } /** * Determine if this cursor is located at the start of a document (= at position (0, 0)). * \return \e true if the cursor is situated at the start of the document, \e false if it isn't. */ Q_DECL_CONSTEXPR inline bool atStartOfDocument() const Q_DECL_NOEXCEPT { return m_line == 0 && m_column == 0; } /** * Get both the line and column of the cursor position. * \param line will be filled with current cursor line * \param column will be filled with current cursor column */ inline void position(int &line, int &column) const Q_DECL_NOEXCEPT { line = m_line; column = m_column; } //!\} /** * Addition operator. Takes two cursors and returns their summation. * \param c1 the first position * \param c2 the second position * \return a the summation of the two input cursors */ Q_DECL_CONSTEXPR inline friend Cursor operator+(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return Cursor(c1.line() + c2.line(), c1.column() + c2.column()); } /** * Addition assignment operator. Adds \p c2 to this cursor. * \param c1 the cursor being added to * \param c2 the position to add * \return a reference to the cursor which has just been added to */ inline friend Cursor &operator+=(Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { c1.setPosition(c1.line() + c2.line(), c1.column() + c2.column()); return c1; } /** * Subtraction operator. Takes two cursors and returns the subtraction * of \p c2 from \p c1. * * \param c1 the first position * \param c2 the second position * \return a cursor representing the subtraction of \p c2 from \p c1 */ Q_DECL_CONSTEXPR inline friend Cursor operator-(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return Cursor(c1.line() - c2.line(), c1.column() - c2.column()); } /** * Subtraction assignment operator. Subtracts \p c2 from \p c1. * \param c1 the cursor being subtracted from * \param c2 the position to subtract * \return a reference to the cursor which has just been subtracted from */ inline friend Cursor &operator-=(Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { c1.setPosition(c1.line() - c2.line(), c1.column() - c2.column()); return c1; } /** * Equality operator. * * \note comparison between two invalid cursors is undefined. * comparison between and invalid and a valid cursor will always be \e false. * * \param c1 first cursor to compare * \param c2 second cursor to compare * \return \e true, if c1's and c2's line and column are \e equal. */ Q_DECL_CONSTEXPR inline friend bool operator==(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return c1.line() == c2.line() && c1.column() == c2.column(); } /** * Inequality operator. * \param c1 first cursor to compare * \param c2 second cursor to compare * \return \e true, if c1's and c2's line and column are \e not equal. */ Q_DECL_CONSTEXPR inline friend bool operator!=(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return !(c1 == c2); } /** * Greater than operator. * \param c1 first cursor to compare * \param c2 second cursor to compare * \return \e true, if c1's position is greater than c2's position, * otherwise \e false. */ Q_DECL_CONSTEXPR inline friend bool operator>(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return c1.line() > c2.line() || (c1.line() == c2.line() && c1.m_column > c2.m_column); } /** * Greater than or equal to operator. * \param c1 first cursor to compare * \param c2 second cursor to compare * \return \e true, if c1's position is greater than or equal to c2's * position, otherwise \e false. */ Q_DECL_CONSTEXPR inline friend bool operator>=(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return c1.line() > c2.line() || (c1.line() == c2.line() && c1.m_column >= c2.m_column); } /** * Less than operator. * \param c1 first cursor to compare * \param c2 second cursor to compare * \return \e true, if c1's position is greater than or equal to c2's * position, otherwise \e false. */ Q_DECL_CONSTEXPR inline friend bool operator<(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return !(c1 >= c2); } /** * Less than or equal to operator. * \param c1 first cursor to compare * \param c2 second cursor to compare * \return \e true, if c1's position is lesser than or equal to c2's * position, otherwise \e false. */ Q_DECL_CONSTEXPR inline friend bool operator<=(const Cursor &c1, const Cursor &c2) Q_DECL_NOEXCEPT { return !(c1 > c2); } /** * qDebug() stream operator. Writes this cursor to the debug output in a nicely formatted way. */ inline friend QDebug operator<<(QDebug s, const Cursor &cursor) { s.nospace() << "(" << cursor.line() << ", " << cursor.column() << ")"; return s.space(); } private: /** * \internal * * Cursor line */ int m_line = 0; /** * \internal * * Cursor column */ int m_column = 0; }; } // namespace KTextEditor Q_DECLARE_TYPEINFO(KTextEditor::Cursor, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(KTextEditor::Cursor) /** * QHash function for KTextEditor::Cursor. * Returns the hash value for @p cursor. */ inline uint qHash(const KTextEditor::Cursor &cursor, uint seed = 0) Q_DECL_NOTHROW { return qHash(qMakePair(cursor.line(), cursor.column()), seed); } namespace QTest { // forward declaration of template in qtestcase.h -template char *toString(const T &); +template char *toString(const T &); /** * QTestLib integration to have nice output in e.g. QCOMPARE failures. */ -template <> KTEXTEDITOR_EXPORT char *toString(const KTextEditor::Cursor &cursor); +template<> KTEXTEDITOR_EXPORT char *toString(const KTextEditor::Cursor &cursor); } #endif diff --git a/src/include/ktexteditor/range.h b/src/include/ktexteditor/range.h index cad43c5a..741fc4d2 100644 --- a/src/include/ktexteditor/range.h +++ b/src/include/ktexteditor/range.h @@ -1,636 +1,636 @@ /* This file is part of the KDE project * Copyright (C) 2003-2005 Hamish Rodda * Copyright (C) 2001-2005 Christoph Cullmann * Copyright (C) 2002 Christian Couder * Copyright (C) 2001 Joseph Wenninger * Copyright (C) 1999 Jochen Wilhelmy * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KTEXTEDITOR_RANGE_H #define KTEXTEDITOR_RANGE_H #include #include #include #include namespace KTextEditor { /** * \short An object representing a section of text, from one Cursor to another. * * A Range is a basic class which represents a range of text with two Cursors, * from a start() position to an end() position. * * For simplicity and convenience, ranges always maintain their start position to * be before or equal to their end position. Attempting to set either the * start or end of the range beyond the respective end or start will result in * both values being set to the specified position. In the constructor, the * start and end will be swapped if necessary. * * If you want additional functionality such as the ability to maintain position * in a document, see MovingRange. * * \sa MovingRange * * \author Hamish Rodda \ */ class KTEXTEDITOR_EXPORT Range { public: /** * Default constructor. Creates a valid range from position (0, 0) to * position (0, 0). */ Q_DECL_CONSTEXPR Range() Q_DECL_NOEXCEPT { } /** * Constructor which creates a range from \e start to \e end. * If start is after end, they will be swapped. * * \param start start position * \param end end position */ Q_DECL_CONSTEXPR Range(const Cursor &start, const Cursor &end) Q_DECL_NOEXCEPT : m_start(qMin(start, end)), m_end(qMax(start, end)) { } /** * Constructor which creates a single-line range from \p start, * extending \p width characters along the same line. * * \param start start position * \param width width of this range in columns along the same line */ Q_DECL_CONSTEXPR Range(const Cursor &start, int width) Q_DECL_NOEXCEPT : m_start(qMin(start, Cursor(start.line(), start.column() + width))), m_end(qMax(start, Cursor(start.line(), start.column() + width))) { } /** * Constructor which creates a range from \p start, to \p endLine, \p endColumn. * * \param start start position * \param endLine end line * \param endColumn end column */ Q_DECL_CONSTEXPR Range(const Cursor &start, int endLine, int endColumn) Q_DECL_NOEXCEPT : m_start(qMin(start, Cursor(endLine, endColumn))), m_end(qMax(start, Cursor(endLine, endColumn))) { } /** * Constructor which creates a range from \e startLine, \e startColumn to \e endLine, \e endColumn. * * \param startLine start line * \param startColumn start column * \param endLine end line * \param endColumn end column */ Q_DECL_CONSTEXPR Range(int startLine, int startColumn, int endLine, int endColumn) Q_DECL_NOEXCEPT : m_start(qMin(Cursor(startLine, startColumn), Cursor(endLine, endColumn))), m_end(qMax(Cursor(startLine, startColumn), Cursor(endLine, endColumn))) { } /** * Validity check. In the base class, returns true unless the range starts before (0,0). */ Q_DECL_CONSTEXPR inline bool isValid() const Q_DECL_NOEXCEPT { return start().isValid() && end().isValid(); } /** * Returns an invalid range. */ Q_DECL_CONSTEXPR static Range invalid() Q_DECL_NOEXCEPT { return Range(Cursor::invalid(), Cursor::invalid()); } /** * Returns the cursor position as string in the format * "start-line:start-column,endl-line:end-column". * \see fromString() */ QString toString() const { return QLatin1Char('[') + m_start.toString() + QLatin1String(", ") + m_end.toString() + QLatin1Char(']'); } /** * Returns a Range created from the string \p str containing the format * "[(start-line, start-column), (endl-line:end-column)]". * In case the string cannot be parsed, an Range::invalid() is returned. * \see toString() */ static Range fromString(const QString &str) Q_DECL_NOEXCEPT { return fromString(str.leftRef(-1)); } /** * Returns a Range created from the string \p str containing the format * "[(start-line, start-column), (endl-line:end-column)]". * In case the string cannot be parsed, an Range::invalid() is returned. * \see toString() */ static Range fromString(const QStringRef &str) Q_DECL_NOEXCEPT; /** * \name Position * * The following functions provide access to, and manipulation of, the range's position. * \{ */ /** * Get the start position of this range. This will always be <= end(). * * \returns const reference to the start position of this range. */ Q_DECL_CONSTEXPR inline Cursor start() const Q_DECL_NOEXCEPT { return m_start; } /** * Get the end position of this range. This will always be >= start(). * * \returns const reference to the end position of this range. */ Q_DECL_CONSTEXPR inline Cursor end() const Q_DECL_NOEXCEPT { return m_end; } /** * Convenience function. Set the start and end lines to \p line. * * \param line the line number to assign to start() and end() */ void setBothLines(int line) Q_DECL_NOEXCEPT; /** * Convenience function. Set the start and end columns to \p column. * * \param column the column number to assign to start() and end() */ void setBothColumns(int column) Q_DECL_NOEXCEPT; /** * Set the start and end cursors to \e range.start() and \e range.end() respectively. * * \param range range to assign to this range */ void setRange(const Range &range) Q_DECL_NOEXCEPT; /** * \overload * \n \n * Set the start and end cursors to \e start and \e end respectively. * * \note If \e start is after \e end, they will be reversed. * * \param start start cursor * \param end end cursor */ void setRange(const Cursor &start, const Cursor &end) Q_DECL_NOEXCEPT; /** * Set the start cursor to \e start. * * \note If \e start is after current end, start and end will be set to new start value. * * \param start new start cursor */ inline void setStart(const Cursor &start) Q_DECL_NOEXCEPT { if (start > end()) { setRange(start, start); } else { setRange(start, end()); } } /** * Set the end cursor to \e end. * * \note If \e end is in front of current start, start and end will be set to new end value. * * \param end new end cursor */ inline void setEnd(const Cursor &end) Q_DECL_NOEXCEPT { if (end < start()) { setRange(end, end); } else { setRange(start(), end); } } /** * Expand this range if necessary to contain \p range. * * \param range range which this range should contain * * \return \e true if expansion occurred, \e false otherwise */ bool expandToRange(const Range &range) Q_DECL_NOEXCEPT; /** * Confine this range if necessary to fit within \p range. * * \param range range which should contain this range * * \return \e true if confinement occurred, \e false otherwise */ bool confineToRange(const Range &range) Q_DECL_NOEXCEPT; /** * Check whether this range is wholly contained within one line, ie. if * the start() and end() positions are on the same line. * * \return \e true if both the start and end positions are on the same * line, otherwise \e false */ Q_DECL_CONSTEXPR inline bool onSingleLine() const Q_DECL_NOEXCEPT { return start().line() == end().line(); } /** * Returns the number of lines separating the start() and end() positions. * * \return the number of lines separating the start() and end() positions; * 0 if the start and end lines are the same. */ Q_DECL_CONSTEXPR inline int numberOfLines() const Q_DECL_NOEXCEPT { return end().line() - start().line(); } /** * Returns the number of columns separating the start() and end() positions. * * \return the number of columns separating the start() and end() positions; * 0 if the start and end columns are the same. */ Q_DECL_CONSTEXPR inline int columnWidth() const Q_DECL_NOEXCEPT { return end().column() - start().column(); } /** * Returns true if this range contains no characters, ie. the start() and * end() positions are the same. * * \returns \e true if the range contains no characters, otherwise \e false */ Q_DECL_CONSTEXPR inline bool isEmpty() const Q_DECL_NOEXCEPT { return start() == end(); } // BEGIN comparison functions /** * \} * * \name Comparison * * The following functions perform checks against this range in comparison * to other lines, columns, cursors, and ranges. * \{ */ /** * Check whether the this range wholly encompasses \e range. * * \param range range to check * * \return \e true, if this range contains \e range, otherwise \e false */ Q_DECL_CONSTEXPR inline bool contains(const Range &range) const Q_DECL_NOEXCEPT { return range.start() >= start() && range.end() <= end(); } /** * Check to see if \p cursor is contained within this range, ie >= start() and \< end(). * * \param cursor the position to test for containment * * \return \e true if the cursor is contained within this range, otherwise \e false. */ Q_DECL_CONSTEXPR inline bool contains(const Cursor &cursor) const Q_DECL_NOEXCEPT { return cursor >= start() && cursor < end(); } /** * Returns true if this range wholly encompasses \p line. * * \param line line to check * * \return \e true if the line is wholly encompassed by this range, otherwise \e false. */ Q_DECL_CONSTEXPR inline bool containsLine(int line) const Q_DECL_NOEXCEPT { return (line > start().line() || (line == start().line() && !start().column())) && line < end().line(); } /** * Check whether the range contains \e column. * * \param column column to check * * \return \e true if the range contains \e column, otherwise \e false */ Q_DECL_CONSTEXPR inline bool containsColumn(int column) const Q_DECL_NOEXCEPT { return column >= start().column() && column < end().column(); } /** * Check whether the this range overlaps with \e range. * * \param range range to check against * * \return \e true, if this range overlaps with \e range, otherwise \e false */ Q_DECL_CONSTEXPR inline bool overlaps(const Range &range) const Q_DECL_NOEXCEPT { return (range.start() <= start()) ? (range.end() > start()) : (range.end() >= end()) ? (range.start() < end()) : contains(range); } /** * Check whether the range overlaps at least part of \e line. * * \param line line to check * * \return \e true, if the range overlaps at least part of \e line, otherwise \e false */ Q_DECL_CONSTEXPR inline bool overlapsLine(int line) const Q_DECL_NOEXCEPT { return line >= start().line() && line <= end().line(); } /** * Check to see if this range overlaps \p column; that is, if \p column is * between start().column() and end().column(). This function is most likely * to be useful in relation to block text editing. * * \param column the column to test * * \return \e true if the column is between the range's starting and ending * columns, otherwise \e false. */ Q_DECL_CONSTEXPR inline bool overlapsColumn(int column) const Q_DECL_NOEXCEPT { return start().column() <= column && end().column() > column; } /** * Check whether \p cursor is located at either of the start() or end() * boundaries. * * \param cursor cursor to check * * \return \e true if the cursor is equal to \p start() or \p end(), * otherwise \e false. */ Q_DECL_CONSTEXPR inline bool boundaryAtCursor(const Cursor &cursor) const Q_DECL_NOEXCEPT { return cursor == start() || cursor == end(); } //!\} // END /** * Intersects this range with another, returning the shared area of * the two ranges. * * \param range other range to intersect with this * * \return the intersection of this range and the supplied \a range. */ Q_DECL_CONSTEXPR inline Range intersect(const Range &range) const Q_DECL_NOEXCEPT { return ((!isValid() || !range.isValid() || *this > range || *this < range)) ? invalid() : Range(qMax(start(), range.start()), qMin(end(), range.end())); } /** * Returns the smallest range which encompasses this range and the * supplied \a range. * * \param range other range to encompass * * \return the smallest range which contains this range and the supplied \a range. */ Q_DECL_CONSTEXPR inline Range encompass(const Range &range) const Q_DECL_NOEXCEPT { return (!isValid()) ? (range.isValid() ? range : invalid()) : (!range.isValid()) ? (*this) : Range(qMin(start(), range.start()), qMax(end(), range.end())); } /** * Addition operator. Takes two ranges and returns their summation. * * \param r1 the first range * \param r2 the second range * * \return a the summation of the two input ranges */ Q_DECL_CONSTEXPR inline friend Range operator+(const Range &r1, const Range &r2) Q_DECL_NOEXCEPT { return Range(r1.start() + r2.start(), r1.end() + r2.end()); } /** * Addition assignment operator. Adds \p r2 to this range. * * \param r1 the first range * \param r2 the second range * * \return a reference to the cursor which has just been added to */ inline friend Range &operator+=(Range &r1, const Range &r2) Q_DECL_NOEXCEPT { r1.setRange(r1.start() + r2.start(), r1.end() + r2.end()); return r1; } /** * Subtraction operator. Takes two ranges and returns the subtraction * of \p r2 from \p r1. * * \param r1 the first range * \param r2 the second range * * \return a range representing the subtraction of \p r2 from \p r1 */ Q_DECL_CONSTEXPR inline friend Range operator-(const Range &r1, const Range &r2) Q_DECL_NOEXCEPT { return Range(r1.start() - r2.start(), r1.end() - r2.end()); } /** * Subtraction assignment operator. Subtracts \p r2 from \p r1. * * \param r1 the first range * \param r2 the second range * * \return a reference to the range which has just been subtracted from */ inline friend Range &operator-=(Range &r1, const Range &r2) Q_DECL_NOEXCEPT { r1.setRange(r1.start() - r2.start(), r1.end() - r2.end()); return r1; } /** * Intersects \a r1 and \a r2. * * \param r1 the first range * \param r2 the second range * * \return the intersected range, invalid() if there is no overlap */ Q_DECL_CONSTEXPR inline friend Range operator&(const Range &r1, const Range &r2)Q_DECL_NOEXCEPT { return r1.intersect(r2); } /** * Intersects \a r1 with \a r2 and assigns the result to \a r1. * * \param r1 the range to assign the intersection to * \param r2 the range to intersect \a r1 with * * \return a reference to this range, after the intersection has taken place */ inline friend Range &operator&=(Range &r1, const Range &r2) Q_DECL_NOEXCEPT { r1.setRange(r1.intersect(r2)); return r1; } /** * Equality operator. * * \param r1 first range to compare * \param r2 second range to compare * * \return \e true if \e r1 and \e r2 equal, otherwise \e false */ Q_DECL_CONSTEXPR inline friend bool operator==(const Range &r1, const Range &r2) Q_DECL_NOEXCEPT { return r1.start() == r2.start() && r1.end() == r2.end(); } /** * Inequality operator. * * \param r1 first range to compare * \param r2 second range to compare * * \return \e true if \e r1 and \e r2 do \e not equal, otherwise \e false */ Q_DECL_CONSTEXPR inline friend bool operator!=(const Range &r1, const Range &r2) Q_DECL_NOEXCEPT { return r1.start() != r2.start() || r1.end() != r2.end(); } /** * Greater than operator. Looks only at the position of the two ranges, * does not consider their size. * * \param r1 first range to compare * \param r2 second range to compare * * \return \e true if \e r1 starts after where \e r2 ends, otherwise \e false */ Q_DECL_CONSTEXPR inline friend bool operator>(const Range &r1, const Range &r2) Q_DECL_NOEXCEPT { return r1.start() > r2.end(); } /** * Less than operator. Looks only at the position of the two ranges, * does not consider their size. * * \param r1 first range to compare * \param r2 second range to compare * * \return \e true if \e r1 ends before \e r2 begins, otherwise \e false */ Q_DECL_CONSTEXPR inline friend bool operator<(const Range &r1, const Range &r2) Q_DECL_NOEXCEPT { return r1.end() < r2.start(); } /** * qDebug() stream operator. Writes this range to the debug output in a nicely formatted way. */ inline friend QDebug operator<<(QDebug s, const Range &range) { s << "[" << range.start() << " -> " << range.end() << "]"; return s; } private: /** * This range's start cursor pointer. * * \internal */ Cursor m_start; /** * This range's end cursor pointer. * * \internal */ Cursor m_end; }; } Q_DECLARE_TYPEINFO(KTextEditor::Range, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(KTextEditor::Range) /** * QHash function for KTextEditor::Range. * Returns the hash value for @p range. */ inline uint qHash(const KTextEditor::Range &range, uint seed = 0) Q_DECL_NOTHROW { return qHash(qMakePair(qHash(range.start()), qHash(range.end())), seed); } namespace QTest { // forward declaration of template in qtestcase.h -template char *toString(const T &); +template char *toString(const T &); /** * QTestLib integration to have nice output in e.g. QCOMPARE failures. */ -template <> KTEXTEDITOR_EXPORT char *toString(const KTextEditor::Range &range); +template<> KTEXTEDITOR_EXPORT char *toString(const KTextEditor::Range &range); } #endif diff --git a/src/inputmode/kateviinputmode.cpp b/src/inputmode/kateviinputmode.cpp index c151de4f..e5d2c589 100644 --- a/src/inputmode/kateviinputmode.cpp +++ b/src/inputmode/kateviinputmode.cpp @@ -1,332 +1,332 @@ /* This file is part of the KDE libraries and the Kate part. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateviinputmode.h" #include "kateconfig.h" #include "kateviewinternal.h" #include #include #include #include #include #include #include #include #include namespace { QString viModeToString(KateVi::ViMode mode) { QString modeStr; switch (mode) { - case KateVi::InsertMode: - modeStr = i18n("VI: INSERT MODE"); - break; - case KateVi::NormalMode: - modeStr = i18n("VI: NORMAL MODE"); - break; - case KateVi::VisualMode: - modeStr = i18n("VI: VISUAL"); - break; - case KateVi::VisualBlockMode: - modeStr = i18n("VI: VISUAL BLOCK"); - break; - case KateVi::VisualLineMode: - modeStr = i18n("VI: VISUAL LINE"); - break; - case KateVi::ReplaceMode: - modeStr = i18n("VI: REPLACE"); - break; + case KateVi::InsertMode: + modeStr = i18n("VI: INSERT MODE"); + break; + case KateVi::NormalMode: + modeStr = i18n("VI: NORMAL MODE"); + break; + case KateVi::VisualMode: + modeStr = i18n("VI: VISUAL"); + break; + case KateVi::VisualBlockMode: + modeStr = i18n("VI: VISUAL BLOCK"); + break; + case KateVi::VisualLineMode: + modeStr = i18n("VI: VISUAL LINE"); + break; + case KateVi::ReplaceMode: + modeStr = i18n("VI: REPLACE"); + break; } return modeStr; } } KateViInputMode::KateViInputMode(KateViewInternal *viewInternal, KateVi::GlobalState *global) : KateAbstractInputMode(viewInternal) , m_viModeEmulatedCommandBar(nullptr) , m_viGlobal(global) , m_caret(KateRenderer::Block) , m_nextKeypressIsOverriddenShortCut(false) , m_activated(false) { m_relLineNumbers = KateViewConfig::global()->viRelativeLineNumbers(); m_viModeManager = new KateVi::InputModeManager(this, view(), viewInternal); } KateViInputMode::~KateViInputMode() { delete m_viModeManager; } void KateViInputMode::activate() { m_activated = true; setCaretStyle(KateRenderer::Block); // TODO: can we end up in insert mode? reset(); // TODO: is this necessary? (well, not anymore I guess) if (view()->selection()) { m_viModeManager->changeViMode(KateVi::VisualMode); view()->setCursorPosition(KTextEditor::Cursor(view()->selectionRange().end().line(), view()->selectionRange().end().column() - 1)); m_viModeManager->m_viVisualMode->updateSelection(); } viewInternal()->iconBorder()->setRelLineNumbersOn(m_relLineNumbers); } void KateViInputMode::deactivate() { if (m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar->hideMe(); } // make sure to turn off edits merging when leaving vi input mode view()->doc()->setUndoMergeAllEdits(false); m_activated = false; viewInternal()->iconBorder()->setRelLineNumbersOn(false); } void KateViInputMode::reset() { if (m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar->hideMe(); } delete m_viModeManager; m_viModeManager = new KateVi::InputModeManager(this, view(), viewInternal()); if (m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar->setViInputModeManager(m_viModeManager); } } bool KateViInputMode::overwrite() const { return m_viModeManager->getCurrentViMode() == KateVi::ViMode::ReplaceMode; } void KateViInputMode::overwrittenChar(const QChar &c) { m_viModeManager->getViReplaceMode()->overwrittenChar(c); } void KateViInputMode::clearSelection() { // do nothing, handled elsewhere } bool KateViInputMode::stealKey(QKeyEvent *k) { if (!KateViewConfig::global()->viInputModeStealKeys()) { return false; } // Actually see if we can make use of this key - if so, we've stolen it; if not, // let Qt's shortcut handling system deal with it. const bool stolen = keyPress(k); if (stolen) { // Qt will replay this QKeyEvent, next time as an ordinary KeyPress. m_nextKeypressIsOverriddenShortCut = true; } return stolen; } KTextEditor::View::InputMode KateViInputMode::viewInputMode() const { return KTextEditor::View::ViInputMode; } QString KateViInputMode::viewInputModeHuman() const { return i18n("vi-mode"); } KTextEditor::View::ViewMode KateViInputMode::viewMode() const { return m_viModeManager->getCurrentViewMode(); } QString KateViInputMode::viewModeHuman() const { QString currentMode = viModeToString(m_viModeManager->getCurrentViMode()); if (m_viModeManager->macroRecorder()->isRecording()) { currentMode.prepend(QLatin1Char('(') + i18n("recording") + QLatin1String(") ")); } QString cmd = m_viModeManager->getVerbatimKeys(); if (!cmd.isEmpty()) { currentMode.prepend(QStringLiteral("%1 ").arg(cmd)); } return currentMode; } void KateViInputMode::gotFocus() { // nothing to do } void KateViInputMode::lostFocus() { // nothing to do } void KateViInputMode::readSessionConfig(const KConfigGroup &config) { // restore vi registers and jump list m_viModeManager->readSessionConfig(config); } void KateViInputMode::writeSessionConfig(KConfigGroup &config) { // save vi registers and jump list m_viModeManager->writeSessionConfig(config); } void KateViInputMode::updateConfig() { KateViewConfig *cfg = view()->config(); // whether relative line numbers should be used or not. m_relLineNumbers = cfg->viRelativeLineNumbers(); if (m_activated) { viewInternal()->iconBorder()->setRelLineNumbersOn(m_relLineNumbers); } } void KateViInputMode::readWriteChanged(bool) { // nothing todo } void KateViInputMode::find() { showViModeEmulatedCommandBar(); viModeEmulatedCommandBar()->init(KateVi::EmulatedCommandBar::SearchForward); } void KateViInputMode::findSelectedForwards() { m_viModeManager->searcher()->findNext(); } void KateViInputMode::findSelectedBackwards() { m_viModeManager->searcher()->findPrevious(); } void KateViInputMode::findReplace() { showViModeEmulatedCommandBar(); viModeEmulatedCommandBar()->init(KateVi::EmulatedCommandBar::SearchForward); } void KateViInputMode::findNext() { m_viModeManager->searcher()->findNext(); } void KateViInputMode::findPrevious() { m_viModeManager->searcher()->findPrevious(); } void KateViInputMode::activateCommandLine() { showViModeEmulatedCommandBar(); viModeEmulatedCommandBar()->init(KateVi::EmulatedCommandBar::Command); } void KateViInputMode::showViModeEmulatedCommandBar() { view()->bottomViewBar()->addBarWidget(viModeEmulatedCommandBar()); view()->bottomViewBar()->showBarWidget(viModeEmulatedCommandBar()); } KateVi::EmulatedCommandBar *KateViInputMode::viModeEmulatedCommandBar() { if (!m_viModeEmulatedCommandBar) { m_viModeEmulatedCommandBar = new KateVi::EmulatedCommandBar(this, m_viModeManager, view()); m_viModeEmulatedCommandBar->hide(); } return m_viModeEmulatedCommandBar; } void KateViInputMode::updateRendererConfig() { // do nothing } bool KateViInputMode::keyPress(QKeyEvent *e) { if (m_nextKeypressIsOverriddenShortCut) { // This is just the replay of a shortcut that we stole, this time as a QKeyEvent. // Ignore it, as we'll have already handled it via stealKey()! m_nextKeypressIsOverriddenShortCut = false; return true; } if (m_viModeManager->handleKeypress(e)) { emit view()->viewModeChanged(view(), viewMode()); return true; } return false; } bool KateViInputMode::blinkCaret() const { return false; } KateRenderer::caretStyles KateViInputMode::caretStyle() const { return m_caret; } void KateViInputMode::toggleInsert() { // do nothing } void KateViInputMode::launchInteractiveCommand(const QString &) { // do nothing so far } QString KateViInputMode::bookmarkLabel(int line) const { return m_viModeManager->marks()->getMarksOnTheLine(line); } void KateViInputMode::setCaretStyle(const KateRenderer::caretStyles caret) { if (m_caret != caret) { m_caret = caret; view()->renderer()->setCaretStyle(m_caret); view()->renderer()->setDrawCaret(true); viewInternal()->paintCursor(); } } diff --git a/src/mode/katewildcardmatcher.cpp b/src/mode/katewildcardmatcher.cpp index 7292c36b..ac7e8909 100644 --- a/src/mode/katewildcardmatcher.cpp +++ b/src/mode/katewildcardmatcher.cpp @@ -1,78 +1,78 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Sebastian Pipping * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katewildcardmatcher.h" #include #include namespace KateWildcardMatcher { bool exactMatch(const QString &candidate, const QString &wildcard, int candidatePosFromRight, int wildcardPosFromRight, bool caseSensitive = true) { for (; wildcardPosFromRight >= 0; wildcardPosFromRight--) { const ushort ch = wildcard[wildcardPosFromRight].unicode(); switch (ch) { - case L'*': - if (candidatePosFromRight == -1) { - break; - } + case L'*': + if (candidatePosFromRight == -1) { + break; + } - if (wildcardPosFromRight == 0) { + if (wildcardPosFromRight == 0) { + return true; + } + + // Eat all we can and go back as far as we have to + for (int j = -1; j <= candidatePosFromRight; j++) { + if (exactMatch(candidate, wildcard, j, wildcardPosFromRight - 1)) { return true; } + } + return false; - // Eat all we can and go back as far as we have to - for (int j = -1; j <= candidatePosFromRight; j++) { - if (exactMatch(candidate, wildcard, j, wildcardPosFromRight - 1)) { - return true; - } - } + case L'?': + if (candidatePosFromRight == -1) { return false; + } - case L'?': - if (candidatePosFromRight == -1) { - return false; - } + candidatePosFromRight--; + break; - candidatePosFromRight--; - break; - - default: - if (candidatePosFromRight == -1) { - return false; - } + default: + if (candidatePosFromRight == -1) { + return false; + } - const ushort candidateCh = candidate[candidatePosFromRight].unicode(); - const bool match = caseSensitive ? (candidateCh == ch) : (QChar::toLower(candidateCh) == QChar::toLower(ch)); - if (match) { - candidatePosFromRight--; - } else { - return false; - } + const ushort candidateCh = candidate[candidatePosFromRight].unicode(); + const bool match = caseSensitive ? (candidateCh == ch) : (QChar::toLower(candidateCh) == QChar::toLower(ch)); + if (match) { + candidatePosFromRight--; + } else { + return false; + } } } return true; } bool exactMatch(const QString &candidate, const QString &wildcard, bool caseSensitive) { return exactMatch(candidate, wildcard, candidate.length() - 1, wildcard.length() - 1, caseSensitive); } } diff --git a/src/render/katerenderer.cpp b/src/render/katerenderer.cpp index 4deb5e6e..ac2ea66a 100644 --- a/src/render/katerenderer.cpp +++ b/src/render/katerenderer.cpp @@ -1,1202 +1,1201 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Mirko Stocker Copyright (C) 2003-2005 Hamish Rodda Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2013 Andrey Matveyakin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katerenderer.h" #include "inlinenotedata.h" #include "katebuffer.h" #include "kateconfig.h" #include "katedocument.h" #include "katehighlight.h" #include "katerenderrange.h" #include "katetextlayout.h" #include "kateview.h" #include "ktexteditor/inlinenote.h" #include "ktexteditor/inlinenoteprovider.h" #include "katepartdebug.h" #include #include #include #include #include #include #include // qCeil static const QChar tabChar(QLatin1Char('\t')); static const QChar spaceChar(QLatin1Char(' ')); static const QChar nbSpaceChar(0xa0); // non-breaking space KateRenderer::KateRenderer(KTextEditor::DocumentPrivate *doc, Kate::TextFolding &folding, KTextEditor::ViewPrivate *view) : m_doc(doc) , m_folding(folding) , m_view(view) , m_tabWidth(m_doc->config()->tabWidth()) , m_indentWidth(m_doc->config()->indentationWidth()) , m_caretStyle(KateRenderer::Line) , m_drawCaret(true) , m_showSelections(true) , m_showTabs(true) , m_showSpaces(KateDocumentConfig::Trailing) , m_showNonPrintableSpaces(false) , m_printerFriendly(false) , m_config(new KateRendererConfig(this)) , m_font(m_config->baseFont()) , m_fontMetrics(m_font) { updateAttributes(); // initialize with a sane font height updateFontHeight(); // make the proper calculation for markerSize updateMarkerSize(); } KateRenderer::~KateRenderer() { delete m_config; } void KateRenderer::updateAttributes() { m_attributes = m_doc->highlight()->attributes(config()->schema()); } KTextEditor::Attribute::Ptr KateRenderer::attribute(uint pos) const { if (pos < (uint)m_attributes.count()) { return m_attributes[pos]; } return m_attributes[0]; } KTextEditor::Attribute::Ptr KateRenderer::specificAttribute(int context) const { if (context >= 0 && context < m_attributes.count()) { return m_attributes[context]; } return m_attributes[0]; } void KateRenderer::setDrawCaret(bool drawCaret) { m_drawCaret = drawCaret; } void KateRenderer::setCaretStyle(KateRenderer::caretStyles style) { m_caretStyle = style; } void KateRenderer::setShowTabs(bool showTabs) { m_showTabs = showTabs; } void KateRenderer::setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces) { m_showSpaces = showSpaces; } void KateRenderer::setShowNonPrintableSpaces(const bool on) { m_showNonPrintableSpaces = on; } void KateRenderer::setTabWidth(int tabWidth) { m_tabWidth = tabWidth; } bool KateRenderer::showIndentLines() const { return m_config->showIndentationLines(); } void KateRenderer::setShowIndentLines(bool showIndentLines) { m_config->setShowIndentationLines(showIndentLines); } void KateRenderer::setIndentWidth(int indentWidth) { m_indentWidth = indentWidth; } void KateRenderer::setShowSelections(bool showSelections) { m_showSelections = showSelections; } void KateRenderer::increaseFontSizes(qreal step) { QFont f(config()->baseFont()); f.setPointSizeF(f.pointSizeF() + step); config()->setFont(f); } void KateRenderer::resetFontSizes() { QFont f(KateRendererConfig::global()->baseFont()); config()->setFont(f); } void KateRenderer::decreaseFontSizes(qreal step) { QFont f(config()->baseFont()); if ((f.pointSizeF() - step) > 0) { f.setPointSizeF(f.pointSizeF() - step); } config()->setFont(f); } bool KateRenderer::isPrinterFriendly() const { return m_printerFriendly; } void KateRenderer::setPrinterFriendly(bool printerFriendly) { m_printerFriendly = printerFriendly; setShowTabs(false); setShowSpaces(KateDocumentConfig::None); setShowSelections(false); setDrawCaret(false); } void KateRenderer::paintTextLineBackground(QPainter &paint, KateLineLayoutPtr layout, int currentViewLine, int xStart, int xEnd) { if (isPrinterFriendly()) { return; } // Normal background color QColor backgroundColor(config()->backgroundColor()); // paint the current line background if we're on the current line QColor currentLineColor = config()->highlightedLineColor(); // Check for mark background int markRed = 0, markGreen = 0, markBlue = 0, markCount = 0; // Retrieve marks for this line uint mrk = m_doc->mark(layout->line()); if (mrk) { for (uint bit = 0; bit < 32; bit++) { KTextEditor::MarkInterface::MarkTypes markType = (KTextEditor::MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { QColor markColor = config()->lineMarkerColor(markType); if (markColor.isValid()) { markCount++; markRed += markColor.red(); markGreen += markColor.green(); markBlue += markColor.blue(); } } } // for } // Marks if (markCount) { markRed /= markCount; markGreen /= markCount; markBlue /= markCount; backgroundColor.setRgb(int((backgroundColor.red() * 0.9) + (markRed * 0.1)), int((backgroundColor.green() * 0.9) + (markGreen * 0.1)), int((backgroundColor.blue() * 0.9) + (markBlue * 0.1))); } // Draw line background paint.fillRect(0, 0, xEnd - xStart, lineHeight() * layout->viewLineCount(), backgroundColor); // paint the current line background if we're on the current line if (currentViewLine != -1) { if (markCount) { markRed /= markCount; markGreen /= markCount; markBlue /= markCount; currentLineColor.setRgb(int((currentLineColor.red() * 0.9) + (markRed * 0.1)), int((currentLineColor.green() * 0.9) + (markGreen * 0.1)), int((currentLineColor.blue() * 0.9) + (markBlue * 0.1))); } paint.fillRect(0, lineHeight() * currentViewLine, xEnd - xStart, lineHeight(), currentLineColor); } } void KateRenderer::paintTabstop(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); paint.setPen(pen); int dist = spaceWidth() * 0.3; QPoint points[8]; points[0] = QPoint(x - dist, y - dist); points[1] = QPoint(x, y); points[2] = QPoint(x, y); points[3] = QPoint(x - dist, y + dist); x += spaceWidth() / 3.0; points[4] = QPoint(x - dist, y - dist); points[5] = QPoint(x, y); points[6] = QPoint(x, y); points[7] = QPoint(x - dist, y + dist); paint.drawLines(points, 4); paint.setPen(penBackup); } void KateRenderer::paintSpace(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(m_markerSize); pen.setCapStyle(Qt::RoundCap); paint.setPen(pen); paint.setRenderHint(QPainter::Antialiasing, true); paint.drawPoint(QPointF(x, y)); paint.setPen(penBackup); paint.setRenderHint(QPainter::Antialiasing, false); } void KateRenderer::paintNonBreakSpace(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); paint.setPen(pen); const int height = fontHeight(); const int width = spaceWidth(); QPoint points[6]; points[0] = QPoint(x + width / 10, y + height / 4); points[1] = QPoint(x + width / 10, y + height / 3); points[2] = QPoint(x + width / 10, y + height / 3); points[3] = QPoint(x + width - width / 10, y + height / 3); points[4] = QPoint(x + width - width / 10, y + height / 3); points[5] = QPoint(x + width - width / 10, y + height / 4); paint.drawLines(points, 3); paint.setPen(penBackup); } void KateRenderer::paintNonPrintableSpaces(QPainter &paint, qreal x, qreal y, const QChar &chr) { paint.save(); QPen pen(config()->spellingMistakeLineColor()); pen.setWidthF(qMax(1.0, spaceWidth() * 0.1)); paint.setPen(pen); const int height = fontHeight(); const int width = m_fontMetrics.boundingRect(chr).width(); const int offset = spaceWidth() * 0.1; QPoint points[8]; points[0] = QPoint(x - offset, y + offset); points[1] = QPoint(x + width + offset, y + offset); points[2] = QPoint(x + width + offset, y + offset); points[3] = QPoint(x + width + offset, y - height - offset); points[4] = QPoint(x + width + offset, y - height - offset); points[5] = QPoint(x - offset, y - height - offset); points[6] = QPoint(x - offset, y - height - offset); points[7] = QPoint(x - offset, y + offset); paint.drawLines(points, 4); paint.restore(); } void KateRenderer::paintIndentMarker(QPainter &paint, uint x, uint y /*row*/) { QPen penBackup(paint.pen()); QPen myPen(config()->indentationLineColor()); static const QVector dashPattern = QVector() << 1 << 1; myPen.setDashPattern(dashPattern); if (y % 2) { myPen.setDashOffset(1); } paint.setPen(myPen); const int height = fontHeight(); const int top = 0; const int bottom = height - 1; QPainter::RenderHints renderHints = paint.renderHints(); paint.setRenderHints(renderHints, false); paint.drawLine(x + 2, top, x + 2, bottom); paint.setRenderHints(renderHints, true); paint.setPen(penBackup); } static bool rangeLessThanForRenderer(const Kate::TextRange *a, const Kate::TextRange *b) { // compare Z-Depth first // smaller Z-Depths should win! if (a->zDepth() > b->zDepth()) { return true; } else if (a->zDepth() < b->zDepth()) { return false; } // end of a > end of b? if (a->end().toCursor() > b->end().toCursor()) { return true; } // if ends are equal, start of a < start of b? if (a->end().toCursor() == b->end().toCursor()) { return a->start().toCursor() < b->start().toCursor(); } return false; } QVector KateRenderer::decorationsForLine(const Kate::TextLine &textLine, int line, bool selectionsOnly, bool completionHighlight, bool completionSelected) const { // limit number of attributes we can highlight in reasonable time const int limitOfRanges = 1024; auto rangesWithAttributes = m_doc->buffer().rangesForLine(line, m_printerFriendly ? nullptr : m_view, true); if (rangesWithAttributes.size() > limitOfRanges) { rangesWithAttributes.clear(); } // Don't compute the highlighting if there isn't going to be any highlighting const auto &al = textLine->attributesList(); if (!(selectionsOnly || !al.isEmpty() || !rangesWithAttributes.isEmpty())) { return QVector(); } // Add the inbuilt highlighting to the list, limit with limitOfRanges RenderRangeVector renderRanges; if (!al.isEmpty()) { auto ¤tRange = renderRanges.pushNewRange(); for (int i = 0; i < std::min(al.count(), limitOfRanges); ++i) { if (al[i].length > 0 && al[i].attributeValue > 0) { currentRange.addRange(KTextEditor::Range(KTextEditor::Cursor(line, al[i].offset), al[i].length), specificAttribute(al[i].attributeValue)); } } } if (!completionHighlight) { // check for dynamic hl stuff const QSet *rangesMouseIn = m_view ? m_view->rangesMouseIn() : nullptr; const QSet *rangesCaretIn = m_view ? m_view->rangesCaretIn() : nullptr; bool anyDynamicHlsActive = m_view && (!rangesMouseIn->empty() || !rangesCaretIn->empty()); // sort all ranges, we want that the most specific ranges win during rendering, multiple equal ranges are kind of random, still better than old smart rangs behavior ;) std::sort(rangesWithAttributes.begin(), rangesWithAttributes.end(), rangeLessThanForRenderer); // loop over all ranges for (int i = 0; i < rangesWithAttributes.size(); ++i) { // real range Kate::TextRange *kateRange = rangesWithAttributes[i]; // calculate attribute, default: normal attribute KTextEditor::Attribute::Ptr attribute = kateRange->attribute(); if (anyDynamicHlsActive) { // check mouse in if (KTextEditor::Attribute::Ptr attributeMouseIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)) { if (rangesMouseIn->contains(kateRange)) { attribute = attributeMouseIn; } } // check caret in if (KTextEditor::Attribute::Ptr attributeCaretIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateCaretIn)) { if (rangesCaretIn->contains(kateRange)) { attribute = attributeCaretIn; } } } // span range renderRanges.pushNewRange().addRange(*kateRange, attribute); } } // Add selection highlighting if we're creating the selection decorations if ((m_view && selectionsOnly && showSelections() && m_view->selection()) || (completionHighlight && completionSelected) || (m_view && m_view->blockSelection())) { auto ¤tRange = renderRanges.pushNewRange(); // Set up the selection background attribute TODO: move this elsewhere, eg. into the config? static KTextEditor::Attribute::Ptr backgroundAttribute; if (!backgroundAttribute) { backgroundAttribute = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); } backgroundAttribute->setBackground(config()->selectionColor()); backgroundAttribute->setForeground(attribute(KTextEditor::dsNormal)->selectedForeground().color()); // Create a range for the current selection if (completionHighlight && completionSelected) { currentRange.addRange(KTextEditor::Range(line, 0, line + 1, 0), backgroundAttribute); } else if (m_view->blockSelection() && m_view->selectionRange().overlapsLine(line)) { currentRange.addRange(m_doc->rangeOnLine(m_view->selectionRange(), line), backgroundAttribute); } else { currentRange.addRange(m_view->selectionRange(), backgroundAttribute); } } // no render ranges, nothing to do, else we loop below endless! if (renderRanges.isEmpty()) { return QVector(); } // Calculate the range which we need to iterate in order to get the highlighting for just this line KTextEditor::Cursor currentPosition, endPosition; if (m_view && selectionsOnly) { if (m_view->blockSelection()) { KTextEditor::Range subRange = m_doc->rangeOnLine(m_view->selectionRange(), line); currentPosition = subRange.start(); endPosition = subRange.end(); } else { KTextEditor::Range rangeNeeded = m_view->selectionRange() & KTextEditor::Range(line, 0, line + 1, 0); currentPosition = qMax(KTextEditor::Cursor(line, 0), rangeNeeded.start()); endPosition = qMin(KTextEditor::Cursor(line + 1, 0), rangeNeeded.end()); } } else { currentPosition = KTextEditor::Cursor(line, 0); endPosition = KTextEditor::Cursor(line + 1, 0); } // Main iterative loop. This walks through each set of highlighting ranges, and stops each // time the highlighting changes. It then creates the corresponding QTextLayout::FormatRanges. QVector newHighlight; while (currentPosition < endPosition) { renderRanges.advanceTo(currentPosition); if (!renderRanges.hasAttribute()) { // No attribute, don't need to create a FormatRange for this text range currentPosition = renderRanges.nextBoundary(); continue; } KTextEditor::Cursor nextPosition = renderRanges.nextBoundary(); // Create the format range and populate with the correct start, length and format info QTextLayout::FormatRange fr; fr.start = currentPosition.column(); if (nextPosition < endPosition || endPosition.line() <= line) { fr.length = nextPosition.column() - currentPosition.column(); } else { // +1 to force background drawing at the end of the line when it's warranted fr.length = textLine->length() - currentPosition.column() + 1; } KTextEditor::Attribute::Ptr a = renderRanges.generateAttribute(); if (a) { fr.format = *a; if (selectionsOnly) { assignSelectionBrushesFromAttribute(fr, *a); } } newHighlight.append(fr); currentPosition = nextPosition; } return newHighlight; } void KateRenderer::assignSelectionBrushesFromAttribute(QTextLayout::FormatRange &target, const KTextEditor::Attribute &attribute) const { if (attribute.hasProperty(SelectedForeground)) { target.format.setForeground(attribute.selectedForeground()); } if (attribute.hasProperty(SelectedBackground)) { target.format.setBackground(attribute.selectedBackground()); } } void KateRenderer::paintTextLine(QPainter &paint, KateLineLayoutPtr range, int xStart, int xEnd, const KTextEditor::Cursor *cursor, PaintTextLineFlags flags) { Q_ASSERT(range->isValid()); // qCDebug(LOG_KTE)<<"KateRenderer::paintTextLine"; // font data const QFontMetricsF &fm = m_fontMetrics; int currentViewLine = -1; if (cursor && cursor->line() == range->line()) { currentViewLine = range->viewLineForColumn(cursor->column()); } paintTextLineBackground(paint, range, currentViewLine, xStart, xEnd); // Draws the dashed underline at the start of a folded block of text. if (!(flags & SkipDrawFirstInvisibleLineUnderlined) && range->startsInvisibleBlock()) { QPen pen(config()->foldingColor()); pen.setCosmetic(true); pen.setStyle(Qt::DashLine); pen.setDashOffset(xStart); pen.setWidth(2); paint.setPen(pen); paint.drawLine(0, (lineHeight() * range->viewLineCount()) - 2, xEnd - xStart, (lineHeight() * range->viewLineCount()) - 2); } if (range->layout()) { bool drawSelection = m_view && m_view->selection() && showSelections() && m_view->selectionRange().overlapsLine(range->line()); // Draw selection in block selection mode. We need 2 kinds of selections that QTextLayout::draw can't render: // - past-end-of-line selection and // - 0-column-wide selection (used to indicate where text will be typed) if (drawSelection && m_view->blockSelection()) { int selectionStartColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().start())); int selectionEndColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().end())); QBrush selectionBrush = config()->selectionColor(); if (selectionStartColumn != selectionEndColumn) { KateTextLayout lastLine = range->viewLine(range->viewLineCount() - 1); if (selectionEndColumn > lastLine.startCol()) { int selectionStartX = (selectionStartColumn > lastLine.startCol()) ? cursorToX(lastLine, selectionStartColumn, true) : 0; int selectionEndX = cursorToX(lastLine, selectionEndColumn, true); paint.fillRect(QRect(selectionStartX - xStart, (int)lastLine.lineLayout().y(), selectionEndX - selectionStartX, lineHeight()), selectionBrush); } } else { const int selectStickWidth = 2; KateTextLayout selectionLine = range->viewLine(range->viewLineForColumn(selectionStartColumn)); int selectionX = cursorToX(selectionLine, selectionStartColumn, true); paint.fillRect(QRect(selectionX - xStart, (int)selectionLine.lineLayout().y(), selectStickWidth, lineHeight()), selectionBrush); } } QVector additionalFormats; if (range->length() > 0) { // We may have changed the pen, be absolutely sure it gets set back to // normal foreground color before drawing text for text that does not // set the pen color paint.setPen(attribute(KTextEditor::dsNormal)->foreground().color()); // Draw the text :) if (drawSelection) { additionalFormats = decorationsForLine(range->textLine(), range->line(), true); range->layout()->draw(&paint, QPoint(-xStart, 0), additionalFormats); } else { range->layout()->draw(&paint, QPoint(-xStart, 0)); } } QBrush backgroundBrush; bool backgroundBrushSet = false; // Loop each individual line for additional text decoration etc. QVectorIterator it = range->layout()->formats(); QVectorIterator it2 = additionalFormats; for (int i = 0; i < range->viewLineCount(); ++i) { KateTextLayout line = range->viewLine(i); bool haveBackground = false; // Determine the background to use, if any, for the end of this view line backgroundBrushSet = false; while (it2.hasNext()) { const QTextLayout::FormatRange &fr = it2.peekNext(); if (fr.start > line.endCol()) { break; } if (fr.start + fr.length > line.endCol()) { if (fr.format.hasProperty(QTextFormat::BackgroundBrush)) { backgroundBrushSet = true; backgroundBrush = fr.format.background(); } haveBackground = true; break; } it2.next(); } while (!haveBackground && it.hasNext()) { const QTextLayout::FormatRange &fr = it.peekNext(); if (fr.start > line.endCol()) { break; } if (fr.start + fr.length > line.endCol()) { if (fr.format.hasProperty(QTextFormat::BackgroundBrush)) { backgroundBrushSet = true; backgroundBrush = fr.format.background(); } break; } it.next(); } // Draw selection or background color outside of areas where text is rendered if (!m_printerFriendly) { bool draw = false; QBrush drawBrush; if (m_view && m_view->selection() && !m_view->blockSelection() && m_view->lineEndSelected(line.end(true))) { draw = true; drawBrush = config()->selectionColor(); } else if (backgroundBrushSet && !m_view->blockSelection()) { draw = true; drawBrush = backgroundBrush; } if (draw) { int fillStartX = line.endX() - line.startX() + line.xOffset() - xStart; int fillStartY = lineHeight() * i; int width = xEnd - xStart - fillStartX; int height = lineHeight(); // reverse X for right-aligned lines if (range->layout()->textOption().alignment() == Qt::AlignRight) { fillStartX = 0; } if (width > 0) { QRect area(fillStartX, fillStartY, width, height); paint.fillRect(area, drawBrush); } } // Draw indent lines if (showIndentLines() && i == 0) { const qreal w = spaceWidth(); const int lastIndentColumn = range->textLine()->indentDepth(m_tabWidth); for (int x = m_indentWidth; x < lastIndentColumn; x += m_indentWidth) { paintIndentMarker(paint, x * w + 1 - xStart, range->line()); } } } // draw an open box to mark non-breaking spaces const QString &text = range->textLine()->string(); int y = lineHeight() * i + fm.ascent() - fm.strikeOutPos(); int nbSpaceIndex = text.indexOf(nbSpaceChar, line.lineLayout().xToCursor(xStart)); while (nbSpaceIndex != -1 && nbSpaceIndex < line.endCol()) { int x = line.lineLayout().cursorToX(nbSpaceIndex); if (x > xEnd) { break; } paintNonBreakSpace(paint, x - xStart, y); nbSpaceIndex = text.indexOf(nbSpaceChar, nbSpaceIndex + 1); } // draw tab stop indicators if (showTabs()) { int tabIndex = text.indexOf(tabChar, line.lineLayout().xToCursor(xStart)); while (tabIndex != -1 && tabIndex < line.endCol()) { int x = line.lineLayout().cursorToX(tabIndex); if (x > xEnd) { break; } paintTabstop(paint, x - xStart + spaceWidth() / 2.0, y); tabIndex = text.indexOf(tabChar, tabIndex + 1); } } // draw trailing spaces if (showSpaces() != KateDocumentConfig::None) { int spaceIndex = line.endCol() - 1; const int trailingPos = showSpaces() == KateDocumentConfig::All ? 0 : qMax(range->textLine()->lastChar(), 0); if (spaceIndex >= trailingPos) { for (; spaceIndex >= line.startCol(); --spaceIndex) { if (!text.at(spaceIndex).isSpace()) { if (showSpaces() == KateDocumentConfig::Trailing) break; else continue; } if (text.at(spaceIndex) != QLatin1Char('\t') || !showTabs()) { if (range->layout()->textOption().alignment() == Qt::AlignRight) { // Draw on left for RTL lines paintSpace(paint, line.lineLayout().cursorToX(spaceIndex) - xStart - spaceWidth() / 2.0, y); } else { paintSpace(paint, line.lineLayout().cursorToX(spaceIndex) - xStart + spaceWidth() / 2.0, y); } } } } } if (showNonPrintableSpaces()) { const int y = lineHeight() * i + fm.ascent(); static const QRegularExpression nonPrintableSpacesRegExp(QStringLiteral("[\\x{2000}-\\x{200F}\\x{2028}-\\x{202F}\\x{205F}-\\x{2064}\\x{206A}-\\x{206F}]")); QRegularExpressionMatchIterator i = nonPrintableSpacesRegExp.globalMatch(text, line.lineLayout().xToCursor(xStart)); while (i.hasNext()) { const int charIndex = i.next().capturedStart(); const int x = line.lineLayout().cursorToX(charIndex); if (x > xEnd) { break; } paintNonPrintableSpaces(paint, x - xStart, y, text[charIndex]); } } } // Draw inline notes if (!isPrinterFriendly()) { const auto inlineNotes = m_view->inlineNotes(range->line()); for (const auto &inlineNoteData : inlineNotes) { KTextEditor::InlineNote inlineNote(inlineNoteData); const int column = inlineNote.position().column(); int viewLine = range->viewLineForColumn(column); // Determine the position where to paint the note. // We start by getting the x coordinate of cursor placed to the column. qreal x = range->viewLine(viewLine).lineLayout().cursorToX(column) - xStart; int textLength = range->length(); if (column == 0 || column < textLength) { // If the note is inside text or at the beginning, then there is a hole in the text where the // note should be painted and the cursor gets placed at the right side of it. So we have to // subtract the width of the note to get to left side of the hole. x -= inlineNote.width(); } else { // If the note is outside the text, then the X coordinate is located at the end of the line. // Add appropriate amount of spaces to reach the required column. x += spaceWidth() * (column - textLength); } qreal y = lineHeight() * viewLine; // Paint the note paint.save(); paint.translate(x, y); inlineNote.provider()->paintInlineNote(inlineNote, paint); paint.restore(); } } // draw word-wrap-honor-indent filling if ((range->viewLineCount() > 1) && range->shiftX() && (range->shiftX() > xStart)) { if (backgroundBrushSet) paint.fillRect(0, lineHeight(), range->shiftX() - xStart, lineHeight() * (range->viewLineCount() - 1), backgroundBrush); paint.fillRect(0, lineHeight(), range->shiftX() - xStart, lineHeight() * (range->viewLineCount() - 1), QBrush(config()->wordWrapMarkerColor(), Qt::Dense4Pattern)); } // Draw caret if (drawCaret() && cursor && range->includesCursor(*cursor)) { int caretWidth, lineWidth = 2; QColor color; QTextLine line = range->layout()->lineForTextPosition(qMin(cursor->column(), range->length())); // Determine the caret's style caretStyles style = caretStyle(); // Make the caret the desired width if (style == Line) { caretWidth = lineWidth; } else if (line.isValid() && cursor->column() < range->length()) { caretWidth = int(line.cursorToX(cursor->column() + 1) - line.cursorToX(cursor->column())); if (caretWidth < 0) { caretWidth = -caretWidth; } } else { caretWidth = spaceWidth(); } // Determine the color if (m_caretOverrideColor.isValid()) { // Could actually use the real highlighting system for this... // would be slower, but more accurate for corner cases color = m_caretOverrideColor; } else { // search for the FormatRange that includes the cursor const auto formatRanges = range->layout()->formats(); for (const QTextLayout::FormatRange &r : formatRanges) { if ((r.start <= cursor->column()) && ((r.start + r.length) > cursor->column())) { // check for Qt::NoBrush, as the returned color is black() and no invalid QColor QBrush foregroundBrush = r.format.foreground(); if (foregroundBrush != Qt::NoBrush) { color = r.format.foreground().color(); } break; } } // still no color found, fall back to default style if (!color.isValid()) { color = attribute(KTextEditor::dsNormal)->foreground().color(); } } paint.save(); switch (style) { - case Line: - paint.setPen(QPen(color, caretWidth)); - break; - case Block: - // use a gray caret so it's possible to see the character - color.setAlpha(128); - paint.setPen(QPen(color, caretWidth)); - break; - case Underline: - break; - case Half: - color.setAlpha(128); - paint.setPen(QPen(color, caretWidth)); - break; + case Line: + paint.setPen(QPen(color, caretWidth)); + break; + case Block: + // use a gray caret so it's possible to see the character + color.setAlpha(128); + paint.setPen(QPen(color, caretWidth)); + break; + case Underline: + break; + case Half: + color.setAlpha(128); + paint.setPen(QPen(color, caretWidth)); + break; } if (cursor->column() <= range->length()) { range->layout()->drawCursor(&paint, QPoint(-xStart, 0), cursor->column(), caretWidth); } else { // Off the end of the line... must be block mode. Draw the caret ourselves. const KateTextLayout &lastLine = range->viewLine(range->viewLineCount() - 1); int x = cursorToX(lastLine, KTextEditor::Cursor(range->line(), cursor->column()), true); if ((x >= xStart) && (x <= xEnd)) { paint.fillRect(x - xStart, (int)lastLine.lineLayout().y(), caretWidth, lineHeight(), color); } } paint.restore(); } } // show word wrap marker if desirable if ((!isPrinterFriendly()) && config()->wordWrapMarker()) { const QPainter::RenderHints backupRenderHints = paint.renderHints(); paint.setPen(config()->wordWrapMarkerColor()); int _x = qreal(m_doc->config()->wordWrapAt()) * fm.horizontalAdvance(QLatin1Char('x')) - xStart; paint.drawLine(_x, 0, _x, lineHeight()); paint.setRenderHints(backupRenderHints); } } uint KateRenderer::fontHeight() const { return m_fontHeight; } uint KateRenderer::documentHeight() const { return m_doc->lines() * lineHeight(); } int KateRenderer::lineHeight() const { return fontHeight(); } bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const { bool hasSel = false; if (m_view->selection() && !m_view->blockSelection()) { if (m_view->lineIsSelection(line)) { start = m_view->selectionRange().start().column(); end = m_view->selectionRange().end().column(); hasSel = true; } else if (line == m_view->selectionRange().start().line()) { start = m_view->selectionRange().start().column(); end = lineLength; hasSel = true; } else if (m_view->selectionRange().containsLine(line)) { start = 0; end = lineLength; hasSel = true; } else if (line == m_view->selectionRange().end().line()) { start = 0; end = m_view->selectionRange().end().column(); hasSel = true; } } else if (m_view->lineHasSelected(line)) { start = m_view->selectionRange().start().column(); end = m_view->selectionRange().end().column(); hasSel = true; } if (start > end) { int temp = end; end = start; start = temp; } return hasSel; } void KateRenderer::updateConfig() { // update the attribute list pointer updateAttributes(); // update font height, do this before we update the view! updateFontHeight(); // trigger view update, if any! if (m_view) { m_view->updateRendererConfig(); } } void KateRenderer::updateFontHeight() { /** * cache font + metrics */ m_font = config()->baseFont(); m_fontMetrics = QFontMetricsF(m_font); /** * ensure minimal height of one pixel to not fall in the div by 0 trap somewhere * * use a line spacing that matches the code in qt to layout/paint text * * see bug 403868 * https://github.com/qt/qtbase/blob/5.12/src/gui/text/qtextlayout.cpp (line 2270 at the moment) where the text height is set as: * * qreal height = maxY + fontHeight - minY; * * with fontHeight: * * qreal fontHeight = font.ascent() + font.descent(); */ m_fontHeight = qMax(1, qCeil(m_fontMetrics.ascent() + m_fontMetrics.descent())); } void KateRenderer::updateMarkerSize() { // marker size will be calculated over the value defined // on dialog m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5)); } qreal KateRenderer::spaceWidth() const { return m_fontMetrics.horizontalAdvance(spaceChar); } void KateRenderer::layoutLine(KateLineLayoutPtr lineLayout, int maxwidth, bool cacheLayout) const { // if maxwidth == -1 we have no wrap Kate::TextLine textLine = lineLayout->textLine(); Q_ASSERT(textLine); QTextLayout *l = lineLayout->layout(); if (!l) { l = new QTextLayout(textLine->string(), m_font); } else { l->setText(textLine->string()); l->setFont(m_font); } l->setCacheEnabled(cacheLayout); // Initial setup of the QTextLayout. // Tab width QTextOption opt; opt.setFlags(QTextOption::IncludeTrailingSpaces); opt.setTabStopDistance(m_tabWidth * m_fontMetrics.horizontalAdvance(spaceChar)); opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); // Find the first strong character in the string. // If it is an RTL character, set the base layout direction of the string to RTL. // // See http://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3). // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol" // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3 // by itself. If this ever change in Qt, the next code block could be removed. if (isLineRightToLeft(lineLayout)) { opt.setAlignment(Qt::AlignRight); opt.setTextDirection(Qt::RightToLeft); } else { opt.setAlignment(Qt::AlignLeft); opt.setTextDirection(Qt::LeftToRight); } l->setTextOption(opt); // Syntax highlighting, inbuilt and arbitrary QVector decorations = decorationsForLine(textLine, lineLayout->line()); int firstLineOffset = 0; if (!isPrinterFriendly()) { const auto inlineNotes = m_view->inlineNotes(lineLayout->line()); for (const KTextEditor::InlineNote &inlineNote : inlineNotes) { const int column = inlineNote.position().column(); int width = inlineNote.width(); // Make space for every inline note. // If it is on column 0 (at the beginning of the line), we must offset the first line. // If it is inside the text, we use absolute letter spacing to create space for it between the two letters. // If it is outside of the text, we don't have to make space for it. if (column == 0) { firstLineOffset = width; } else if (column < l->text().length()) { QTextCharFormat text_char_format; text_char_format.setFontLetterSpacing(width); text_char_format.setFontLetterSpacingType(QFont::AbsoluteSpacing); decorations.append(QTextLayout::FormatRange {column - 1, 1, text_char_format}); } } } l->setFormats(decorations); // Begin layouting l->beginLayout(); int height = 0; int shiftX = 0; bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0); - forever - { + forever { QTextLine line = l->createLine(); if (!line.isValid()) { break; } if (maxwidth > 0) { line.setLineWidth(maxwidth); } // we include the leading, this must match the ::updateFontHeight code! line.setLeadingIncluded(true); line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height)); if (needShiftX && line.width() > 0) { needShiftX = false; // Determine x offset for subsequent-lines-of-paragraph indenting int pos = textLine->nextNonSpaceChar(0); if (pos > 0) { shiftX = (int)line.cursorToX(pos); } // check for too deep shift value and limit if necessary if (shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) { shiftX = 0; } // if shiftX > 0, the maxwidth has to adapted maxwidth -= shiftX; lineLayout->setShiftX(shiftX); } height += lineHeight(); } l->endLayout(); lineLayout->setLayout(l); } // 1) QString::isRightToLeft() sux // 2) QString::isRightToLeft() is marked as internal (WTF?) // 3) QString::isRightToLeft() does not seem to work on my setup // 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore: // 5) isStringRightToLeft() kicks ass bool KateRenderer::isLineRightToLeft(KateLineLayoutPtr lineLayout) const { QString s = lineLayout->textLine()->string(); int i = 0; // borrowed from QString::updateProperties() while (i != s.length()) { QChar c = s.at(i); switch (c.direction()) { - case QChar::DirL: - case QChar::DirLRO: - case QChar::DirLRE: - return false; - - case QChar::DirR: - case QChar::DirAL: - case QChar::DirRLO: - case QChar::DirRLE: - return true; - - default: - break; + case QChar::DirL: + case QChar::DirLRO: + case QChar::DirLRE: + return false; + + case QChar::DirR: + case QChar::DirAL: + case QChar::DirRLO: + case QChar::DirRLE: + return true; + + default: + break; } i++; } return false; #if 0 // or should we use the direction of the widget? QWidget *display = qobject_cast(view()->parent()); if (!display) { return false; } return display->layoutDirection() == Qt::RightToLeft; #endif } int KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const { return cursorToX(range, KTextEditor::Cursor(range.line(), col), returnPastLine); } int KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor &pos, bool returnPastLine) const { Q_ASSERT(range.isValid()); int x; if (range.lineLayout().width() > 0) { x = (int)range.lineLayout().cursorToX(pos.column()); } else { x = 0; } int over = pos.column() - range.endCol(); if (returnPastLine && over > 0) { x += over * spaceWidth(); } return x; } KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const { Q_ASSERT(range.isValid()); KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x)); // TODO wrong for RTL lines? if (returnPastLine && range.endCol(true) == -1 && x > range.width() + range.xOffset()) { ret.setColumn(ret.column() + ((x - (range.width() + range.xOffset())) / spaceWidth())); } return ret; } void KateRenderer::setCaretOverrideColor(const QColor &color) { m_caretOverrideColor = color; } diff --git a/src/render/katetextlayout.h b/src/render/katetextlayout.h index 65d8997a..f207bf05 100644 --- a/src/render/katetextlayout.h +++ b/src/render/katetextlayout.h @@ -1,107 +1,107 @@ /* This file is part of the KDE libraries Copyright (C) 2002-2005 Hamish Rodda Copyright (C) 2003 Anakim Border * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef _KATE_TEXTLAYOUT_H_ #define _KATE_TEXTLAYOUT_H_ #include #include "katelinelayout.h" /** * This class represents one visible line of text; with dynamic wrapping, * many KateTextLayouts can be needed to represent one actual line of text * (ie. one KateLineLayout) */ class KateTextLayout { friend class KateLineLayout; friend class KateLayoutCache; - template friend class QVector; + template friend class QVector; public: bool isValid() const; static KateTextLayout invalid(); int line() const; int virtualLine() const; /** Return the index of this visual line inside the document line (KateLineLayout). */ int viewLine() const; const QTextLine &lineLayout() const; KateLineLayoutPtr kateLineLayout() const; int startCol() const; KTextEditor::Cursor start() const; /** * Return the end column of this text line. * * \param indicateEOL set to true to return -1 if this layout is the * end of the line, otherwise false to return the end column number */ int endCol(bool indicateEOL = false) const; /** * Return the end position of this text line. * * \param indicateEOL set to true to return -1 if this layout is the * end of the line, otherwise false to return the end column number */ KTextEditor::Cursor end(bool indicateEOL = false) const; int length() const; bool isEmpty() const; bool wrap() const; bool isDirty() const; bool setDirty(bool dirty = true); int startX() const; int endX() const; int width() const; int xOffset() const; bool isRightToLeft() const; bool includesCursor(const KTextEditor::Cursor &realCursor) const; friend bool operator>(const KateLineLayout &r, const KTextEditor::Cursor &c); friend bool operator>=(const KateLineLayout &r, const KTextEditor::Cursor &c); friend bool operator<(const KateLineLayout &r, const KTextEditor::Cursor &c); friend bool operator<=(const KateLineLayout &r, const KTextEditor::Cursor &c); void debugOutput() const; private: explicit KateTextLayout(KateLineLayoutPtr line = KateLineLayoutPtr(), int viewLine = 0); KateLineLayoutPtr m_lineLayout; QTextLine m_textLayout; int m_viewLine; mutable int m_startX; bool m_invalidDirty = true; }; #endif diff --git a/src/schema/katecolortreewidget.cpp b/src/schema/katecolortreewidget.cpp index 96636482..44f24253 100644 --- a/src/schema/katecolortreewidget.cpp +++ b/src/schema/katecolortreewidget.cpp @@ -1,385 +1,385 @@ /* This file is part of the KDE libraries Copyright (C) 2012-2018 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katecolortreewidget.h" #include "katecategorydrawer.h" #include #include #include #include "katepartdebug.h" #include #include #include #include #include #include #include // BEGIN KateColorTreeItem class KateColorTreeItem : public QTreeWidgetItem { public: KateColorTreeItem(const KateColorItem &colorItem, QTreeWidgetItem *parent = nullptr) : QTreeWidgetItem(parent) , m_colorItem(colorItem) { setText(0, m_colorItem.name); if (!colorItem.whatsThis.isEmpty()) { setData(1, Qt::WhatsThisRole, colorItem.whatsThis); } if (!colorItem.useDefault) { setData(2, Qt::ToolTipRole, i18n("Use default color from the KDE color scheme")); } } QColor color() const { return m_colorItem.color; } void setColor(const QColor &c) { m_colorItem.color = c; } QColor defaultColor() const { return m_colorItem.defaultColor; } bool useDefaultColor() const { return m_colorItem.useDefault; } void setUseDefaultColor(bool useDefault) { m_colorItem.useDefault = useDefault; QString tooltip = useDefault ? QString() : i18n("Use default color from the KDE color scheme"); setData(2, Qt::ToolTipRole, tooltip); } QString key() { return m_colorItem.key; } KateColorItem colorItem() const { return m_colorItem; } private: KateColorItem m_colorItem; }; // END KateColorTreeItem // BEGIN KateColorTreeDelegate class KateColorTreeDelegate : public QStyledItemDelegate { public: KateColorTreeDelegate(KateColorTreeWidget *widget) : QStyledItemDelegate(widget) , m_tree(widget) { } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { QSize sh = QStyledItemDelegate::sizeHint(option, index); if (!index.parent().isValid()) { sh.rheight() += 2 * m_categoryDrawer.leftMargin(); } else { sh.rheight() += m_categoryDrawer.leftMargin(); } if (index.column() == 0) { sh.rwidth() += m_categoryDrawer.leftMargin(); } else if (index.column() == 1) { sh.rwidth() = 150; } else { sh.rwidth() += m_categoryDrawer.leftMargin(); } return sh; } QRect fullCategoryRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { QModelIndex i = index; if (i.parent().isValid()) { i = i.parent(); } QTreeWidgetItem *item = m_tree->itemFromIndex(i); QRect r = m_tree->visualItemRect(item); // adapt width r.setLeft(m_categoryDrawer.leftMargin()); r.setWidth(m_tree->viewport()->width() - m_categoryDrawer.leftMargin() - m_categoryDrawer.rightMargin()); // adapt height if (item->isExpanded() && item->childCount() > 0) { const int childCount = item->childCount(); const int h = sizeHint(option, index.model()->index(0, 0, index)).height(); r.setHeight(r.height() + childCount * h); } r.setTop(r.top() + m_categoryDrawer.leftMargin()); return r; } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { Q_ASSERT(index.isValid()); Q_ASSERT(index.column() >= 0 && index.column() <= 2); // BEGIN: draw toplevel items if (!index.parent().isValid()) { QStyleOptionViewItem opt(option); const QRegion cl = painter->clipRegion(); painter->setClipRect(opt.rect); opt.rect = fullCategoryRect(option, index); m_categoryDrawer.drawCategory(index, 0, opt, painter); painter->setClipRegion(cl); return; } // END: draw toplevel items // BEGIN: draw background of category for all other items { QStyleOptionViewItem opt(option); opt.rect = fullCategoryRect(option, index); const QRegion cl = painter->clipRegion(); QRect cr = option.rect; if (index.column() == 0) { if (m_tree->layoutDirection() == Qt::LeftToRight) { cr.setLeft(5); } else { cr.setRight(opt.rect.right()); } } painter->setClipRect(cr); m_categoryDrawer.drawCategory(index, 0, opt, painter); painter->setClipRegion(cl); painter->setRenderHint(QPainter::Antialiasing, false); } // END: draw background of category for all other items // paint the text QStyledItemDelegate::paint(painter, option, index); if (index.column() == 0) { return; } painter->setClipRect(option.rect); KateColorTreeItem *item = dynamic_cast(m_tree->itemFromIndex(index)); // BEGIN: draw color button if (index.column() == 1) { QColor color = item->useDefaultColor() ? item->defaultColor() : item->color(); QStyleOptionButton opt; opt.rect = option.rect; opt.palette = m_tree->palette(); m_tree->style()->drawControl(QStyle::CE_PushButton, &opt, painter, m_tree); opt.rect = m_tree->style()->subElementRect(QStyle::SE_PushButtonContents, &opt, m_tree); opt.rect.adjust(1, 1, -1, -1); painter->fillRect(opt.rect, color); qDrawShadePanel(painter, opt.rect, opt.palette, true, 1, nullptr); } // END: draw color button // BEGIN: draw reset icon if (index.column() == 2 && !item->useDefaultColor()) { // get right pixmap const bool enabled = (option.state & QStyle::State_MouseOver || option.state & QStyle::State_HasFocus); const QPixmap p = QIcon::fromTheme(QStringLiteral("edit-undo")).pixmap(16, 16, enabled ? QIcon::Normal : QIcon::Disabled); // compute rect with scaled sizes const QRect rect(option.rect.left() + 10, option.rect.top() + (option.rect.height() - p.height() / p.devicePixelRatio() + 1) / 2, p.width() / p.devicePixelRatio(), p.height() / p.devicePixelRatio()); painter->drawPixmap(rect, p); } // END: draw reset icon } private: KateColorTreeWidget *m_tree; KateCategoryDrawer m_categoryDrawer; }; // END KateColorTreeDelegate KateColorTreeWidget::KateColorTreeWidget(QWidget *parent) : QTreeWidget(parent) { setItemDelegate(new KateColorTreeDelegate(this)); QStringList headers; headers << QString() // i18nc("@title:column the color name", "Color Role") << QString() // i18nc("@title:column a color button", "Color") << QString(); // i18nc("@title:column use default color", "Reset") setHeaderLabels(headers); setHeaderHidden(true); setRootIsDecorated(false); setIndentation(25); } bool KateColorTreeWidget::edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) { // accept edit only for color buttons in column 1 and reset in column 2 if (!index.parent().isValid() || index.column() < 1) { return QTreeWidget::edit(index, trigger, event); } bool accept = false; if (event && event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); accept = (ke->key() == Qt::Key_Space); // allow Space to edit } switch (trigger) { - case QAbstractItemView::DoubleClicked: - case QAbstractItemView::SelectedClicked: - case QAbstractItemView::EditKeyPressed: // = F2 - accept = true; - break; - default: - break; + case QAbstractItemView::DoubleClicked: + case QAbstractItemView::SelectedClicked: + case QAbstractItemView::EditKeyPressed: // = F2 + accept = true; + break; + default: + break; } if (accept) { KateColorTreeItem *item = dynamic_cast(itemFromIndex(index)); const QColor color = item->useDefaultColor() ? item->defaultColor() : item->color(); if (index.column() == 1) { const QColor selectedColor = QColorDialog::getColor(color, this); if (selectedColor.isValid()) { item->setUseDefaultColor(false); item->setColor(selectedColor); viewport()->update(); emit changed(); } } else if (index.column() == 2 && !item->useDefaultColor()) { item->setUseDefaultColor(true); viewport()->update(); emit changed(); } return false; } return QTreeWidget::edit(index, trigger, event); } void KateColorTreeWidget::drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const { Q_UNUSED(painter) Q_UNUSED(rect) Q_UNUSED(index) } void KateColorTreeWidget::selectDefaults() { bool somethingChanged = false; // use default colors for all selected items for (int a = 0; a < topLevelItemCount(); ++a) { QTreeWidgetItem *top = topLevelItem(a); for (int b = 0; b < top->childCount(); ++b) { KateColorTreeItem *it = dynamic_cast(top->child(b)); Q_ASSERT(it); if (!it->useDefaultColor()) { it->setUseDefaultColor(true); somethingChanged = true; } } } if (somethingChanged) { viewport()->update(); emit changed(); } } void KateColorTreeWidget::addColorItem(const KateColorItem &colorItem) { QTreeWidgetItem *categoryItem = nullptr; for (int i = 0; i < topLevelItemCount(); ++i) { if (topLevelItem(i)->text(0) == colorItem.category) { categoryItem = topLevelItem(i); break; } } if (!categoryItem) { categoryItem = new QTreeWidgetItem(); categoryItem->setText(0, colorItem.category); addTopLevelItem(categoryItem); expandItem(categoryItem); } new KateColorTreeItem(colorItem, categoryItem); resizeColumnToContents(0); } void KateColorTreeWidget::addColorItems(const QVector &colorItems) { for (const KateColorItem &item : colorItems) { addColorItem(item); } } QVector KateColorTreeWidget::colorItems() const { QVector items; for (int a = 0; a < topLevelItemCount(); ++a) { QTreeWidgetItem *top = topLevelItem(a); for (int b = 0; b < top->childCount(); ++b) { KateColorTreeItem *item = dynamic_cast(top->child(b)); Q_ASSERT(item); items.append(item->colorItem()); } } return items; } QColor KateColorTreeWidget::findColor(const QString &key) const { for (int a = 0; a < topLevelItemCount(); ++a) { QTreeWidgetItem *top = topLevelItem(a); for (int b = 0; b < top->childCount(); ++b) { KateColorTreeItem *item = dynamic_cast(top->child(b)); if (item->key() == key) { if (item->useDefaultColor()) { return item->defaultColor(); } else { return item->color(); } } } } return QColor(); } diff --git a/src/schema/katestyletreewidget.cpp b/src/schema/katestyletreewidget.cpp index 4bd2ce7c..5b2bca47 100644 --- a/src/schema/katestyletreewidget.cpp +++ b/src/schema/katestyletreewidget.cpp @@ -1,719 +1,719 @@ /* This file is part of the KDE libraries Copyright (C) 2001-2003 Christoph Cullmann Copyright (C) 2002, 2003 Anders Lund Copyright (C) 2005-2006 Hamish Rodda Copyright (C) 2007 Mirko Stocker * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katestyletreewidget.h" #include "kateconfig.h" #include "katedefaultcolors.h" #include "kateextendedattribute.h" #include "kateglobal.h" #include #include #include #include #include #include #include #include #include // BEGIN KateStyleTreeDelegate class KateStyleTreeDelegate : public QStyledItemDelegate { public: KateStyleTreeDelegate(KateStyleTreeWidget *widget); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: QBrush getBrushForColorColumn(const QModelIndex &index, int column) const; KateStyleTreeWidget *m_widget; }; // END // BEGIN KateStyleTreeWidgetItem decl /* QListViewItem subclass to display/edit a style, bold/italic is check boxes, normal and selected colors are boxes, which will display a color chooser when activated. The context name for the style will be drawn using the editor default font and the chosen colors. This widget id designed to handle the default as well as the individual hl style lists. This widget is designed to work with the KateStyleTreeWidget class exclusively. Added by anders, jan 23 2002. */ class KateStyleTreeWidgetItem : public QTreeWidgetItem { public: KateStyleTreeWidgetItem(QTreeWidgetItem *parent, const QString &styleName, KTextEditor::Attribute::Ptr defaultstyle, KTextEditor::Attribute::Ptr data = KTextEditor::Attribute::Ptr()); KateStyleTreeWidgetItem(QTreeWidget *parent, const QString &styleName, KTextEditor::Attribute::Ptr defaultstyle, KTextEditor::Attribute::Ptr data = KTextEditor::Attribute::Ptr()); ~KateStyleTreeWidgetItem() override { } enum columns { Context = 0, Bold, Italic, Underline, StrikeOut, Foreground, SelectedForeground, Background, SelectedBackground, UseDefaultStyle, NumColumns }; /* initializes the style from the default and the hldata */ void initStyle(); /* updates the hldata's style */ void updateStyle(); /* For bool fields, toggles them, for color fields, display a color chooser */ void changeProperty(int p); /** unset a color. * c is 100 (BGColor) or 101 (SelectedBGColor) for now. */ void unsetColor(int c); /* style context name */ QString contextName() const { return text(0); } /* only true for a hl mode item using its default style */ bool defStyle() const; /* true for default styles */ bool isDefault() const; /* whichever style is active (currentStyle for hl mode styles not using the default style, defaultStyle otherwise) */ KTextEditor::Attribute::Ptr style() const { return currentStyle; } QVariant data(int column, int role) const override; KateStyleTreeWidget *treeWidget() const; private: /* private methods to change properties */ void toggleDefStyle(); void setColor(int); /* helper function to copy the default style into the KateExtendedAttribute, when a property is changed and we are using default style. */ KTextEditor::Attribute::Ptr currentStyle, // the style currently in use (was "is") defaultStyle; // default style for hl mode contexts and default styles (was "ds") KTextEditor::Attribute::Ptr actualStyle; // itemdata for hl mode contexts (was "st") }; // END // BEGIN KateStyleTreeWidget KateStyleTreeWidget::KateStyleTreeWidget(QWidget *parent, bool showUseDefaults) : QTreeWidget(parent) { setItemDelegate(new KateStyleTreeDelegate(this)); setRootIsDecorated(false); QStringList headers; headers << i18nc("@title:column Meaning of text in editor", "Context") << QString() << QString() << QString() << QString() << i18nc("@title:column Text style", "Normal") << i18nc("@title:column Text style", "Selected") << i18nc("@title:column Text style", "Background") << i18nc("@title:column Text style", "Background Selected"); if (showUseDefaults) { headers << i18n("Use Default Style"); } setHeaderLabels(headers); headerItem()->setIcon(1, QIcon::fromTheme(QStringLiteral("format-text-bold"))); headerItem()->setIcon(2, QIcon::fromTheme(QStringLiteral("format-text-italic"))); headerItem()->setIcon(3, QIcon::fromTheme(QStringLiteral("format-text-underline"))); headerItem()->setIcon(4, QIcon::fromTheme(QStringLiteral("format-text-strikethrough"))); // grap the bg color, selected color and default font const KColorScheme &colors(KTextEditor::EditorPrivate::self()->defaultColors().view()); normalcol = colors.foreground().color(); bgcol = KateRendererConfig::global()->backgroundColor(); selcol = KateRendererConfig::global()->selectionColor(); docfont = KateRendererConfig::global()->baseFont(); QPalette pal = viewport()->palette(); pal.setColor(QPalette::Window, bgcol); viewport()->setPalette(pal); } QIcon brushIcon(const QColor &color) { QPixmap pm(16, 16); QRect all(0, 0, 15, 15); { QPainter p(&pm); p.fillRect(all, color); p.setPen(Qt::black); p.drawRect(all); } return QIcon(pm); } bool KateStyleTreeWidget::edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) { if (index.column() == KateStyleTreeWidgetItem::Context) { return false; } KateStyleTreeWidgetItem *i = dynamic_cast(itemFromIndex(index)); if (!i) { return QTreeWidget::edit(index, trigger, event); } switch (trigger) { - case QAbstractItemView::DoubleClicked: - case QAbstractItemView::SelectedClicked: - case QAbstractItemView::EditKeyPressed: - i->changeProperty(index.column()); - update(index); - update(index.sibling(index.row(), KateStyleTreeWidgetItem::Context)); - return false; - default: - return QTreeWidget::edit(index, trigger, event); + case QAbstractItemView::DoubleClicked: + case QAbstractItemView::SelectedClicked: + case QAbstractItemView::EditKeyPressed: + i->changeProperty(index.column()); + update(index); + update(index.sibling(index.row(), KateStyleTreeWidgetItem::Context)); + return false; + default: + return QTreeWidget::edit(index, trigger, event); } } void KateStyleTreeWidget::resizeColumns() { for (int i = 0; i < columnCount(); ++i) { resizeColumnToContents(i); } } void KateStyleTreeWidget::showEvent(QShowEvent *event) { QTreeWidget::showEvent(event); resizeColumns(); } void KateStyleTreeWidget::contextMenuEvent(QContextMenuEvent *event) { KateStyleTreeWidgetItem *i = dynamic_cast(itemAt(event->pos())); if (!i) { return; } QMenu m(this); KTextEditor::Attribute::Ptr currentStyle = i->style(); // the title is used, because the menu obscures the context name when // displayed on behalf of spacePressed(). QPainter p; p.setPen(Qt::black); const QIcon emptyColorIcon = brushIcon(viewport()->palette().base().color()); QIcon cl = brushIcon(i->style()->foreground().color()); QIcon scl = brushIcon(i->style()->selectedForeground().color()); QIcon bgcl = i->style()->hasProperty(QTextFormat::BackgroundBrush) ? brushIcon(i->style()->background().color()) : emptyColorIcon; QIcon sbgcl = i->style()->hasProperty(CustomProperties::SelectedBackground) ? brushIcon(i->style()->selectedBackground().color()) : emptyColorIcon; m.addSection(i->contextName()); QAction *a = m.addAction(i18n("&Bold"), this, SLOT(changeProperty())); a->setCheckable(true); a->setChecked(currentStyle->fontBold()); a->setData(KateStyleTreeWidgetItem::Bold); a = m.addAction(i18n("&Italic"), this, SLOT(changeProperty())); a->setCheckable(true); a->setChecked(currentStyle->fontItalic()); a->setData(KateStyleTreeWidgetItem::Italic); a = m.addAction(i18n("&Underline"), this, SLOT(changeProperty())); a->setCheckable(true); a->setChecked(currentStyle->fontUnderline()); a->setData(KateStyleTreeWidgetItem::Underline); a = m.addAction(i18n("S&trikeout"), this, SLOT(changeProperty())); a->setCheckable(true); a->setChecked(currentStyle->fontStrikeOut()); a->setData(KateStyleTreeWidgetItem::StrikeOut); m.addSeparator(); a = m.addAction(cl, i18n("Normal &Color..."), this, SLOT(changeProperty())); a->setData(KateStyleTreeWidgetItem::Foreground); a = m.addAction(scl, i18n("&Selected Color..."), this, SLOT(changeProperty())); a->setData(KateStyleTreeWidgetItem::SelectedForeground); a = m.addAction(bgcl, i18n("&Background Color..."), this, SLOT(changeProperty())); a->setData(KateStyleTreeWidgetItem::Background); a = m.addAction(sbgcl, i18n("S&elected Background Color..."), this, SLOT(changeProperty())); a->setData(KateStyleTreeWidgetItem::SelectedBackground); // defaulters m.addSeparator(); a = m.addAction(emptyColorIcon, i18n("Unset Normal Color"), this, SLOT(unsetColor())); a->setData(1); a = m.addAction(emptyColorIcon, i18n("Unset Selected Color"), this, SLOT(unsetColor())); a->setData(2); // unsetters KTextEditor::Attribute::Ptr style = i->style(); if (style->hasProperty(QTextFormat::BackgroundBrush)) { a = m.addAction(emptyColorIcon, i18n("Unset Background Color"), this, SLOT(unsetColor())); a->setData(3); } if (style->hasProperty(CustomProperties::SelectedBackground)) { a = m.addAction(emptyColorIcon, i18n("Unset Selected Background Color"), this, SLOT(unsetColor())); a->setData(4); } if (!i->isDefault() && !i->defStyle()) { m.addSeparator(); a = m.addAction(i18n("Use &Default Style"), this, SLOT(changeProperty())); a->setCheckable(true); a->setChecked(i->defStyle()); a->setData(KateStyleTreeWidgetItem::UseDefaultStyle); } m.exec(event->globalPos()); } void KateStyleTreeWidget::changeProperty() { static_cast(currentItem())->changeProperty(static_cast(sender())->data().toInt()); } void KateStyleTreeWidget::unsetColor() { static_cast(currentItem())->unsetColor(static_cast(sender())->data().toInt()); } void KateStyleTreeWidget::updateGroupHeadings() { for (int i = 0; i < topLevelItemCount(); i++) { QTreeWidgetItem *currentTopLevelItem = topLevelItem(i); QTreeWidgetItem *firstChild = currentTopLevelItem->child(0); if (firstChild) { QColor foregroundColor = firstChild->data(KateStyleTreeWidgetItem::Foreground, Qt::DisplayRole).value(); QColor backgroundColor = firstChild->data(KateStyleTreeWidgetItem::Background, Qt::DisplayRole).value(); currentTopLevelItem->setForeground(KateStyleTreeWidgetItem::Context, foregroundColor); if (backgroundColor.isValid()) { currentTopLevelItem->setBackground(KateStyleTreeWidgetItem::Context, backgroundColor); } } } } void KateStyleTreeWidget::emitChanged() { updateGroupHeadings(); emit changed(); } void KateStyleTreeWidget::addItem(const QString &styleName, KTextEditor::Attribute::Ptr defaultstyle, KTextEditor::Attribute::Ptr data) { new KateStyleTreeWidgetItem(this, styleName, std::move(defaultstyle), std::move(data)); } void KateStyleTreeWidget::addItem(QTreeWidgetItem *parent, const QString &styleName, KTextEditor::Attribute::Ptr defaultstyle, KTextEditor::Attribute::Ptr data) { new KateStyleTreeWidgetItem(parent, styleName, std::move(defaultstyle), std::move(data)); updateGroupHeadings(); } // END // BEGIN KateStyleTreeWidgetItem KateStyleTreeDelegate::KateStyleTreeDelegate(KateStyleTreeWidget *widget) : m_widget(widget) { } QBrush KateStyleTreeDelegate::getBrushForColorColumn(const QModelIndex &index, int column) const { QModelIndex colorIndex = index.sibling(index.row(), column); QVariant displayData = colorIndex.model()->data(colorIndex); return displayData.value(); } void KateStyleTreeDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { static QSet columns; if (columns.isEmpty()) { columns << KateStyleTreeWidgetItem::Foreground << KateStyleTreeWidgetItem::SelectedForeground << KateStyleTreeWidgetItem::Background << KateStyleTreeWidgetItem::SelectedBackground; } if (index.column() == KateStyleTreeWidgetItem::Context) { QStyleOptionViewItem styleContextItem(option); QBrush brush = getBrushForColorColumn(index, KateStyleTreeWidgetItem::SelectedBackground); if (brush != QBrush()) { styleContextItem.palette.setBrush(QPalette::Highlight, brush); } brush = getBrushForColorColumn(index, KateStyleTreeWidgetItem::SelectedForeground); if (brush != QBrush()) { styleContextItem.palette.setBrush(QPalette::HighlightedText, brush); } return QStyledItemDelegate::paint(painter, styleContextItem, index); } QStyledItemDelegate::paint(painter, option, index); if (!columns.contains(index.column())) { return; } QVariant displayData = index.model()->data(index); if (displayData.type() != QVariant::Brush) { return; } QBrush brush = displayData.value(); QStyleOptionButton opt; opt.rect = option.rect; opt.palette = m_widget->palette(); bool set = brush != QBrush(); if (!set) { opt.text = i18nc("No text or background color set", "None set"); brush = Qt::white; } m_widget->style()->drawControl(QStyle::CE_PushButton, &opt, painter, m_widget); if (set) { painter->fillRect(m_widget->style()->subElementRect(QStyle::SE_PushButtonContents, &opt, m_widget), brush); } } KateStyleTreeWidgetItem::KateStyleTreeWidgetItem(QTreeWidgetItem *parent, const QString &stylename, KTextEditor::Attribute::Ptr defaultAttribute, KTextEditor::Attribute::Ptr actualAttribute) : QTreeWidgetItem(parent) , currentStyle(nullptr) , defaultStyle(std::move(defaultAttribute)) , actualStyle(std::move(actualAttribute)) { initStyle(); setText(0, stylename); } KateStyleTreeWidgetItem::KateStyleTreeWidgetItem(QTreeWidget *parent, const QString &stylename, KTextEditor::Attribute::Ptr defaultAttribute, KTextEditor::Attribute::Ptr actualAttribute) : QTreeWidgetItem(parent) , currentStyle(nullptr) , defaultStyle(std::move(defaultAttribute)) , actualStyle(std::move(actualAttribute)) { initStyle(); setText(0, stylename); } void KateStyleTreeWidgetItem::initStyle() { if (!actualStyle) { currentStyle = defaultStyle; } else { currentStyle = new KTextEditor::Attribute(*defaultStyle); if (actualStyle->hasAnyProperty()) { *currentStyle += *actualStyle; } } setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); } static Qt::CheckState toCheckState(bool b) { return b ? Qt::Checked : Qt::Unchecked; } QVariant KateStyleTreeWidgetItem::data(int column, int role) const { if (column == Context) { switch (role) { - case Qt::ForegroundRole: - if (style()->hasProperty(QTextFormat::ForegroundBrush)) { - return style()->foreground().color(); - } - break; - - case Qt::BackgroundRole: - if (style()->hasProperty(QTextFormat::BackgroundBrush)) { - return style()->background().color(); - } - break; - - case Qt::FontRole: - return style()->font(); - break; + case Qt::ForegroundRole: + if (style()->hasProperty(QTextFormat::ForegroundBrush)) { + return style()->foreground().color(); + } + break; + + case Qt::BackgroundRole: + if (style()->hasProperty(QTextFormat::BackgroundBrush)) { + return style()->background().color(); + } + break; + + case Qt::FontRole: + return style()->font(); + break; } } if (role == Qt::CheckStateRole) { switch (column) { - case Bold: - return toCheckState(style()->fontBold()); - case Italic: - return toCheckState(style()->fontItalic()); - case Underline: - return toCheckState(style()->fontUnderline()); - case StrikeOut: - return toCheckState(style()->fontStrikeOut()); - case UseDefaultStyle: - /* can't compare all attributes, currentStyle has always more than defaultStyle (e.g. the item's name), - * so we just compare the important ones:*/ - return toCheckState(currentStyle->foreground() == defaultStyle->foreground() && currentStyle->background() == defaultStyle->background() && currentStyle->selectedForeground() == defaultStyle->selectedForeground() && - currentStyle->selectedBackground() == defaultStyle->selectedBackground() && currentStyle->fontBold() == defaultStyle->fontBold() && currentStyle->fontItalic() == defaultStyle->fontItalic() && - currentStyle->fontUnderline() == defaultStyle->fontUnderline() && currentStyle->fontStrikeOut() == defaultStyle->fontStrikeOut()); + case Bold: + return toCheckState(style()->fontBold()); + case Italic: + return toCheckState(style()->fontItalic()); + case Underline: + return toCheckState(style()->fontUnderline()); + case StrikeOut: + return toCheckState(style()->fontStrikeOut()); + case UseDefaultStyle: + /* can't compare all attributes, currentStyle has always more than defaultStyle (e.g. the item's name), + * so we just compare the important ones:*/ + return toCheckState(currentStyle->foreground() == defaultStyle->foreground() && currentStyle->background() == defaultStyle->background() && currentStyle->selectedForeground() == defaultStyle->selectedForeground() && + currentStyle->selectedBackground() == defaultStyle->selectedBackground() && currentStyle->fontBold() == defaultStyle->fontBold() && currentStyle->fontItalic() == defaultStyle->fontItalic() && + currentStyle->fontUnderline() == defaultStyle->fontUnderline() && currentStyle->fontStrikeOut() == defaultStyle->fontStrikeOut()); } } if (role == Qt::DisplayRole) { switch (column) { - case Foreground: - return style()->foreground(); - case SelectedForeground: - return style()->selectedForeground(); - case Background: - return style()->background(); - case SelectedBackground: - return style()->selectedBackground(); + case Foreground: + return style()->foreground(); + case SelectedForeground: + return style()->selectedForeground(); + case Background: + return style()->background(); + case SelectedBackground: + return style()->selectedBackground(); } } return QTreeWidgetItem::data(column, role); } void KateStyleTreeWidgetItem::updateStyle() { // nothing there, not update it, will crash if (!actualStyle) { return; } if (currentStyle->hasProperty(QTextFormat::FontWeight)) { if (currentStyle->fontWeight() != actualStyle->fontWeight()) { actualStyle->setFontWeight(currentStyle->fontWeight()); } } else { actualStyle->clearProperty(QTextFormat::FontWeight); } if (currentStyle->hasProperty(QTextFormat::FontItalic)) { if (currentStyle->fontItalic() != actualStyle->fontItalic()) { actualStyle->setFontItalic(currentStyle->fontItalic()); } } else { actualStyle->clearProperty(QTextFormat::FontItalic); } if (currentStyle->hasProperty(QTextFormat::FontStrikeOut)) { if (currentStyle->fontStrikeOut() != actualStyle->fontStrikeOut()) { actualStyle->setFontStrikeOut(currentStyle->fontStrikeOut()); } } else { actualStyle->clearProperty(QTextFormat::FontStrikeOut); } if (currentStyle->hasProperty(QTextFormat::TextUnderlineStyle)) { if (currentStyle->fontUnderline() != actualStyle->fontUnderline()) { actualStyle->setFontUnderline(currentStyle->fontUnderline()); } } else { actualStyle->clearProperty(QTextFormat::TextUnderlineStyle); } if (currentStyle->hasProperty(CustomProperties::Outline)) { if (currentStyle->outline() != actualStyle->outline()) { actualStyle->setOutline(currentStyle->outline()); } } else { actualStyle->clearProperty(CustomProperties::Outline); } if (currentStyle->hasProperty(QTextFormat::ForegroundBrush)) { if (currentStyle->foreground() != actualStyle->foreground()) { actualStyle->setForeground(currentStyle->foreground()); } } else { actualStyle->clearProperty(QTextFormat::ForegroundBrush); } if (currentStyle->hasProperty(CustomProperties::SelectedForeground)) { if (currentStyle->selectedForeground() != actualStyle->selectedForeground()) { actualStyle->setSelectedForeground(currentStyle->selectedForeground()); } } else { actualStyle->clearProperty(CustomProperties::SelectedForeground); } if (currentStyle->hasProperty(QTextFormat::BackgroundBrush)) { if (currentStyle->background() != actualStyle->background()) { actualStyle->setBackground(currentStyle->background()); } } else { actualStyle->clearProperty(QTextFormat::BackgroundBrush); } if (currentStyle->hasProperty(CustomProperties::SelectedBackground)) { if (currentStyle->selectedBackground() != actualStyle->selectedBackground()) { actualStyle->setSelectedBackground(currentStyle->selectedBackground()); } } else { actualStyle->clearProperty(CustomProperties::SelectedBackground); } } /* only true for a hl mode item using its default style */ bool KateStyleTreeWidgetItem::defStyle() const { return actualStyle && actualStyle->properties() != defaultStyle->properties(); } /* true for default styles */ bool KateStyleTreeWidgetItem::isDefault() const { return actualStyle ? false : true; } void KateStyleTreeWidgetItem::changeProperty(int p) { if (p == Bold) { currentStyle->setFontBold(!currentStyle->fontBold()); } else if (p == Italic) { currentStyle->setFontItalic(!currentStyle->fontItalic()); } else if (p == Underline) { currentStyle->setFontUnderline(!currentStyle->fontUnderline()); } else if (p == StrikeOut) { currentStyle->setFontStrikeOut(!currentStyle->fontStrikeOut()); } else if (p == UseDefaultStyle) { toggleDefStyle(); } else { setColor(p); } updateStyle(); treeWidget()->emitChanged(); } void KateStyleTreeWidgetItem::toggleDefStyle() { if (*currentStyle == *defaultStyle) { KMessageBox::information(treeWidget(), i18n("\"Use Default Style\" will be automatically unset when you change any style properties."), i18n("Kate Styles"), QStringLiteral("Kate hl config use defaults")); } else { currentStyle = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute(*defaultStyle)); updateStyle(); QModelIndex currentIndex = treeWidget()->currentIndex(); while (currentIndex.isValid()) { treeWidget()->update(currentIndex); currentIndex = currentIndex.sibling(currentIndex.row(), currentIndex.column() - 1); } } } void KateStyleTreeWidgetItem::setColor(int column) { QColor c; // use this QColor d; // default color if (column == Foreground) { c = currentStyle->foreground().color(); d = defaultStyle->foreground().color(); } else if (column == SelectedForeground) { c = currentStyle->selectedForeground().color(); d = defaultStyle->selectedForeground().color(); } else if (column == Background) { c = currentStyle->background().color(); d = defaultStyle->background().color(); } else if (column == SelectedBackground) { c = currentStyle->selectedBackground().color(); d = defaultStyle->selectedBackground().color(); } if (!c.isValid()) { c = d; } const QColor selectedColor = QColorDialog::getColor(c, treeWidget()); if (!selectedColor.isValid()) { return; } // if set default, and the attrib is set in the default style use it // else if set default, unset it // else set the selected color switch (column) { - case Foreground: - currentStyle->setForeground(selectedColor); - break; - case SelectedForeground: - currentStyle->setSelectedForeground(selectedColor); - break; - case Background: - currentStyle->setBackground(selectedColor); - break; - case SelectedBackground: - currentStyle->setSelectedBackground(selectedColor); - break; + case Foreground: + currentStyle->setForeground(selectedColor); + break; + case SelectedForeground: + currentStyle->setSelectedForeground(selectedColor); + break; + case Background: + currentStyle->setBackground(selectedColor); + break; + case SelectedBackground: + currentStyle->setSelectedBackground(selectedColor); + break; } // FIXME // repaint(); } void KateStyleTreeWidgetItem::unsetColor(int colorId) { switch (colorId) { - case 1: - if (defaultStyle->hasProperty(QTextFormat::ForegroundBrush)) { - currentStyle->setForeground(defaultStyle->foreground()); - } else { - currentStyle->clearProperty(QTextFormat::ForegroundBrush); - } - break; - case 2: - if (defaultStyle->hasProperty(CustomProperties::SelectedForeground)) { - currentStyle->setSelectedForeground(defaultStyle->selectedForeground()); - } else { - currentStyle->clearProperty(CustomProperties::SelectedForeground); - } - break; - case 3: - if (currentStyle->hasProperty(QTextFormat::BackgroundBrush)) { - currentStyle->clearProperty(QTextFormat::BackgroundBrush); - } - break; - case 4: - if (currentStyle->hasProperty(CustomProperties::SelectedBackground)) { - currentStyle->clearProperty(CustomProperties::SelectedBackground); - } - break; + case 1: + if (defaultStyle->hasProperty(QTextFormat::ForegroundBrush)) { + currentStyle->setForeground(defaultStyle->foreground()); + } else { + currentStyle->clearProperty(QTextFormat::ForegroundBrush); + } + break; + case 2: + if (defaultStyle->hasProperty(CustomProperties::SelectedForeground)) { + currentStyle->setSelectedForeground(defaultStyle->selectedForeground()); + } else { + currentStyle->clearProperty(CustomProperties::SelectedForeground); + } + break; + case 3: + if (currentStyle->hasProperty(QTextFormat::BackgroundBrush)) { + currentStyle->clearProperty(QTextFormat::BackgroundBrush); + } + break; + case 4: + if (currentStyle->hasProperty(CustomProperties::SelectedBackground)) { + currentStyle->clearProperty(CustomProperties::SelectedBackground); + } + break; } updateStyle(); treeWidget()->emitChanged(); } KateStyleTreeWidget *KateStyleTreeWidgetItem::treeWidget() const { return static_cast(QTreeWidgetItem::treeWidget()); } // END diff --git a/src/script/katescriptmanager.cpp b/src/script/katescriptmanager.cpp index 3f2aa769..ff223c52 100644 --- a/src/script/katescriptmanager.cpp +++ b/src/script/katescriptmanager.cpp @@ -1,331 +1,331 @@ // This file is part of the KDE libraries // Copyright (C) 2005 Christoph Cullmann // Copyright (C) 2005 Joseph Wenninger // Copyright (C) 2006-2018 Dominik Haumann // Copyright (C) 2008 Paul Giannaros // Copyright (C) 2010 Joseph Wenninger // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License version 2 as published by the Free Software Foundation. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public License // along with this library; see the file COPYING.LIB. If not, write to // the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, // Boston, MA 02110-1301, USA. #include "katescriptmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "katecmd.h" #include "kateglobal.h" #include "katepartdebug.h" KateScriptManager *KateScriptManager::m_instance = nullptr; KateScriptManager::KateScriptManager() : KTextEditor::Command({QStringLiteral("reload-scripts")}) { // use cached info collect(); } KateScriptManager::~KateScriptManager() { qDeleteAll(m_indentationScripts); qDeleteAll(m_commandLineScripts); m_instance = nullptr; } KateIndentScript *KateScriptManager::indenter(const QString &language) { KateIndentScript *highestPriorityIndenter = nullptr; const auto indenters = m_languageToIndenters.value(language.toLower()); for (KateIndentScript *indenter : indenters) { // don't overwrite if there is already a result with a higher priority if (highestPriorityIndenter && indenter->indentHeader().priority() < highestPriorityIndenter->indentHeader().priority()) { #ifdef DEBUG_SCRIPTMANAGER qCDebug(LOG_KTE) << "Not overwriting indenter for" << language << "as the priority isn't big enough (" << indenter->indentHeader().priority() << '<' << highestPriorityIndenter->indentHeader().priority() << ')'; #endif } else { highestPriorityIndenter = indenter; } } #ifdef DEBUG_SCRIPTMANAGER if (highestPriorityIndenter) { qCDebug(LOG_KTE) << "Found indenter" << highestPriorityIndenter->url() << "for" << language; } else { qCDebug(LOG_KTE) << "No indenter for" << language; } #endif return highestPriorityIndenter; } /** * Small helper: QJsonValue to QStringList */ static QStringList jsonToStringList(const QJsonValue &value) { QStringList list; const auto array = value.toArray(); for (const QJsonValue &value : array) { if (value.isString()) { list.append(value.toString()); } } return list; } void KateScriptManager::collect() { // clear out the old scripts and reserve enough space qDeleteAll(m_indentationScripts); qDeleteAll(m_commandLineScripts); m_indentationScripts.clear(); m_commandLineScripts.clear(); m_languageToIndenters.clear(); m_indentationScriptMap.clear(); /** * now, we search all kinds of known scripts */ for (const auto &type : {QLatin1String("indentation"), QLatin1String("commands")}) { // basedir for filesystem lookup const QString basedir = QLatin1String("/katepart5/script/") + type; QStringList dirs; // first writable locations, e.g. stuff the user has provided dirs += QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + basedir; // then resources, e.g. the stuff we ship with us dirs.append(QLatin1String(":/ktexteditor/script/") + type); // then all other locations, this includes global stuff installed by other applications // this will not allow global stuff to overwrite the stuff we ship in our resources to allow to install a more up-to-date ktexteditor lib locally! const auto genericDataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); for (const QString &dir : genericDataDirs) { dirs.append(dir + basedir); } QStringList list; for (const QString &dir : qAsConst(dirs)) { const QStringList fileNames = QDir(dir).entryList({QStringLiteral("*.js")}); for (const QString &file : qAsConst(fileNames)) { list.append(dir + QLatin1Char('/') + file); } } // iterate through the files and read info out of cache or file, no double loading of same scripts QSet unique; for (const QString &fileName : qAsConst(list)) { /** * get file basename */ const QString baseName = QFileInfo(fileName).baseName(); /** * only load scripts once, even if multiple installed variants found! */ if (unique.contains(baseName)) continue; /** * remember the script */ unique.insert(baseName); /** * open file or skip it */ QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qCDebug(LOG_KTE) << "Script parse error: Cannot open file " << qPrintable(fileName) << '\n'; continue; } /** * search json header or skip this file */ QByteArray fileContent = file.readAll(); int startOfJson = fileContent.indexOf('{'); if (startOfJson < 0) { qCDebug(LOG_KTE) << "Script parse error: Cannot find start of json header at start of file " << qPrintable(fileName) << '\n'; continue; } int endOfJson = fileContent.indexOf("\n};", startOfJson); if (endOfJson < 0) { // as fallback, check also mac os line ending endOfJson = fileContent.indexOf("\r};", startOfJson); } if (endOfJson < 0) { qCDebug(LOG_KTE) << "Script parse error: Cannot find end of json header at start of file " << qPrintable(fileName) << '\n'; continue; } endOfJson += 2; // we want the end including the } but not the ; /** * parse json header or skip this file */ QJsonParseError error; const QJsonDocument metaInfo(QJsonDocument::fromJson(fileContent.mid(startOfJson, endOfJson - startOfJson), &error)); if (error.error || !metaInfo.isObject()) { qCDebug(LOG_KTE) << "Script parse error: Cannot parse json header at start of file " << qPrintable(fileName) << error.errorString() << endOfJson << fileContent.mid(endOfJson - 25, 25).replace('\n', ' '); continue; } /** * remember type */ KateScriptHeader generalHeader; if (type == QLatin1String("indentation")) { generalHeader.setScriptType(Kate::ScriptType::Indentation); } else if (type == QLatin1String("commands")) { generalHeader.setScriptType(Kate::ScriptType::CommandLine); } else { // should never happen, we dictate type by directory Q_ASSERT(false); } const QJsonObject metaInfoObject = metaInfo.object(); generalHeader.setLicense(metaInfoObject.value(QStringLiteral("license")).toString()); generalHeader.setAuthor(metaInfoObject.value(QStringLiteral("author")).toString()); generalHeader.setRevision(metaInfoObject.value(QStringLiteral("revision")).toInt()); generalHeader.setKateVersion(metaInfoObject.value(QStringLiteral("kate-version")).toString()); // now, cast accordingly based on type switch (generalHeader.scriptType()) { - case Kate::ScriptType::Indentation: { - KateIndentScriptHeader indentHeader; - indentHeader.setName(metaInfoObject.value(QStringLiteral("name")).toString()); - indentHeader.setBaseName(baseName); - if (indentHeader.name().isNull()) { - qCDebug(LOG_KTE) << "Script value error: No name specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping indenter" << '\n'; - continue; - } - - // required style? - indentHeader.setRequiredStyle(metaInfoObject.value(QStringLiteral("required-syntax-style")).toString()); - // which languages does this support? - QStringList indentLanguages = jsonToStringList(metaInfoObject.value(QStringLiteral("indent-languages"))); - if (!indentLanguages.isEmpty()) { - indentHeader.setIndentLanguages(indentLanguages); - } else { - indentHeader.setIndentLanguages(QStringList() << indentHeader.name()); + case Kate::ScriptType::Indentation: { + KateIndentScriptHeader indentHeader; + indentHeader.setName(metaInfoObject.value(QStringLiteral("name")).toString()); + indentHeader.setBaseName(baseName); + if (indentHeader.name().isNull()) { + qCDebug(LOG_KTE) << "Script value error: No name specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping indenter" << '\n'; + continue; + } + + // required style? + indentHeader.setRequiredStyle(metaInfoObject.value(QStringLiteral("required-syntax-style")).toString()); + // which languages does this support? + QStringList indentLanguages = jsonToStringList(metaInfoObject.value(QStringLiteral("indent-languages"))); + if (!indentLanguages.isEmpty()) { + indentHeader.setIndentLanguages(indentLanguages); + } else { + indentHeader.setIndentLanguages(QStringList() << indentHeader.name()); #ifdef DEBUG_SCRIPTMANAGER - qCDebug(LOG_KTE) << "Script value warning: No indent-languages specified for indent " - << "script " << qPrintable(fileName) << ". Using the name (" << qPrintable(indentHeader.name()) << ")\n"; + qCDebug(LOG_KTE) << "Script value warning: No indent-languages specified for indent " + << "script " << qPrintable(fileName) << ". Using the name (" << qPrintable(indentHeader.name()) << ")\n"; #endif - } - // priority - indentHeader.setPriority(metaInfoObject.value(QStringLiteral("priority")).toInt()); - - KateIndentScript *script = new KateIndentScript(fileName, indentHeader); - script->setGeneralHeader(generalHeader); - for (const QString &language : indentHeader.indentLanguages()) { - m_languageToIndenters[language.toLower()].push_back(script); - } - - m_indentationScriptMap.insert(indentHeader.baseName(), script); - m_indentationScripts.append(script); - break; } - case Kate::ScriptType::CommandLine: { - KateCommandLineScriptHeader commandHeader; - commandHeader.setFunctions(jsonToStringList(metaInfoObject.value(QStringLiteral("functions")))); - commandHeader.setActions(metaInfoObject.value(QStringLiteral("actions")).toArray()); - if (commandHeader.functions().isEmpty()) { - qCDebug(LOG_KTE) << "Script value error: No functions specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping script" << '\n'; - continue; - } - KateCommandLineScript *script = new KateCommandLineScript(fileName, commandHeader); - script->setGeneralHeader(generalHeader); - m_commandLineScripts.push_back(script); - break; + // priority + indentHeader.setPriority(metaInfoObject.value(QStringLiteral("priority")).toInt()); + + KateIndentScript *script = new KateIndentScript(fileName, indentHeader); + script->setGeneralHeader(generalHeader); + for (const QString &language : indentHeader.indentLanguages()) { + m_languageToIndenters[language.toLower()].push_back(script); } - case Kate::ScriptType::Unknown: - default: - qCDebug(LOG_KTE) << "Script value warning: Unknown type ('" << qPrintable(type) << "'): " << qPrintable(fileName) << '\n'; + + m_indentationScriptMap.insert(indentHeader.baseName(), script); + m_indentationScripts.append(script); + break; + } + case Kate::ScriptType::CommandLine: { + KateCommandLineScriptHeader commandHeader; + commandHeader.setFunctions(jsonToStringList(metaInfoObject.value(QStringLiteral("functions")))); + commandHeader.setActions(metaInfoObject.value(QStringLiteral("actions")).toArray()); + if (commandHeader.functions().isEmpty()) { + qCDebug(LOG_KTE) << "Script value error: No functions specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping script" << '\n'; + continue; + } + KateCommandLineScript *script = new KateCommandLineScript(fileName, commandHeader); + script->setGeneralHeader(generalHeader); + m_commandLineScripts.push_back(script); + break; + } + case Kate::ScriptType::Unknown: + default: + qCDebug(LOG_KTE) << "Script value warning: Unknown type ('" << qPrintable(type) << "'): " << qPrintable(fileName) << '\n'; } } } #ifdef DEBUG_SCRIPTMANAGER // XX Test if (indenter("Python")) { qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("triggerCharacters").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("triggerCharacters").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("blafldsjfklas").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("indent").isValid() << "\n"; } if (indenter("C")) { qCDebug(LOG_KTE) << "C: " << qPrintable(indenter("C")->url()) << "\n"; } if (indenter("lisp")) { qCDebug(LOG_KTE) << "LISP: " << qPrintable(indenter("Lisp")->url()) << "\n"; } #endif } void KateScriptManager::reload() { collect(); emit reloaded(); } /// Kate::Command stuff bool KateScriptManager::exec(KTextEditor::View *view, const QString &_cmd, QString &errorMsg, const KTextEditor::Range &) { Q_UNUSED(view) QVector args = _cmd.splitRef(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts); const QString cmd = args.first().toString(); if (cmd == QLatin1String("reload-scripts")) { reload(); return true; } return false; } bool KateScriptManager::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view) if (cmd == QLatin1String("reload-scripts")) { msg = i18n("Reload all JavaScript files (indenters, command line scripts, etc)."); return true; } return false; } diff --git a/src/search/kateregexp.cpp b/src/search/kateregexp.cpp index ce2123b4..999fe517 100644 --- a/src/search/kateregexp.cpp +++ b/src/search/kateregexp.cpp @@ -1,295 +1,295 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2009 Bernhard Beschow * Copyright (C) 2007 Sebastian Pipping * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateregexp.h" KateRegExp::KateRegExp(const QString &pattern, Qt::CaseSensitivity cs, QRegExp::PatternSyntax syntax) : m_regExp(pattern, cs, syntax) { } // these things can besides '.' and '\s' make pattern multi-line: // \n, \x000A, \x????-\x????, \0012, \0???-\0??? // a multi-line pattern must not pass as single-line, the other // way around will just result in slower searches and is therefore // not as critical int KateRegExp::repairPattern(bool &stillMultiLine) { const QString &text = pattern(); // read-only input for parsing // get input const int inputLen = text.length(); int input = 0; // walker index // prepare output QString output; output.reserve(2 * inputLen + 1); // twice should be enough for the average case // parser state stillMultiLine = false; int replaceCount = 0; bool insideClass = false; while (input < inputLen) { if (insideClass) { // wait for closing, unescaped ']' switch (text[input].unicode()) { - case L'\\': - switch (text[input + 1].unicode()) { - case L'x': - if (input + 5 < inputLen) { - // copy "\x????" unmodified - output.append(text.midRef(input, 6)); - input += 6; - } else { - // copy "\x" unmodified - output.append(text.midRef(input, 2)); - input += 2; - } - stillMultiLine = true; - break; - - case L'0': - if (input + 4 < inputLen) { - // copy "\0???" unmodified - output.append(text.midRef(input, 5)); - input += 5; - } else { - // copy "\0" unmodified - output.append(text.midRef(input, 2)); - input += 2; - } - stillMultiLine = true; - break; - - case L's': - // replace "\s" with "[ \t]" - output.append(QLatin1String(" \\t")); - input += 2; - replaceCount++; - break; - - case L'n': - stillMultiLine = true; - // FALLTROUGH - Q_FALLTHROUGH(); - default: - // copy "\?" unmodified - output.append(text.midRef(input, 2)); - input += 2; + case L'\\': + switch (text[input + 1].unicode()) { + case L'x': + if (input + 5 < inputLen) { + // copy "\x????" unmodified + output.append(text.midRef(input, 6)); + input += 6; + } else { + // copy "\x" unmodified + output.append(text.midRef(input, 2)); + input += 2; } + stillMultiLine = true; break; - case L']': - // copy "]" unmodified - insideClass = false; - output.append(text[input]); - input++; + case L'0': + if (input + 4 < inputLen) { + // copy "\0???" unmodified + output.append(text.midRef(input, 5)); + input += 5; + } else { + // copy "\0" unmodified + output.append(text.midRef(input, 2)); + input += 2; + } + stillMultiLine = true; break; + case L's': + // replace "\s" with "[ \t]" + output.append(QLatin1String(" \\t")); + input += 2; + replaceCount++; + break; + + case L'n': + stillMultiLine = true; + // FALLTROUGH + Q_FALLTHROUGH(); default: - // copy "?" unmodified - output.append(text[input]); - input++; + // copy "\?" unmodified + output.append(text.midRef(input, 2)); + input += 2; + } + break; + + case L']': + // copy "]" unmodified + insideClass = false; + output.append(text[input]); + input++; + break; + + default: + // copy "?" unmodified + output.append(text[input]); + input++; } } else { // search for real dots and \S switch (text[input].unicode()) { - case L'\\': - switch (text[input + 1].unicode()) { - case L'x': - if (input + 5 < inputLen) { - // copy "\x????" unmodified - output.append(text.midRef(input, 6)); - input += 6; - } else { - // copy "\x" unmodified - output.append(text.midRef(input, 2)); - input += 2; - } - stillMultiLine = true; - break; - - case L'0': - if (input + 4 < inputLen) { - // copy "\0???" unmodified - output.append(text.midRef(input, 5)); - input += 5; - } else { - // copy "\0" unmodified - output.append(text.midRef(input, 2)); - input += 2; - } - stillMultiLine = true; - break; - - case L's': - // replace "\s" with "[ \t]" - output.append(QLatin1String("[ \\t]")); - input += 2; - replaceCount++; - break; - - case L'n': - stillMultiLine = true; - // FALLTROUGH - Q_FALLTHROUGH(); - default: - // copy "\?" unmodified - output.append(text.midRef(input, 2)); - input += 2; + case L'\\': + switch (text[input + 1].unicode()) { + case L'x': + if (input + 5 < inputLen) { + // copy "\x????" unmodified + output.append(text.midRef(input, 6)); + input += 6; + } else { + // copy "\x" unmodified + output.append(text.midRef(input, 2)); + input += 2; } + stillMultiLine = true; break; - case L'.': - // replace " with "[^\n]" - output.append(QLatin1String("[^\\n]")); - input++; - replaceCount++; + case L'0': + if (input + 4 < inputLen) { + // copy "\0???" unmodified + output.append(text.midRef(input, 5)); + input += 5; + } else { + // copy "\0" unmodified + output.append(text.midRef(input, 2)); + input += 2; + } + stillMultiLine = true; break; - case L'[': - // copy "]" unmodified - insideClass = true; - output.append(text[input]); - input++; + case L's': + // replace "\s" with "[ \t]" + output.append(QLatin1String("[ \\t]")); + input += 2; + replaceCount++; break; + case L'n': + stillMultiLine = true; + // FALLTROUGH + Q_FALLTHROUGH(); default: - // copy "?" unmodified - output.append(text[input]); - input++; + // copy "\?" unmodified + output.append(text.midRef(input, 2)); + input += 2; + } + break; + + case L'.': + // replace " with "[^\n]" + output.append(QLatin1String("[^\\n]")); + input++; + replaceCount++; + break; + + case L'[': + // copy "]" unmodified + insideClass = true; + output.append(text[input]); + input++; + break; + + default: + // copy "?" unmodified + output.append(text[input]); + input++; } } } // Overwrite with repaired pattern m_regExp.setPattern(output); return replaceCount; } bool KateRegExp::isMultiLine() const { const QString &text = pattern(); // parser state bool insideClass = false; for (int input = 0; input < text.length(); /*empty*/) { if (insideClass) { // wait for closing, unescaped ']' switch (text[input].unicode()) { - case L'\\': - switch (text[input + 1].unicode()) { - case L'x': - return true; - - case L'0': - return true; - - case L's': - // replace "\s" with "[ \t]" - input += 2; - break; - - case L'n': - return true; - // FALLTROUGH - - default: - // copy "\?" unmodified - input += 2; - } - break; + case L'\\': + switch (text[input + 1].unicode()) { + case L'x': + return true; - case L']': - // copy "]" unmodified - insideClass = false; - input++; + case L'0': + return true; + + case L's': + // replace "\s" with "[ \t]" + input += 2; break; + case L'n': + return true; + // FALLTROUGH + default: - // copy "?" unmodified - input++; + // copy "\?" unmodified + input += 2; + } + break; + + case L']': + // copy "]" unmodified + insideClass = false; + input++; + break; + + default: + // copy "?" unmodified + input++; } } else { // search for real dots and \S switch (text[input].unicode()) { - case L'\\': - switch (text[input + 1].unicode()) { - case L'x': - return true; - - case L'0': - return true; - - case L's': - // replace "\s" with "[ \t]" - input += 2; - break; + case L'\\': + switch (text[input + 1].unicode()) { + case L'x': + return true; - case L'n': - return true; + case L'0': + return true; - default: - // copy "\?" unmodified - input += 2; - } - break; - - case L'.': - // replace " with "[^\n]" - input++; + case L's': + // replace "\s" with "[ \t]" + input += 2; break; - case L'[': - // copy "]" unmodified - insideClass = true; - input++; - break; + case L'n': + return true; default: - // copy "?" unmodified - input++; + // copy "\?" unmodified + input += 2; + } + break; + + case L'.': + // replace " with "[^\n]" + input++; + break; + + case L'[': + // copy "]" unmodified + insideClass = true; + input++; + break; + + default: + // copy "?" unmodified + input++; } } } return false; } int KateRegExp::indexIn(const QString &str, int start, int end) const { return m_regExp.indexIn(str.left(end), start, QRegExp::CaretAtZero); } int KateRegExp::lastIndexIn(const QString &str, int start, int end) const { const int index = m_regExp.lastIndexIn(str.mid(start, end - start), -1, QRegExp::CaretAtZero); if (index == -1) { return -1; } const int index2 = m_regExp.indexIn(str.left(end), start + index, QRegExp::CaretAtZero); return index2; } diff --git a/src/search/kateregexpsearch.cpp b/src/search/kateregexpsearch.cpp index 55f83d74..af131bd5 100644 --- a/src/search/kateregexpsearch.cpp +++ b/src/search/kateregexpsearch.cpp @@ -1,729 +1,729 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2010 Bernhard Beschow * Copyright (C) 2007 Sebastian Pipping * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // BEGIN includes #include "kateregexpsearch.h" #include "kateregexp.h" #include // END includes // Turn debug messages on/off here // #define FAST_DEBUG_ENABLE #ifdef FAST_DEBUG_ENABLE #define FAST_DEBUG(x) qCDebug(LOG_KTE) << x #else #define FAST_DEBUG(x) #endif class KateRegExpSearch::ReplacementStream { public: struct counter { counter(int value, int minWidth) : value(value) , minWidth(minWidth) { } const int value; const int minWidth; }; struct cap { cap(int n) : n(n) { } const int n; }; enum CaseConversion { upperCase, ///< \U ... uppercase from now on upperCaseFirst, ///< \u ... uppercase the first letter lowerCase, ///< \L ... lowercase from now on lowerCaseFirst, ///< \l ... lowercase the first letter keepCase ///< \E ... back to original case }; public: ReplacementStream(const QStringList &capturedTexts); QString str() const { return m_str; } ReplacementStream &operator<<(const QString &); ReplacementStream &operator<<(const counter &); ReplacementStream &operator<<(const cap &); ReplacementStream &operator<<(CaseConversion); private: const QStringList m_capturedTexts; CaseConversion m_caseConversion; QString m_str; }; KateRegExpSearch::ReplacementStream::ReplacementStream(const QStringList &capturedTexts) : m_capturedTexts(capturedTexts) , m_caseConversion(keepCase) { } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(const QString &str) { switch (m_caseConversion) { - case upperCase: - // Copy as uppercase - m_str.append(str.toUpper()); - break; - - case upperCaseFirst: - if (str.length() > 0) { - m_str.append(str.at(0).toUpper()); - m_str.append(str.midRef(1)); - m_caseConversion = keepCase; - } - break; - - case lowerCase: - // Copy as lowercase - m_str.append(str.toLower()); - break; - - case lowerCaseFirst: - if (str.length() > 0) { - m_str.append(str.at(0).toLower()); - m_str.append(str.midRef(1)); - m_caseConversion = keepCase; - } - break; + case upperCase: + // Copy as uppercase + m_str.append(str.toUpper()); + break; + + case upperCaseFirst: + if (str.length() > 0) { + m_str.append(str.at(0).toUpper()); + m_str.append(str.midRef(1)); + m_caseConversion = keepCase; + } + break; + + case lowerCase: + // Copy as lowercase + m_str.append(str.toLower()); + break; + + case lowerCaseFirst: + if (str.length() > 0) { + m_str.append(str.at(0).toLower()); + m_str.append(str.midRef(1)); + m_caseConversion = keepCase; + } + break; - case keepCase: // FALLTHROUGH - default: - // Copy unmodified - m_str.append(str); - break; + case keepCase: // FALLTHROUGH + default: + // Copy unmodified + m_str.append(str); + break; } return *this; } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(const counter &c) { // Zero padded counter value m_str.append(QStringLiteral("%1").arg(c.value, c.minWidth, 10, QLatin1Char('0'))); return *this; } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(const cap &cap) { if (0 <= cap.n && cap.n < m_capturedTexts.size()) { (*this) << m_capturedTexts[cap.n]; } else { // Insert just the number to be consistent with QRegExp ("\c" becomes "c") m_str.append(QString::number(cap.n)); } return *this; } KateRegExpSearch::ReplacementStream &KateRegExpSearch::ReplacementStream::operator<<(CaseConversion caseConversion) { m_caseConversion = caseConversion; return *this; } // BEGIN d'tor, c'tor // // KateSearch Constructor // KateRegExpSearch::KateRegExpSearch(const KTextEditor::Document *document, Qt::CaseSensitivity caseSensitivity) : m_document(document) , m_caseSensitivity(caseSensitivity) { } // // KateSearch Destructor // KateRegExpSearch::~KateRegExpSearch() { } // helper structs for captures re-construction struct TwoViewCursor { int index; int openLine; int openCol; int closeLine; int closeCol; // note: open/close distinction does not seem needed // anymore. i keep it to make a potential way back // easier. overhead is minimal. }; struct IndexPair { int openIndex; int closeIndex; }; QVector KateRegExpSearch::search(const QString &pattern, const KTextEditor::Range &inputRange, bool backwards) { // regex search KateRegExp regexp(pattern, m_caseSensitivity); if (regexp.isEmpty() || !regexp.isValid() || !inputRange.isValid() || (inputRange.start() == inputRange.end())) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } // detect pattern type (single- or mutli-line) bool isMultiLine; // detect '.' and '\s' and fix them const bool dotMatchesNewline = false; // TODO const int replacements = regexp.repairPattern(isMultiLine); if (dotMatchesNewline && (replacements > 0)) { isMultiLine = true; } const int firstLineIndex = inputRange.start().line(); const int minColStart = inputRange.start().column(); // const int maxColEnd = inputRange.end().column(); if (isMultiLine) { // multi-line regex search (both forward and backward mode) QString wholeDocument; const int inputLineCount = inputRange.end().line() - inputRange.start().line() + 1; FAST_DEBUG("multi line search (lines " << firstLineIndex << ".." << firstLineIndex + inputLineCount - 1 << ")"); // nothing to do... if (firstLineIndex >= m_document->lines()) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } QVector lineLens(inputLineCount); // first line if (firstLineIndex < 0 || m_document->lines() <= firstLineIndex) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } const QString firstLine = m_document->line(firstLineIndex); const int firstLineLen = firstLine.length() - minColStart; wholeDocument.append(firstLine.rightRef(firstLineLen)); lineLens[0] = firstLineLen; FAST_DEBUG(" line" << 0 << "has length" << lineLens[0]); // second line and after for (int i = 1; i < inputLineCount; i++) { const int lineNum = firstLineIndex + i; if (lineNum < 0 || m_document->lines() <= lineNum) { QVector result; result.append(KTextEditor::Range::invalid()); return result; } const QString text = m_document->line(lineNum); lineLens[i] = text.length(); wholeDocument.append(QLatin1Char('\n')); wholeDocument.append(text); FAST_DEBUG(" line" << i << "has length" << lineLens[i]); } const int pos = backwards ? regexp.lastIndexIn(wholeDocument, 0, wholeDocument.length()) : regexp.indexIn(wholeDocument, 0, wholeDocument.length()); if (pos == -1) { // no match FAST_DEBUG("not found"); { QVector result; result.append(KTextEditor::Range::invalid()); return result; } } #ifdef FAST_DEBUG_ENABLE const int matchLen = regexp.matchedLength(); FAST_DEBUG("found at relative pos " << pos << ", length " << matchLen); #endif // save opening and closing indices and build a map. // the correct values will be written into it later. QMap indicesToCursors; const int numCaptures = regexp.numCaptures(); QVector indexPairs(1 + numCaptures); for (int z = 0; z <= numCaptures; z++) { const int openIndex = regexp.pos(z); IndexPair &pair = indexPairs[z]; if (openIndex == -1) { // empty capture gives invalid pair.openIndex = -1; pair.closeIndex = -1; FAST_DEBUG("capture []"); } else { const int closeIndex = openIndex + regexp.cap(z).length(); pair.openIndex = openIndex; pair.closeIndex = closeIndex; FAST_DEBUG("capture [" << pair.openIndex << ".." << pair.closeIndex << "]"); // each key no more than once if (!indicesToCursors.contains(openIndex)) { TwoViewCursor *twoViewCursor = new TwoViewCursor; twoViewCursor->index = openIndex; indicesToCursors.insert(openIndex, twoViewCursor); FAST_DEBUG(" border index added: " << openIndex); } if (!indicesToCursors.contains(closeIndex)) { TwoViewCursor *twoViewCursor = new TwoViewCursor; twoViewCursor->index = closeIndex; indicesToCursors.insert(closeIndex, twoViewCursor); FAST_DEBUG(" border index added: " << closeIndex); } } } // find out where they belong int curRelLine = 0; int curRelCol = 0; int curRelIndex = 0; QMap::const_iterator iter = indicesToCursors.constBegin(); while (iter != indicesToCursors.constEnd()) { // forward to index, save line/col const int index = (*iter)->index; FAST_DEBUG("resolving position" << index); TwoViewCursor &twoViewCursor = *(*iter); while (curRelIndex <= index) { FAST_DEBUG("walk pos (" << curRelLine << "," << curRelCol << ") = " << curRelIndex << "relative, steps more to go" << index - curRelIndex); const int curRelLineLen = lineLens[curRelLine]; const int curLineRemainder = curRelLineLen - curRelCol; const int lineFeedIndex = curRelIndex + curLineRemainder; if (index <= lineFeedIndex) { if (index == lineFeedIndex) { // on this line _on_ line feed FAST_DEBUG(" on line feed"); const int absLine = curRelLine + firstLineIndex; twoViewCursor.openLine = twoViewCursor.closeLine = absLine; twoViewCursor.openCol = twoViewCursor.closeCol = ((curRelLine == 0) ? minColStart : 0) + curRelLineLen; // advance to next line const int advance = (index - curRelIndex) + 1; curRelLine++; curRelCol = 0; curRelIndex += advance; } else { // index < lineFeedIndex // on this line _before_ line feed FAST_DEBUG(" before line feed"); const int diff = (index - curRelIndex); const int absLine = curRelLine + firstLineIndex; const int absCol = ((curRelLine == 0) ? minColStart : 0) + curRelCol + diff; twoViewCursor.openLine = twoViewCursor.closeLine = absLine; twoViewCursor.openCol = twoViewCursor.closeCol = absCol; // advance on same line const int advance = diff + 1; curRelCol += advance; curRelIndex += advance; } FAST_DEBUG("open(" << twoViewCursor.openLine << "," << twoViewCursor.openCol << ") close(" << twoViewCursor.closeLine << "," << twoViewCursor.closeCol << ")"); } else { // if (index > lineFeedIndex) // not on this line // advance to next line FAST_DEBUG(" not on this line"); const int advance = curLineRemainder + 1; curRelLine++; curRelCol = 0; curRelIndex += advance; } } ++iter; } // build result array QVector result(1 + numCaptures); for (int y = 0; y <= numCaptures; y++) { IndexPair &pair = indexPairs[y]; if ((pair.openIndex == -1) || (pair.closeIndex == -1)) { result[y] = KTextEditor::Range::invalid(); } else { const TwoViewCursor *const openCursors = indicesToCursors[pair.openIndex]; const TwoViewCursor *const closeCursors = indicesToCursors[pair.closeIndex]; const int startLine = openCursors->openLine; const int startCol = openCursors->openCol; const int endLine = closeCursors->closeLine; const int endCol = closeCursors->closeCol; FAST_DEBUG("range " << y << ": (" << startLine << ", " << startCol << ")..(" << endLine << ", " << endCol << ")"); result[y] = KTextEditor::Range(startLine, startCol, endLine, endCol); } } // free structs allocated for indicesToCursors iter = indicesToCursors.constBegin(); while (iter != indicesToCursors.constEnd()) { TwoViewCursor *const twoViewCursor = *iter; delete twoViewCursor; ++iter; } return result; } else { // single-line regex search (both forward of backward mode) const int minLeft = inputRange.start().column(); const uint maxRight = inputRange.end().column(); // first not included const int forMin = inputRange.start().line(); const int forMax = inputRange.end().line(); const int forInit = backwards ? forMax : forMin; const int forInc = backwards ? -1 : +1; FAST_DEBUG("single line " << (backwards ? forMax : forMin) << ".." << (backwards ? forMin : forMax)); for (int j = forInit; (forMin <= j) && (j <= forMax); j += forInc) { if (j < 0 || m_document->lines() <= j) { FAST_DEBUG("searchText | line " << j << ": no"); QVector result; result.append(KTextEditor::Range::invalid()); return result; } const QString textLine = m_document->line(j); // Find (and don't match ^ in between...) const int first = (j == forMin) ? minLeft : 0; const int last = (j == forMax) ? maxRight : textLine.length(); const int foundAt = (backwards ? regexp.lastIndexIn(textLine, first, last) : regexp.indexIn(textLine, first, last)); const bool found = (foundAt != -1); /* TODO do we still need this? // A special case which can only occur when searching with a regular expression consisting // only of a lookahead (e.g. ^(?=\{) for a function beginning without selecting '{'). if (myMatchLen == 0 && line == startPosition.line() && foundAt == (uint) col) { if (col < lineLength(line)) col++; else { line++; col = 0; } continue; } */ if (found) { FAST_DEBUG("line " << j << ": yes"); // build result array const int numCaptures = regexp.numCaptures(); QVector result(1 + numCaptures); result[0] = KTextEditor::Range(j, foundAt, j, foundAt + regexp.matchedLength()); FAST_DEBUG("result range " << 0 << ": (" << j << ", " << foundAt << ")..(" << j << ", " << foundAt + regexp.matchedLength() << ")"); for (int y = 1; y <= numCaptures; y++) { const int openIndex = regexp.pos(y); if (openIndex == -1) { result[y] = KTextEditor::Range::invalid(); FAST_DEBUG("capture []"); } else { const int closeIndex = openIndex + regexp.cap(y).length(); FAST_DEBUG("result range " << y << ": (" << j << ", " << openIndex << ")..(" << j << ", " << closeIndex << ")"); result[y] = KTextEditor::Range(j, openIndex, j, closeIndex); } } return result; } else { FAST_DEBUG("searchText | line " << j << ": no"); } } } QVector result; result.append(KTextEditor::Range::invalid()); return result; } /*static*/ QString KateRegExpSearch::escapePlaintext(const QString &text) { return buildReplacement(text, QStringList(), 0, false); } /*static*/ QString KateRegExpSearch::buildReplacement(const QString &text, const QStringList &capturedTexts, int replacementCounter) { return buildReplacement(text, capturedTexts, replacementCounter, true); } /*static*/ QString KateRegExpSearch::buildReplacement(const QString &text, const QStringList &capturedTexts, int replacementCounter, bool replacementGoodies) { // get input const int inputLen = text.length(); int input = 0; // walker index // prepare output ReplacementStream out(capturedTexts); while (input < inputLen) { switch (text[input].unicode()) { - case L'\n': + case L'\n': + out << text[input]; + input++; + break; + + case L'\\': + if (input + 1 >= inputLen) { + // copy backslash out << text[input]; input++; break; + } - case L'\\': - if (input + 1 >= inputLen) { - // copy backslash - out << text[input]; - input++; - break; - } - - switch (text[input + 1].unicode()) { - case L'0': // "\0000".."\0377" - if (input + 4 >= inputLen) { - out << ReplacementStream::cap(0); - input += 2; - } else { - bool stripAndSkip = false; - const ushort text_2 = text[input + 2].unicode(); - if ((text_2 >= L'0') && (text_2 <= L'3')) { - const ushort text_3 = text[input + 3].unicode(); - if ((text_3 >= L'0') && (text_3 <= L'7')) { - const ushort text_4 = text[input + 4].unicode(); - if ((text_4 >= L'0') && (text_4 <= L'7')) { - int digits[3]; - for (int i = 0; i < 3; i++) { - digits[i] = 7 - (L'7' - text[input + 2 + i].unicode()); - } - const int ch = 64 * digits[0] + 8 * digits[1] + digits[2]; - out << QChar(ch); - input += 5; - } else { - stripAndSkip = true; - } - } else { - stripAndSkip = true; + switch (text[input + 1].unicode()) { + case L'0': // "\0000".."\0377" + if (input + 4 >= inputLen) { + out << ReplacementStream::cap(0); + input += 2; + } else { + bool stripAndSkip = false; + const ushort text_2 = text[input + 2].unicode(); + if ((text_2 >= L'0') && (text_2 <= L'3')) { + const ushort text_3 = text[input + 3].unicode(); + if ((text_3 >= L'0') && (text_3 <= L'7')) { + const ushort text_4 = text[input + 4].unicode(); + if ((text_4 >= L'0') && (text_4 <= L'7')) { + int digits[3]; + for (int i = 0; i < 3; i++) { + digits[i] = 7 - (L'7' - text[input + 2 + i].unicode()); } + const int ch = 64 * digits[0] + 8 * digits[1] + digits[2]; + out << QChar(ch); + input += 5; } else { stripAndSkip = true; } - - if (stripAndSkip) { - out << ReplacementStream::cap(0); - input += 2; - } + } else { + stripAndSkip = true; } - break; + } else { + stripAndSkip = true; + } - // single letter captures \x - case L'1': - case L'2': - case L'3': - case L'4': - case L'5': - case L'6': - case L'7': - case L'8': - case L'9': - out << ReplacementStream::cap(9 - (L'9' - text[input + 1].unicode())); + if (stripAndSkip) { + out << ReplacementStream::cap(0); input += 2; - break; + } + } + break; - // multi letter captures \{xxxx} - case L'{': { - // allow {1212124}.... captures, see bug 365124 + testReplaceManyCapturesBug365124 - int capture = 0; - int captureSize = 2; - while ((input + captureSize) < inputLen) { - const ushort nextDigit = text[input + captureSize].unicode(); - if ((nextDigit >= L'0') && (nextDigit <= L'9')) { - capture = (10 * capture) + (9 - (L'9' - nextDigit)); - ++captureSize; - continue; - } - if (nextDigit == L'}') { - ++captureSize; - break; - } - break; - } - out << ReplacementStream::cap(capture); - input += captureSize; + // single letter captures \x + case L'1': + case L'2': + case L'3': + case L'4': + case L'5': + case L'6': + case L'7': + case L'8': + case L'9': + out << ReplacementStream::cap(9 - (L'9' - text[input + 1].unicode())); + input += 2; + break; + + // multi letter captures \{xxxx} + case L'{': { + // allow {1212124}.... captures, see bug 365124 + testReplaceManyCapturesBug365124 + int capture = 0; + int captureSize = 2; + while ((input + captureSize) < inputLen) { + const ushort nextDigit = text[input + captureSize].unicode(); + if ((nextDigit >= L'0') && (nextDigit <= L'9')) { + capture = (10 * capture) + (9 - (L'9' - nextDigit)); + ++captureSize; + continue; + } + if (nextDigit == L'}') { + ++captureSize; break; } + break; + } + out << ReplacementStream::cap(capture); + input += captureSize; + break; + } - case L'E': // FALLTHROUGH - case L'L': // FALLTHROUGH - case L'l': // FALLTHROUGH - case L'U': // FALLTHROUGH - case L'u': - if (!replacementGoodies) { - // strip backslash ("\?" -> "?") - out << text[input + 1]; - } else { - // handle case switcher - switch (text[input + 1].unicode()) { - case L'L': - out << ReplacementStream::lowerCase; - break; - - case L'l': - out << ReplacementStream::lowerCaseFirst; - break; - - case L'U': - out << ReplacementStream::upperCase; - break; - - case L'u': - out << ReplacementStream::upperCaseFirst; - break; - - case L'E': // FALLTHROUGH - default: - out << ReplacementStream::keepCase; - } - } - input += 2; + case L'E': // FALLTHROUGH + case L'L': // FALLTHROUGH + case L'l': // FALLTHROUGH + case L'U': // FALLTHROUGH + case L'u': + if (!replacementGoodies) { + // strip backslash ("\?" -> "?") + out << text[input + 1]; + } else { + // handle case switcher + switch (text[input + 1].unicode()) { + case L'L': + out << ReplacementStream::lowerCase; break; - case L'#': - if (!replacementGoodies) { - // strip backslash ("\?" -> "?") - out << text[input + 1]; - input += 2; - } else { - // handle replacement counter - // eat and count all following hash marks - // each hash stands for a leading zero: \### will produces 001, 002, ... - int minWidth = 1; - while ((input + minWidth + 1 < inputLen) && (text[input + minWidth + 1].unicode() == L'#')) { - minWidth++; - } - out << ReplacementStream::counter(replacementCounter, minWidth); - input += 1 + minWidth; - } + case L'l': + out << ReplacementStream::lowerCaseFirst; break; - case L'a': - out << QChar(0x07); - input += 2; + case L'U': + out << ReplacementStream::upperCase; break; - case L'f': - out << QChar(0x0c); - input += 2; + case L'u': + out << ReplacementStream::upperCaseFirst; break; - case L'n': - out << QChar(0x0a); - input += 2; - break; + case L'E': // FALLTHROUGH + default: + out << ReplacementStream::keepCase; + } + } + input += 2; + break; - case L'r': - out << QChar(0x0d); - input += 2; - break; + case L'#': + if (!replacementGoodies) { + // strip backslash ("\?" -> "?") + out << text[input + 1]; + input += 2; + } else { + // handle replacement counter + // eat and count all following hash marks + // each hash stands for a leading zero: \### will produces 001, 002, ... + int minWidth = 1; + while ((input + minWidth + 1 < inputLen) && (text[input + minWidth + 1].unicode() == L'#')) { + minWidth++; + } + out << ReplacementStream::counter(replacementCounter, minWidth); + input += 1 + minWidth; + } + break; - case L't': - out << QChar(0x09); - input += 2; - break; + case L'a': + out << QChar(0x07); + input += 2; + break; - case L'v': - out << QChar(0x0b); - input += 2; - break; + case L'f': + out << QChar(0x0c); + input += 2; + break; - case L'x': // "\x0000".."\xffff" - if (input + 5 >= inputLen) { - // strip backslash ("\x" -> "x") - out << text[input + 1]; - input += 2; - } else { - bool stripAndSkip = false; - const ushort text_2 = text[input + 2].unicode(); - if (((text_2 >= L'0') && (text_2 <= L'9')) || ((text_2 >= L'a') && (text_2 <= L'f')) || ((text_2 >= L'A') && (text_2 <= L'F'))) { - const ushort text_3 = text[input + 3].unicode(); - if (((text_3 >= L'0') && (text_3 <= L'9')) || ((text_3 >= L'a') && (text_3 <= L'f')) || ((text_3 >= L'A') && (text_3 <= L'F'))) { - const ushort text_4 = text[input + 4].unicode(); - if (((text_4 >= L'0') && (text_4 <= L'9')) || ((text_4 >= L'a') && (text_4 <= L'f')) || ((text_4 >= L'A') && (text_4 <= L'F'))) { - const ushort text_5 = text[input + 5].unicode(); - if (((text_5 >= L'0') && (text_5 <= L'9')) || ((text_5 >= L'a') && (text_5 <= L'f')) || ((text_5 >= L'A') && (text_5 <= L'F'))) { - int digits[4]; - for (int i = 0; i < 4; i++) { - const ushort cur = text[input + 2 + i].unicode(); - if ((cur >= L'0') && (cur <= L'9')) { - digits[i] = 9 - (L'9' - cur); - } else if ((cur >= L'a') && (cur <= L'f')) { - digits[i] = 15 - (L'f' - cur); - } else { // if ((cur >= L'A') && (cur <= L'F'))) - digits[i] = 15 - (L'F' - cur); - } - } - - const int ch = 4096 * digits[0] + 256 * digits[1] + 16 * digits[2] + digits[3]; - out << QChar(ch); - input += 6; - } else { - stripAndSkip = true; + case L'n': + out << QChar(0x0a); + input += 2; + break; + + case L'r': + out << QChar(0x0d); + input += 2; + break; + + case L't': + out << QChar(0x09); + input += 2; + break; + + case L'v': + out << QChar(0x0b); + input += 2; + break; + + case L'x': // "\x0000".."\xffff" + if (input + 5 >= inputLen) { + // strip backslash ("\x" -> "x") + out << text[input + 1]; + input += 2; + } else { + bool stripAndSkip = false; + const ushort text_2 = text[input + 2].unicode(); + if (((text_2 >= L'0') && (text_2 <= L'9')) || ((text_2 >= L'a') && (text_2 <= L'f')) || ((text_2 >= L'A') && (text_2 <= L'F'))) { + const ushort text_3 = text[input + 3].unicode(); + if (((text_3 >= L'0') && (text_3 <= L'9')) || ((text_3 >= L'a') && (text_3 <= L'f')) || ((text_3 >= L'A') && (text_3 <= L'F'))) { + const ushort text_4 = text[input + 4].unicode(); + if (((text_4 >= L'0') && (text_4 <= L'9')) || ((text_4 >= L'a') && (text_4 <= L'f')) || ((text_4 >= L'A') && (text_4 <= L'F'))) { + const ushort text_5 = text[input + 5].unicode(); + if (((text_5 >= L'0') && (text_5 <= L'9')) || ((text_5 >= L'a') && (text_5 <= L'f')) || ((text_5 >= L'A') && (text_5 <= L'F'))) { + int digits[4]; + for (int i = 0; i < 4; i++) { + const ushort cur = text[input + 2 + i].unicode(); + if ((cur >= L'0') && (cur <= L'9')) { + digits[i] = 9 - (L'9' - cur); + } else if ((cur >= L'a') && (cur <= L'f')) { + digits[i] = 15 - (L'f' - cur); + } else { // if ((cur >= L'A') && (cur <= L'F'))) + digits[i] = 15 - (L'F' - cur); } - } else { - stripAndSkip = true; } + + const int ch = 4096 * digits[0] + 256 * digits[1] + 16 * digits[2] + digits[3]; + out << QChar(ch); + input += 6; } else { stripAndSkip = true; } + } else { + stripAndSkip = true; } - - if (stripAndSkip) { - // strip backslash ("\x" -> "x") - out << text[input + 1]; - input += 2; - } + } else { + stripAndSkip = true; } - break; + } - default: - // strip backslash ("\?" -> "?") + if (stripAndSkip) { + // strip backslash ("\x" -> "x") out << text[input + 1]; input += 2; + } } break; default: - out << text[input]; - input++; + // strip backslash ("\?" -> "?") + out << text[input + 1]; + input += 2; + } + break; + + default: + out << text[input]; + input++; } } return out.str(); } // Kill our helpers again #ifdef FAST_DEBUG_ENABLE #undef FAST_DEBUG_ENABLE #endif #undef FAST_DEBUG diff --git a/src/search/katesearchbar.cpp b/src/search/katesearchbar.cpp index c0ac9413..f6a6f369 100644 --- a/src/search/katesearchbar.cpp +++ b/src/search/katesearchbar.cpp @@ -1,1684 +1,1684 @@ /* This file is part of the KDE libraries Copyright (C) 2009-2010 Bernhard Beschow Copyright (C) 2007 Sebastian Pipping Copyright (C) 2007 Matthew Woehlke Copyright (C) 2007 Thomas Friedrichsmeier This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katesearchbar.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katematch.h" #include "kateregexp.h" #include "katerenderer.h" #include "kateundomanager.h" #include "kateview.h" #include #include #include #include "ui_searchbarincremental.h" #include "ui_searchbarpower.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // Turn debug messages on/off here // #define FAST_DEBUG_ENABLE #ifdef FAST_DEBUG_ENABLE #define FAST_DEBUG(x) qCDebug(LOG_KTE) << x #else #define FAST_DEBUG(x) #endif using namespace KTextEditor; namespace { class AddMenuManager { private: QVector m_insertBefore; QVector m_insertAfter; QSet m_actionPointers; uint m_indexWalker; QMenu *m_menu; public: AddMenuManager(QMenu *parent, int expectedItemCount) : m_insertBefore(QVector(expectedItemCount)) , m_insertAfter(QVector(expectedItemCount)) , m_indexWalker(0) , m_menu(nullptr) { Q_ASSERT(parent != nullptr); m_menu = parent->addMenu(i18n("Add...")); if (m_menu == nullptr) { return; } m_menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); } void enableMenu(bool enabled) { if (m_menu == nullptr) { return; } m_menu->setEnabled(enabled); } void addEntry(const QString &before, const QString &after, const QString &description, const QString &realBefore = QString(), const QString &realAfter = QString()) { if (m_menu == nullptr) { return; } QAction *const action = m_menu->addAction(before + after + QLatin1Char('\t') + description); m_insertBefore[m_indexWalker] = QString(realBefore.isEmpty() ? before : realBefore); m_insertAfter[m_indexWalker] = QString(realAfter.isEmpty() ? after : realAfter); action->setData(QVariant(m_indexWalker++)); m_actionPointers.insert(action); } void addSeparator() { if (m_menu == nullptr) { return; } m_menu->addSeparator(); } void handle(QAction *action, QLineEdit *lineEdit) { if (!m_actionPointers.contains(action)) { return; } const int cursorPos = lineEdit->cursorPosition(); const int index = action->data().toUInt(); const QString &before = m_insertBefore[index]; const QString &after = m_insertAfter[index]; lineEdit->insert(before + after); lineEdit->setCursorPosition(cursorPos + before.count()); lineEdit->setFocus(); } }; } // anon namespace KateSearchBar::KateSearchBar(bool initAsPower, KTextEditor::ViewPrivate *view, KateViewConfig *config) : KateViewBarWidget(true, view) , m_view(view) , m_config(config) , m_layout(new QVBoxLayout()) , m_widget(nullptr) , m_incUi(nullptr) , m_incInitCursor(view->cursorPosition()) , m_powerUi(nullptr) , highlightMatchAttribute(new Attribute()) , highlightReplacementAttribute(new Attribute()) , m_incHighlightAll(false) , m_incFromCursor(true) , m_incMatchCase(false) , m_powerMatchCase(true) , m_powerFromCursor(false) , m_powerHighlightAll(false) , m_powerMode(0) { connect(view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); connect(view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); connect(this, &KateSearchBar::findOrReplaceAllFinished, this, &KateSearchBar::endFindOrReplaceAll); // init match attribute Attribute::Ptr mouseInAttribute(new Attribute()); mouseInAttribute->setFontBold(true); highlightMatchAttribute->setDynamicAttribute(Attribute::ActivateMouseIn, mouseInAttribute); Attribute::Ptr caretInAttribute(new Attribute()); caretInAttribute->setFontItalic(true); highlightMatchAttribute->setDynamicAttribute(Attribute::ActivateCaretIn, caretInAttribute); updateHighlightColors(); // Modify parent QWidget *const widget = centralWidget(); widget->setLayout(m_layout); m_layout->setContentsMargins(0, 0, 0, 0); // allow to have small size, for e.g. Kile setMinimumWidth(100); // Copy global to local config backup const auto searchFlags = m_config->searchFlags(); m_incHighlightAll = (searchFlags & KateViewConfig::IncHighlightAll) != 0; m_incFromCursor = (searchFlags & KateViewConfig::IncFromCursor) != 0; m_incMatchCase = (searchFlags & KateViewConfig::IncMatchCase) != 0; m_powerMatchCase = (searchFlags & KateViewConfig::PowerMatchCase) != 0; m_powerFromCursor = (searchFlags & KateViewConfig::PowerFromCursor) != 0; m_powerHighlightAll = (searchFlags & KateViewConfig::PowerHighlightAll) != 0; m_powerMode = ((searchFlags & KateViewConfig::PowerModeRegularExpression) != 0) ? MODE_REGEX : (((searchFlags & KateViewConfig::PowerModeEscapeSequences) != 0) ? MODE_ESCAPE_SEQUENCES : (((searchFlags & KateViewConfig::PowerModeWholeWords) != 0) ? MODE_WHOLE_WORDS : MODE_PLAIN_TEXT)); // Load one of either dialogs if (initAsPower) { enterPowerMode(); } else { enterIncrementalMode(); } updateSelectionOnly(); } KateSearchBar::~KateSearchBar() { if (!m_cancelFindOrReplace) { // Finish/Cancel the still running job to avoid a crash endFindOrReplaceAll(); } clearHighlights(); delete m_layout; delete m_widget; delete m_incUi; delete m_powerUi; } void KateSearchBar::closed() { // remove search from the view bar, because it vertically bloats up the // stacked layout in KateViewBar. if (viewBar()) { viewBar()->removeBarWidget(this); } clearHighlights(); } void KateSearchBar::setReplacementPattern(const QString &replacementPattern) { Q_ASSERT(isPower()); if (this->replacementPattern() == replacementPattern) { return; } m_powerUi->replacement->setEditText(replacementPattern); } QString KateSearchBar::replacementPattern() const { Q_ASSERT(isPower()); return m_powerUi->replacement->currentText(); } void KateSearchBar::setSearchMode(KateSearchBar::SearchMode mode) { Q_ASSERT(isPower()); m_powerUi->searchMode->setCurrentIndex(mode); } void KateSearchBar::findNext() { const bool found = find(); if (found) { QComboBox *combo = m_powerUi != nullptr ? m_powerUi->pattern : m_incUi->pattern; // Add to search history addCurrentTextToHistory(combo); } } void KateSearchBar::findPrevious() { const bool found = find(SearchBackward); if (found) { QComboBox *combo = m_powerUi != nullptr ? m_powerUi->pattern : m_incUi->pattern; // Add to search history addCurrentTextToHistory(combo); } } void KateSearchBar::showResultMessage() { QString text; if (m_replaceMode) { text = i18ncp("short translation", "1 replacement made", "%1 replacements made", m_matchCounter); } else { text = i18ncp("short translation", "1 match found", "%1 matches found", m_matchCounter); } if (m_infoMessage) { m_infoMessage->setText(text); } else { m_infoMessage = new KTextEditor::Message(text, KTextEditor::Message::Positive); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(3000); // 3 seconds m_infoMessage->setView(m_view); m_view->doc()->postMessage(m_infoMessage); } } void KateSearchBar::showSearchWrappedHint(SearchDirection searchDirection) { // show message widget when wrapping const QIcon icon = (searchDirection == SearchForward) ? QIcon::fromTheme(QStringLiteral("go-down-search")) : QIcon::fromTheme(QStringLiteral("go-up-search")); if (!m_wrappedMessage || m_lastSearchDirection != searchDirection) { m_lastSearchDirection = searchDirection; m_wrappedMessage = new KTextEditor::Message(i18n("Search wrapped"), KTextEditor::Message::Information); m_wrappedMessage->setIcon(icon); m_wrappedMessage->setPosition(KTextEditor::Message::BottomInView); m_wrappedMessage->setAutoHide(2000); m_wrappedMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_wrappedMessage->setView(m_view); m_view->doc()->postMessage(m_wrappedMessage); } } void KateSearchBar::highlightMatch(const Range &range) { KTextEditor::MovingRange *const highlight = m_view->doc()->newMovingRange(range, Kate::TextRange::DoNotExpand); highlight->setView(m_view); // show only in this view highlight->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlight->setZDepth(-10000.0); highlight->setAttribute(highlightMatchAttribute); m_hlRanges.append(highlight); } void KateSearchBar::highlightReplacement(const Range &range) { KTextEditor::MovingRange *const highlight = m_view->doc()->newMovingRange(range, Kate::TextRange::DoNotExpand); highlight->setView(m_view); // show only in this view highlight->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlight->setZDepth(-10000.0); highlight->setAttribute(highlightReplacementAttribute); m_hlRanges.append(highlight); } void KateSearchBar::indicateMatch(MatchResult matchResult) { QLineEdit *const lineEdit = isPower() ? m_powerUi->pattern->lineEdit() : m_incUi->pattern->lineEdit(); QPalette background(lineEdit->palette()); switch (matchResult) { + case MatchFound: // FALLTHROUGH + case MatchWrappedForward: + case MatchWrappedBackward: + // Green background for line edit + KColorScheme::adjustBackground(background, KColorScheme::PositiveBackground); + break; + case MatchMismatch: + // Red background for line edit + KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground); + break; + case MatchNothing: + // Reset background of line edit + background = QPalette(); + break; + case MatchNeutral: + KColorScheme::adjustBackground(background, KColorScheme::NeutralBackground); + break; + } + + // Update status label + if (m_incUi != nullptr) { + QPalette foreground(m_incUi->status->palette()); + switch (matchResult) { case MatchFound: // FALLTHROUGH + case MatchNothing: + KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); + m_incUi->status->clear(); + break; case MatchWrappedForward: case MatchWrappedBackward: - // Green background for line edit - KColorScheme::adjustBackground(background, KColorScheme::PositiveBackground); + KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); + if (matchResult == MatchWrappedBackward) { + m_incUi->status->setText(i18n("Reached top, continued from bottom")); + } else { + m_incUi->status->setText(i18n("Reached bottom, continued from top")); + } break; case MatchMismatch: - // Red background for line edit - KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground); - break; - case MatchNothing: - // Reset background of line edit - background = QPalette(); + KColorScheme::adjustForeground(foreground, KColorScheme::NegativeText, QPalette::WindowText, KColorScheme::Window); + m_incUi->status->setText(i18n("Not found")); break; case MatchNeutral: - KColorScheme::adjustBackground(background, KColorScheme::NeutralBackground); + /* do nothing */ break; - } - - // Update status label - if (m_incUi != nullptr) { - QPalette foreground(m_incUi->status->palette()); - switch (matchResult) { - case MatchFound: // FALLTHROUGH - case MatchNothing: - KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); - m_incUi->status->clear(); - break; - case MatchWrappedForward: - case MatchWrappedBackward: - KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); - if (matchResult == MatchWrappedBackward) { - m_incUi->status->setText(i18n("Reached top, continued from bottom")); - } else { - m_incUi->status->setText(i18n("Reached bottom, continued from top")); - } - break; - case MatchMismatch: - KColorScheme::adjustForeground(foreground, KColorScheme::NegativeText, QPalette::WindowText, KColorScheme::Window); - m_incUi->status->setText(i18n("Not found")); - break; - case MatchNeutral: - /* do nothing */ - break; } m_incUi->status->setPalette(foreground); } lineEdit->setPalette(background); } /*static*/ void KateSearchBar::selectRange(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { view->setCursorPositionInternal(range.end()); view->setSelection(range); } void KateSearchBar::selectRange2(const KTextEditor::Range &range) { disconnect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); selectRange(m_view, range); connect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); } void KateSearchBar::onIncPatternChanged(const QString &pattern) { if (!m_incUi) { return; } // clear prior highlightings (deletes info message if present) clearHighlights(); m_incUi->next->setDisabled(pattern.isEmpty()); m_incUi->prev->setDisabled(pattern.isEmpty()); KateMatch match(m_view->doc(), searchOptions()); if (!pattern.isEmpty()) { // Find, first try const Range inputRange = KTextEditor::Range(m_incInitCursor, m_view->document()->documentEnd()); match.searchText(inputRange, pattern); } const bool wrap = !match.isValid() && !pattern.isEmpty(); if (wrap) { // Find, second try const KTextEditor::Range inputRange = m_view->document()->documentRange(); match.searchText(inputRange, pattern); } const MatchResult matchResult = match.isValid() ? (wrap ? MatchWrappedForward : MatchFound) : pattern.isEmpty() ? MatchNothing : MatchMismatch; const Range selectionRange = pattern.isEmpty() ? Range(m_incInitCursor, m_incInitCursor) : match.isValid() ? match.range() : Range::invalid(); // don't update m_incInitCursor when we move the cursor disconnect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); selectRange2(selectionRange); connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); indicateMatch(matchResult); } void KateSearchBar::setMatchCase(bool matchCase) { if (this->matchCase() == matchCase) { return; } if (isPower()) { m_powerUi->matchCase->setChecked(matchCase); } else { m_incUi->matchCase->setChecked(matchCase); } } void KateSearchBar::onMatchCaseToggled(bool /*matchCase*/) { sendConfig(); if (m_incUi != nullptr) { // Re-search with new settings const QString pattern = m_incUi->pattern->currentText(); onIncPatternChanged(pattern); } else { indicateMatch(MatchNothing); } } bool KateSearchBar::matchCase() const { return isPower() ? m_powerUi->matchCase->isChecked() : m_incUi->matchCase->isChecked(); } void KateSearchBar::fixForSingleLine(Range &range, SearchDirection searchDirection) { FAST_DEBUG("Single-line workaround checking BEFORE" << range); if (searchDirection == SearchForward) { const int line = range.start().line(); const int col = range.start().column(); const int maxColWithNewline = m_view->document()->lineLength(line) + 1; if (col == maxColWithNewline) { FAST_DEBUG("Starting on a newline" << range); const int maxLine = m_view->document()->lines() - 1; if (line < maxLine) { range.setRange(Cursor(line + 1, 0), range.end()); FAST_DEBUG("Search range fixed to " << range); } else { FAST_DEBUG("Already at last line"); range = Range::invalid(); } } } else { const int col = range.end().column(); if (col == 0) { FAST_DEBUG("Ending after a newline" << range); const int line = range.end().line(); if (line > 0) { const int maxColWithNewline = m_view->document()->lineLength(line - 1); range.setRange(range.start(), Cursor(line - 1, maxColWithNewline)); FAST_DEBUG("Search range fixed to " << range); } else { FAST_DEBUG("Already at first line"); range = Range::invalid(); } } } FAST_DEBUG("Single-line workaround checking AFTER" << range); } void KateSearchBar::onReturnPressed() { const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); const bool shiftDown = (modifiers & Qt::ShiftModifier) != 0; const bool controlDown = (modifiers & Qt::ControlModifier) != 0; if (shiftDown) { // Shift down, search backwards findPrevious(); } else { // Shift up, search forwards findNext(); } if (controlDown) { emit hideMe(); } } bool KateSearchBar::findOrReplace(SearchDirection searchDirection, const QString *replacement) { // What to find? if (searchPattern().isEmpty()) { return false; // == Pattern error } // don't let selectionChanged signal mess around in this routine disconnect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); // clear previous highlights if there are any clearHighlights(); const SearchOptions enabledOptions = searchOptions(searchDirection); // Where to find? Range inputRange; const Range selection = m_view->selection() ? m_view->selectionRange() : Range::invalid(); if (selection.isValid()) { if (selectionOnly()) { // First match in selection inputRange = selection; } else { // Next match after/before selection if a match was selected before if (searchDirection == SearchForward) { inputRange.setRange(selection.start(), m_view->document()->documentEnd()); } else { inputRange.setRange(Cursor(0, 0), selection.end()); } } } else { // No selection const Cursor cursorPos = m_view->cursorPosition(); if (searchDirection == SearchForward) { inputRange.setRange(cursorPos, m_view->document()->documentEnd()); } else { inputRange.setRange(Cursor(0, 0), cursorPos); } } FAST_DEBUG("Search range is" << inputRange); { const bool regexMode = enabledOptions.testFlag(Regex); const bool multiLinePattern = regexMode ? KateRegExp(searchPattern()).isMultiLine() : false; // Single-line pattern workaround if (regexMode && !multiLinePattern) { fixForSingleLine(inputRange, searchDirection); } } KateMatch match(m_view->doc(), enabledOptions); Range afterReplace = Range::invalid(); // Find, first try match.searchText(inputRange, searchPattern()); if (match.isValid() && match.range() == selection) { // Same match again if (replacement != nullptr) { // Selection is match -> replace KTextEditor::MovingRange *smartInputRange = m_view->doc()->newMovingRange(inputRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); afterReplace = match.replace(*replacement, m_view->blockSelection()); inputRange = *smartInputRange; delete smartInputRange; } if (!selectionOnly()) { // Find, second try after old selection if (searchDirection == SearchForward) { const Cursor start = (replacement != nullptr) ? afterReplace.end() : selection.end(); inputRange.setRange(start, inputRange.end()); } else { const Cursor end = (replacement != nullptr) ? afterReplace.start() : selection.start(); inputRange.setRange(inputRange.start(), end); } } // Single-line pattern workaround fixForSingleLine(inputRange, searchDirection); match.searchText(inputRange, searchPattern()); } bool askWrap = !match.isValid() && (!selection.isValid() || !selectionOnly()); bool wrap = false; if (askWrap) { askWrap = false; wrap = true; } if (askWrap) { QString question = searchDirection == SearchForward ? i18n("Bottom of file reached. Continue from top?") : i18n("Top of file reached. Continue from bottom?"); wrap = (KMessageBox::questionYesNo(nullptr, question, i18n("Continue search?"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("DoNotShowAgainContinueSearchDialog")) == KMessageBox::Yes); } if (wrap) { showSearchWrappedHint(searchDirection); inputRange = m_view->document()->documentRange(); match.searchText(inputRange, searchPattern()); } if (match.isValid()) { selectRange2(match.range()); } const MatchResult matchResult = !match.isValid() ? MatchMismatch : !wrap ? MatchFound : searchDirection == SearchForward ? MatchWrappedForward : MatchWrappedBackward; indicateMatch(matchResult); // highlight replacements if applicable if (afterReplace.isValid()) { highlightReplacement(afterReplace); } // restore connection connect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); return true; // == No pattern error } void KateSearchBar::findAll() { // clear highlightings of prior search&replace action clearHighlights(); Range inputRange = (m_view->selection() && selectionOnly()) ? m_view->selectionRange() : m_view->document()->documentRange(); beginFindAll(inputRange); } void KateSearchBar::onPowerPatternChanged(const QString & /*pattern*/) { givePatternFeedback(); indicateMatch(MatchNothing); } bool KateSearchBar::isPatternValid() const { if (searchPattern().isEmpty()) { return false; } return searchOptions().testFlag(WholeWords) ? searchPattern().trimmed() == searchPattern() : searchOptions().testFlag(Regex) ? QRegularExpression(searchPattern()).isValid() : true; } void KateSearchBar::givePatternFeedback() { // Enable/disable next/prev and replace next/all m_powerUi->findNext->setEnabled(isPatternValid()); m_powerUi->findPrev->setEnabled(isPatternValid()); m_powerUi->replaceNext->setEnabled(isPatternValid()); m_powerUi->replaceAll->setEnabled(isPatternValid()); m_powerUi->findAll->setEnabled(isPatternValid()); } void KateSearchBar::addCurrentTextToHistory(QComboBox *combo) { const QString text = combo->currentText(); const int index = combo->findText(text); if (index > 0) { combo->removeItem(index); } if (index != 0) { combo->insertItem(0, text); combo->setCurrentIndex(0); } // sync to application config KTextEditor::EditorPrivate::self()->saveSearchReplaceHistoryModels(); } void KateSearchBar::backupConfig(bool ofPower) { if (ofPower) { m_powerMatchCase = m_powerUi->matchCase->isChecked(); m_powerMode = m_powerUi->searchMode->currentIndex(); } else { m_incMatchCase = m_incUi->matchCase->isChecked(); } } void KateSearchBar::sendConfig() { const auto pastFlags = m_config->searchFlags(); auto futureFlags = pastFlags; if (m_powerUi != nullptr) { const bool OF_POWER = true; backupConfig(OF_POWER); // Update power search flags only const auto incFlagsOnly = pastFlags & (KateViewConfig::IncHighlightAll | KateViewConfig::IncFromCursor | KateViewConfig::IncMatchCase); futureFlags = incFlagsOnly | (m_powerMatchCase ? KateViewConfig::PowerMatchCase : 0) | (m_powerFromCursor ? KateViewConfig::PowerFromCursor : 0) | (m_powerHighlightAll ? KateViewConfig::PowerHighlightAll : 0) | ((m_powerMode == MODE_REGEX) ? KateViewConfig::PowerModeRegularExpression : ((m_powerMode == MODE_ESCAPE_SEQUENCES) ? KateViewConfig::PowerModeEscapeSequences : ((m_powerMode == MODE_WHOLE_WORDS) ? KateViewConfig::PowerModeWholeWords : KateViewConfig::PowerModePlainText))); } else if (m_incUi != nullptr) { const bool OF_INCREMENTAL = false; backupConfig(OF_INCREMENTAL); // Update incremental search flags only const auto powerFlagsOnly = pastFlags & (KateViewConfig::PowerMatchCase | KateViewConfig::PowerFromCursor | KateViewConfig::PowerHighlightAll | KateViewConfig::PowerModeRegularExpression | KateViewConfig::PowerModeEscapeSequences | KateViewConfig::PowerModeWholeWords | KateViewConfig::PowerModePlainText); futureFlags = powerFlagsOnly | (m_incHighlightAll ? KateViewConfig::IncHighlightAll : 0) | (m_incFromCursor ? KateViewConfig::IncFromCursor : 0) | (m_incMatchCase ? KateViewConfig::IncMatchCase : 0); } // Adjust global config m_config->setSearchFlags(futureFlags); } void KateSearchBar::replaceNext() { const QString replacement = m_powerUi->replacement->currentText(); if (findOrReplace(SearchForward, &replacement)) { // Never merge replace actions with other replace actions/user actions m_view->doc()->undoManager()->undoSafePoint(); // Add to search history addCurrentTextToHistory(m_powerUi->pattern); // Add to replace history addCurrentTextToHistory(m_powerUi->replacement); } } // replacement == NULL --> Only highlight all matches // replacement != NULL --> Replace and highlight all matches void KateSearchBar::beginFindOrReplaceAll(Range inputRange, const QString &replacement, bool replaceMode /* = true*/) { // don't let selectionChanged signal mess around in this routine disconnect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); // Cancel job when user close the document to avoid crash connect(m_view->doc(), &KTextEditor::Document::aboutToClose, this, &KateSearchBar::endFindOrReplaceAll); if (m_powerUi) { // Offer Cancel button and disable not useful buttons m_powerUi->searchCancelStacked->setCurrentIndex(m_powerUi->searchCancelStacked->indexOf(m_powerUi->cancelPage)); m_powerUi->findNext->setEnabled(false); m_powerUi->findPrev->setEnabled(false); m_powerUi->replaceNext->setEnabled(false); } m_highlightRanges.clear(); m_inputRange = inputRange; m_workingRange = m_view->doc()->newMovingRange(m_inputRange); m_replacement = replacement; m_replaceMode = replaceMode; m_matchCounter = 0; m_cancelFindOrReplace = false; // Ensure we have a GO! findOrReplaceAll(); } void KateSearchBar::findOrReplaceAll() { const SearchOptions enabledOptions = searchOptions(SearchForward); const bool regexMode = enabledOptions.testFlag(Regex); const bool multiLinePattern = regexMode ? KateRegExp(searchPattern()).isMultiLine() : false; // we highlight all ranges of a replace, up to some hard limit // e.g. if you replace 100000 things, rendering will break down otherwise ;=) const int maxHighlightings = 65536; // reuse match object to avoid massive moving range creation KateMatch match(m_view->doc(), enabledOptions); bool block = m_view->selection() && m_view->blockSelection(); int line = m_inputRange.start().line(); QElapsedTimer rolex; // Watchog to suspend the work after some time rolex.start(); bool timeOut = false; bool done = false; do { if (block) { delete m_workingRange; // Never forget that! m_workingRange = m_view->doc()->newMovingRange(m_view->doc()->rangeOnLine(m_inputRange, line)); } do { match.searchText(*m_workingRange, searchPattern()); if (!match.isValid()) { done = true; break; } bool const originalMatchEmpty = match.isEmpty(); // Work with the match Range lastRange; if (m_replaceMode) { if (m_matchCounter == 0) { static_cast(m_view->document())->startEditing(); } // Replace lastRange = match.replace(m_replacement, false, ++m_matchCounter); } else { lastRange = match.range(); ++m_matchCounter; } // remember ranges if limit not reached if (m_matchCounter < maxHighlightings) { m_highlightRanges.push_back(lastRange); } else { m_highlightRanges.clear(); // TODO Info user that highlighting is disabled } // Continue after match if (lastRange.end() >= m_workingRange->end()) { done = true; break; } KTextEditor::DocumentCursor workingStart(m_view->doc(), lastRange.end()); if (originalMatchEmpty) { // Can happen for regex patterns like "^". // If we don't advance here we will loop forever... workingStart.move(1); } else if (regexMode && !multiLinePattern && workingStart.atEndOfLine()) { // single-line regexps might match the naked line end // therefore we better advance to the next line workingStart.move(1); } m_workingRange->setRange(workingStart.toCursor(), m_workingRange->end()); // Are we done? if (!m_workingRange->toRange().isValid() || workingStart.atEndOfDocument()) { done = true; break; } timeOut = rolex.elapsed() > 150; } while (!m_cancelFindOrReplace && !timeOut); } while (!m_cancelFindOrReplace && !timeOut && block && ++line <= m_inputRange.end().line()); if (done || m_cancelFindOrReplace) { emit findOrReplaceAllFinished(); } else if (timeOut) { QTimer::singleShot(0, this, &KateSearchBar::findOrReplaceAll); } showResultMessage(); } void KateSearchBar::endFindOrReplaceAll() { // Don't forget to remove our "crash protector" disconnect(m_view->doc(), &KTextEditor::Document::aboutToClose, this, &KateSearchBar::endFindOrReplaceAll); // After last match if (m_matchCounter > 0) { if (m_replaceMode) { static_cast(m_view->document())->finishEditing(); } } // Add ScrollBarMarks if (!m_highlightRanges.empty()) { KTextEditor::MarkInterface *iface = qobject_cast(m_view->document()); if (iface) { iface->setMarkDescription(KTextEditor::MarkInterface::SearchMatch, i18n("SearchHighLight")); iface->setMarkPixmap(KTextEditor::MarkInterface::SearchMatch, QIcon().pixmap(0, 0)); for (const Range &r : m_highlightRanges) { iface->addMark(r.start().line(), KTextEditor::MarkInterface::SearchMatch); } } } // Add highlights if (m_replaceMode) { for (const Range &r : qAsConst(m_highlightRanges)) { highlightReplacement(r); } // Never merge replace actions with other replace actions/user actions m_view->doc()->undoManager()->undoSafePoint(); } else { for (const Range &r : qAsConst(m_highlightRanges)) { highlightMatch(r); } // indicateMatch(m_matchCounter > 0 ? MatchFound : MatchMismatch); TODO } // Clean-Up the still hold MovingRange delete m_workingRange; // restore connection connect(m_view, &KTextEditor::View::selectionChanged, this, &KateSearchBar::updateSelectionOnly); if (m_powerUi) { // Offer Find and Replace buttons and enable again useful buttons m_powerUi->searchCancelStacked->setCurrentIndex(m_powerUi->searchCancelStacked->indexOf(m_powerUi->searchPage)); m_powerUi->findNext->setEnabled(true); m_powerUi->findPrev->setEnabled(true); m_powerUi->replaceNext->setEnabled(true); // Add to search history addCurrentTextToHistory(m_powerUi->pattern); // Add to replace history addCurrentTextToHistory(m_powerUi->replacement); } m_cancelFindOrReplace = true; // Indicate we are not running } void KateSearchBar::replaceAll() { // clear prior highlightings (deletes info message if present) clearHighlights(); // What to find/replace? const QString replacement = m_powerUi->replacement->currentText(); // Where to replace? const bool selected = m_view->selection(); Range inputRange = (selected && selectionOnly()) ? m_view->selectionRange() : m_view->document()->documentRange(); beginFindOrReplaceAll(inputRange, replacement); } void KateSearchBar::setSearchPattern(const QString &searchPattern) { if (searchPattern == this->searchPattern()) { return; } if (isPower()) { m_powerUi->pattern->setEditText(searchPattern); } else { m_incUi->pattern->setEditText(searchPattern); } } QString KateSearchBar::searchPattern() const { return (m_powerUi != nullptr) ? m_powerUi->pattern->currentText() : m_incUi->pattern->currentText(); } void KateSearchBar::setSelectionOnly(bool selectionOnly) { if (this->selectionOnly() == selectionOnly) { return; } if (isPower()) { m_powerUi->selectionOnly->setChecked(selectionOnly); } } bool KateSearchBar::selectionOnly() const { return isPower() ? m_powerUi->selectionOnly->isChecked() : false; } KTextEditor::SearchOptions KateSearchBar::searchOptions(SearchDirection searchDirection) const { SearchOptions enabledOptions = KTextEditor::Default; if (!matchCase()) { enabledOptions |= CaseInsensitive; } if (searchDirection == SearchBackward) { enabledOptions |= Backwards; } if (m_powerUi != nullptr) { switch (m_powerUi->searchMode->currentIndex()) { - case MODE_WHOLE_WORDS: - enabledOptions |= WholeWords; - break; + case MODE_WHOLE_WORDS: + enabledOptions |= WholeWords; + break; - case MODE_ESCAPE_SEQUENCES: - enabledOptions |= EscapeSequences; - break; + case MODE_ESCAPE_SEQUENCES: + enabledOptions |= EscapeSequences; + break; - case MODE_REGEX: - enabledOptions |= Regex; - break; + case MODE_REGEX: + enabledOptions |= Regex; + break; - case MODE_PLAIN_TEXT: // FALLTHROUGH - default: - break; + case MODE_PLAIN_TEXT: // FALLTHROUGH + default: + break; } } return enabledOptions; } struct ParInfo { int openIndex; bool capturing; int captureNumber; // 1..9 }; QVector KateSearchBar::getCapturePatterns(const QString &pattern) const { QVector capturePatterns; capturePatterns.reserve(9); QStack parInfos; const int inputLen = pattern.length(); int input = 0; // walker index bool insideClass = false; int captureCount = 0; while (input < inputLen) { if (insideClass) { // Wait for closing, unescaped ']' if (pattern[input].unicode() == L']') { insideClass = false; } input++; } else { switch (pattern[input].unicode()) { - case L'\\': - // Skip this and any next character - input += 2; - break; - - case L'(': - ParInfo curInfo; - curInfo.openIndex = input; - curInfo.capturing = (input + 1 >= inputLen) || (pattern[input + 1].unicode() != '?'); - if (curInfo.capturing) { - captureCount++; - } - curInfo.captureNumber = captureCount; - parInfos.push(curInfo); - - input++; - break; - - case L')': - if (!parInfos.empty()) { - ParInfo &top = parInfos.top(); - if (top.capturing && (top.captureNumber <= 9)) { - const int start = top.openIndex + 1; - const int len = input - start; - if (capturePatterns.size() < top.captureNumber) { - capturePatterns.resize(top.captureNumber); - } - capturePatterns[top.captureNumber - 1] = pattern.mid(start, len); + case L'\\': + // Skip this and any next character + input += 2; + break; + + case L'(': + ParInfo curInfo; + curInfo.openIndex = input; + curInfo.capturing = (input + 1 >= inputLen) || (pattern[input + 1].unicode() != '?'); + if (curInfo.capturing) { + captureCount++; + } + curInfo.captureNumber = captureCount; + parInfos.push(curInfo); + + input++; + break; + + case L')': + if (!parInfos.empty()) { + ParInfo &top = parInfos.top(); + if (top.capturing && (top.captureNumber <= 9)) { + const int start = top.openIndex + 1; + const int len = input - start; + if (capturePatterns.size() < top.captureNumber) { + capturePatterns.resize(top.captureNumber); } - parInfos.pop(); + capturePatterns[top.captureNumber - 1] = pattern.mid(start, len); } + parInfos.pop(); + } - input++; - break; + input++; + break; - case L'[': - input++; - insideClass = true; - break; + case L'[': + input++; + insideClass = true; + break; - default: - input++; - break; + default: + input++; + break; } } } return capturePatterns; } void KateSearchBar::showExtendedContextMenu(bool forPattern, const QPoint &pos) { // Make original menu QComboBox *comboBox = forPattern ? m_powerUi->pattern : m_powerUi->replacement; QMenu *const contextMenu = comboBox->lineEdit()->createStandardContextMenu(); if (contextMenu == nullptr) { return; } bool extendMenu = false; bool regexMode = false; switch (m_powerUi->searchMode->currentIndex()) { - case MODE_REGEX: - regexMode = true; - // FALLTHROUGH + case MODE_REGEX: + regexMode = true; + // FALLTHROUGH - case MODE_ESCAPE_SEQUENCES: - extendMenu = true; - break; + case MODE_ESCAPE_SEQUENCES: + extendMenu = true; + break; - default: - break; + default: + break; } AddMenuManager addMenuManager(contextMenu, 37); if (!extendMenu) { addMenuManager.enableMenu(extendMenu); } else { // Build menu if (forPattern) { if (regexMode) { addMenuManager.addEntry(QStringLiteral("^"), QString(), i18n("Beginning of line")); addMenuManager.addEntry(QStringLiteral("$"), QString(), i18n("End of line")); addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("."), QString(), i18n("Any single character (excluding line breaks)")); addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("+"), QString(), i18n("One or more occurrences")); addMenuManager.addEntry(QStringLiteral("*"), QString(), i18n("Zero or more occurrences")); addMenuManager.addEntry(QStringLiteral("?"), QString(), i18n("Zero or one occurrences")); addMenuManager.addEntry(QStringLiteral("{a"), QStringLiteral(",b}"), i18n(" through occurrences"), QStringLiteral("{"), QStringLiteral(",}")); addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing")); addMenuManager.addEntry(QStringLiteral("|"), QString(), i18n("Or")); addMenuManager.addEntry(QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters")); addMenuManager.addEntry(QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters")); addMenuManager.addSeparator(); } } else { addMenuManager.addEntry(QStringLiteral("\\0"), QString(), i18n("Whole match reference")); addMenuManager.addSeparator(); if (regexMode) { const QString pattern = m_powerUi->pattern->currentText(); const QVector capturePatterns = getCapturePatterns(pattern); const int captureCount = capturePatterns.count(); for (int i = 1; i <= 9; i++) { const QString number = QString::number(i); const QString &captureDetails = (i <= captureCount) ? QLatin1String(" = (") + capturePatterns[i - 1].leftRef(30) + QLatin1Char(')') : QString(); addMenuManager.addEntry(QLatin1String("\\") + number, QString(), i18n("Reference") + QLatin1Char(' ') + number + captureDetails); } addMenuManager.addSeparator(); } } addMenuManager.addEntry(QStringLiteral("\\n"), QString(), i18n("Line break")); addMenuManager.addEntry(QStringLiteral("\\t"), QString(), i18n("Tab")); if (forPattern && regexMode) { addMenuManager.addEntry(QStringLiteral("\\b"), QString(), i18n("Word boundary")); addMenuManager.addEntry(QStringLiteral("\\B"), QString(), i18n("Not word boundary")); addMenuManager.addEntry(QStringLiteral("\\d"), QString(), i18n("Digit")); addMenuManager.addEntry(QStringLiteral("\\D"), QString(), i18n("Non-digit")); addMenuManager.addEntry(QStringLiteral("\\s"), QString(), i18n("Whitespace (excluding line breaks)")); addMenuManager.addEntry(QStringLiteral("\\S"), QString(), i18n("Non-whitespace (excluding line breaks)")); addMenuManager.addEntry(QStringLiteral("\\w"), QString(), i18n("Word character (alphanumerics plus '_')")); addMenuManager.addEntry(QStringLiteral("\\W"), QString(), i18n("Non-word character")); } addMenuManager.addEntry(QStringLiteral("\\0???"), QString(), i18n("Octal character 000 to 377 (2^8-1)"), QStringLiteral("\\0")); addMenuManager.addEntry(QStringLiteral("\\x????"), QString(), i18n("Hex character 0000 to FFFF (2^16-1)"), QStringLiteral("\\x")); addMenuManager.addEntry(QStringLiteral("\\\\"), QString(), i18n("Backslash")); if (forPattern && regexMode) { addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("(?:E"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:")); addMenuManager.addEntry(QStringLiteral("(?=E"), QStringLiteral(")"), i18n("Lookahead"), QStringLiteral("(?=")); addMenuManager.addEntry(QStringLiteral("(?!E"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!")); } if (!forPattern) { addMenuManager.addSeparator(); addMenuManager.addEntry(QStringLiteral("\\L"), QString(), i18n("Begin lowercase conversion")); addMenuManager.addEntry(QStringLiteral("\\U"), QString(), i18n("Begin uppercase conversion")); addMenuManager.addEntry(QStringLiteral("\\E"), QString(), i18n("End case conversion")); addMenuManager.addEntry(QStringLiteral("\\l"), QString(), i18n("Lowercase first character conversion")); addMenuManager.addEntry(QStringLiteral("\\u"), QString(), i18n("Uppercase first character conversion")); addMenuManager.addEntry(QStringLiteral("\\#[#..]"), QString(), i18n("Replacement counter (for Replace All)"), QStringLiteral("\\#")); } } // Show menu QAction *const result = contextMenu->exec(comboBox->mapToGlobal(pos)); if (result != nullptr) { addMenuManager.handle(result, comboBox->lineEdit()); } } void KateSearchBar::onPowerModeChanged(int /*index*/) { if (m_powerUi->searchMode->currentIndex() == MODE_REGEX) { m_powerUi->matchCase->setChecked(true); } sendConfig(); indicateMatch(MatchNothing); givePatternFeedback(); } void KateSearchBar::nextMatchForSelection(KTextEditor::ViewPrivate *view, SearchDirection searchDirection) { const bool selected = view->selection(); if (selected) { const QString pattern = view->selectionText(); // How to find? SearchOptions enabledOptions(KTextEditor::Default); if (searchDirection == SearchBackward) { enabledOptions |= Backwards; } // Where to find? const Range selRange = view->selectionRange(); Range inputRange; if (searchDirection == SearchForward) { inputRange.setRange(selRange.end(), view->doc()->documentEnd()); } else { inputRange.setRange(Cursor(0, 0), selRange.start()); } // Find, first try KateMatch match(view->doc(), enabledOptions); match.searchText(inputRange, pattern); if (match.isValid()) { selectRange(view, match.range()); } else { // Find, second try showSearchWrappedHint(searchDirection); if (searchDirection == SearchForward) { inputRange.setRange(Cursor(0, 0), selRange.start()); } else { inputRange.setRange(selRange.end(), view->doc()->documentEnd()); } KateMatch match2(view->doc(), enabledOptions); match2.searchText(inputRange, pattern); if (match2.isValid()) { selectRange(view, match2.range()); } } } else { // Select current word so we can search for that the next time const Cursor cursorPos = view->cursorPosition(); KTextEditor::Range wordRange = view->document()->wordRangeAt(cursorPos); if (wordRange.isValid()) { selectRange(view, wordRange); } } } void KateSearchBar::enterPowerMode() { QString initialPattern; bool selectionOnly = false; // Guess settings from context: init pattern with current selection const bool selected = m_view->selection(); if (selected) { const Range &selection = m_view->selectionRange(); if (selection.onSingleLine()) { // ... with current selection initialPattern = m_view->selectionText(); } else { // Enable selection only selectionOnly = true; } } // If there's no new selection, we'll use the existing pattern if (initialPattern.isNull()) { // Coming from power search? const bool fromReplace = (m_powerUi != nullptr) && (m_widget->isVisible()); if (fromReplace) { QLineEdit *const patternLineEdit = m_powerUi->pattern->lineEdit(); Q_ASSERT(patternLineEdit != nullptr); patternLineEdit->selectAll(); m_powerUi->pattern->setFocus(Qt::MouseFocusReason); return; } // Coming from incremental search? const bool fromIncremental = (m_incUi != nullptr) && (m_widget->isVisible()); if (fromIncremental) { initialPattern = m_incUi->pattern->currentText(); } } // Create dialog const bool create = (m_powerUi == nullptr); if (create) { // Kill incremental widget if (m_incUi != nullptr) { // Backup current settings const bool OF_INCREMENTAL = false; backupConfig(OF_INCREMENTAL); // Kill widget delete m_incUi; m_incUi = nullptr; m_layout->removeWidget(m_widget); m_widget->deleteLater(); // I didn't get a crash here but for symmetrie to the other mutate slot^ } // Add power widget m_widget = new QWidget(this); m_powerUi = new Ui::PowerSearchBar; m_powerUi->setupUi(m_widget); m_layout->addWidget(m_widget); // Bind to shared history models m_powerUi->pattern->setDuplicatesEnabled(false); m_powerUi->pattern->setInsertPolicy(QComboBox::InsertAtTop); m_powerUi->pattern->setMaxCount(m_config->maxHistorySize()); m_powerUi->pattern->setModel(KTextEditor::EditorPrivate::self()->searchHistoryModel()); m_powerUi->pattern->lineEdit()->setClearButtonEnabled(true); m_powerUi->pattern->setCompleter(nullptr); m_powerUi->replacement->setDuplicatesEnabled(false); m_powerUi->replacement->setInsertPolicy(QComboBox::InsertAtTop); m_powerUi->replacement->setMaxCount(m_config->maxHistorySize()); m_powerUi->replacement->setModel(KTextEditor::EditorPrivate::self()->replaceHistoryModel()); m_powerUi->replacement->lineEdit()->setClearButtonEnabled(true); m_powerUi->replacement->setCompleter(nullptr); // Icons // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing. QIcon mutateIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system"))); QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold"))); m_powerUi->mutate->setIcon(mutateIcon); m_powerUi->mutate->setChecked(true); m_powerUi->findNext->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); m_powerUi->findPrev->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search"))); m_powerUi->findAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); m_powerUi->matchCase->setIcon(matchCaseIcon); m_powerUi->selectionOnly->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all"))); // Focus proxy centralWidget()->setFocusProxy(m_powerUi->pattern); } m_powerUi->selectionOnly->setChecked(selectionOnly); // Restore previous settings if (create) { m_powerUi->matchCase->setChecked(m_powerMatchCase); m_powerUi->searchMode->setCurrentIndex(m_powerMode); } // force current index of -1 --> shows 1st completion entry instead of 2nd m_powerUi->pattern->setCurrentIndex(-1); m_powerUi->replacement->setCurrentIndex(-1); // Set initial search pattern QLineEdit *const patternLineEdit = m_powerUi->pattern->lineEdit(); Q_ASSERT(patternLineEdit != nullptr); patternLineEdit->setText(initialPattern); patternLineEdit->selectAll(); // Set initial replacement text QLineEdit *const replacementLineEdit = m_powerUi->replacement->lineEdit(); Q_ASSERT(replacementLineEdit != nullptr); replacementLineEdit->setText(QString()); // Propagate settings (slots are still inactive on purpose) onPowerPatternChanged(initialPattern); givePatternFeedback(); if (create) { // Slots connect(m_powerUi->mutate, SIGNAL(clicked()), this, SLOT(enterIncrementalMode())); connect(patternLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onPowerPatternChanged(QString))); connect(m_powerUi->findNext, SIGNAL(clicked()), this, SLOT(findNext())); connect(m_powerUi->findPrev, SIGNAL(clicked()), this, SLOT(findPrevious())); connect(m_powerUi->replaceNext, SIGNAL(clicked()), this, SLOT(replaceNext())); connect(m_powerUi->replaceAll, SIGNAL(clicked()), this, SLOT(replaceAll())); connect(m_powerUi->searchMode, SIGNAL(currentIndexChanged(int)), this, SLOT(onPowerModeChanged(int))); connect(m_powerUi->matchCase, SIGNAL(toggled(bool)), this, SLOT(onMatchCaseToggled(bool))); connect(m_powerUi->findAll, SIGNAL(clicked()), this, SLOT(findAll())); connect(m_powerUi->cancel, &QPushButton::clicked, this, &KateSearchBar::onPowerCancelFindOrReplace); // Make [return] in pattern line edit trigger action connect(patternLineEdit, SIGNAL(returnPressed()), this, SLOT(onReturnPressed())); connect(replacementLineEdit, SIGNAL(returnPressed()), this, SLOT(replaceNext())); // Hook into line edit context menus m_powerUi->pattern->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_powerUi->pattern, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onPowerPatternContextMenuRequest(QPoint))); m_powerUi->replacement->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_powerUi->replacement, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onPowerReplacmentContextMenuRequest(QPoint))); } // Focus if (m_widget->isVisible()) { m_powerUi->pattern->setFocus(Qt::MouseFocusReason); } } void KateSearchBar::enterIncrementalMode() { QString initialPattern; // Guess settings from context: init pattern with current selection const bool selected = m_view->selection(); if (selected) { const Range &selection = m_view->selectionRange(); if (selection.onSingleLine()) { // ... with current selection initialPattern = m_view->selectionText(); } } // If there's no new selection, we'll use the existing pattern if (initialPattern.isNull()) { // Coming from incremental search? const bool fromIncremental = (m_incUi != nullptr) && (m_widget->isVisible()); if (fromIncremental) { m_incUi->pattern->lineEdit()->selectAll(); m_incUi->pattern->setFocus(Qt::MouseFocusReason); return; } // Coming from power search? const bool fromReplace = (m_powerUi != nullptr) && (m_widget->isVisible()); if (fromReplace) { initialPattern = m_powerUi->pattern->currentText(); } } // Still no search pattern? Use the word under the cursor if (initialPattern.isNull()) { const KTextEditor::Cursor cursorPosition = m_view->cursorPosition(); initialPattern = m_view->doc()->wordAt(cursorPosition); } // Create dialog const bool create = (m_incUi == nullptr); if (create) { // Kill power widget if (m_powerUi != nullptr) { // Backup current settings const bool OF_POWER = true; backupConfig(OF_POWER); // Kill widget delete m_powerUi; m_powerUi = nullptr; m_layout->removeWidget(m_widget); m_widget->deleteLater(); // deleteLater, because it's not a good idea too delete the widget and there for the button triggering this slot } // Add incremental widget m_widget = new QWidget(this); m_incUi = new Ui::IncrementalSearchBar; m_incUi->setupUi(m_widget); m_layout->addWidget(m_widget); // new QShortcut(KStandardShortcut::paste().primary(), m_incUi->pattern, SLOT(paste()), 0, Qt::WidgetWithChildrenShortcut); // if (!KStandardShortcut::paste().alternate().isEmpty()) // new QShortcut(KStandardShortcut::paste().alternate(), m_incUi->pattern, SLOT(paste()), 0, Qt::WidgetWithChildrenShortcut); // Icons // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing. QIcon mutateIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system"))); QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold"))); m_incUi->mutate->setIcon(mutateIcon); m_incUi->next->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); m_incUi->prev->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search"))); m_incUi->matchCase->setIcon(matchCaseIcon); // Ensure minimum size m_incUi->pattern->setMinimumWidth(12 * m_incUi->pattern->fontMetrics().height()); // Customize status area m_incUi->status->setTextElideMode(Qt::ElideLeft); // Focus proxy centralWidget()->setFocusProxy(m_incUi->pattern); m_incUi->pattern->setDuplicatesEnabled(false); m_incUi->pattern->setInsertPolicy(QComboBox::InsertAtTop); m_incUi->pattern->setMaxCount(m_config->maxHistorySize()); m_incUi->pattern->setModel(KTextEditor::EditorPrivate::self()->searchHistoryModel()); m_incUi->pattern->lineEdit()->setClearButtonEnabled(true); m_incUi->pattern->setCompleter(nullptr); } // Restore previous settings if (create) { m_incUi->matchCase->setChecked(m_incMatchCase); } // force current index of -1 --> shows 1st completion entry instead of 2nd m_incUi->pattern->setCurrentIndex(-1); // Set initial search pattern if (!create) { disconnect(m_incUi->pattern, SIGNAL(editTextChanged(QString)), this, SLOT(onIncPatternChanged(QString))); } m_incUi->pattern->setEditText(initialPattern); connect(m_incUi->pattern, SIGNAL(editTextChanged(QString)), this, SLOT(onIncPatternChanged(QString))); m_incUi->pattern->lineEdit()->selectAll(); // Propagate settings (slots are still inactive on purpose) if (initialPattern.isEmpty()) { // Reset edit color indicateMatch(MatchNothing); } // Enable/disable next/prev m_incUi->next->setDisabled(initialPattern.isEmpty()); m_incUi->prev->setDisabled(initialPattern.isEmpty()); if (create) { // Slots connect(m_incUi->mutate, SIGNAL(clicked()), this, SLOT(enterPowerMode())); connect(m_incUi->pattern->lineEdit(), SIGNAL(returnPressed()), this, SLOT(onReturnPressed())); connect(m_incUi->next, SIGNAL(clicked()), this, SLOT(findNext())); connect(m_incUi->prev, SIGNAL(clicked()), this, SLOT(findPrevious())); connect(m_incUi->matchCase, SIGNAL(toggled(bool)), this, SLOT(onMatchCaseToggled(bool))); } // Focus if (m_widget->isVisible()) { m_incUi->pattern->setFocus(Qt::MouseFocusReason); } } bool KateSearchBar::clearHighlights() { // Remove ScrollBarMarks KTextEditor::MarkInterface *iface = qobject_cast(m_view->document()); if (iface) { const QHash marks = iface->marks(); QHashIterator i(marks); while (i.hasNext()) { i.next(); if (i.value()->type & KTextEditor::MarkInterface::SearchMatch) { iface->removeMark(i.value()->line, KTextEditor::MarkInterface::SearchMatch); } } } if (m_infoMessage) { delete m_infoMessage; } if (m_hlRanges.isEmpty()) { return false; } qDeleteAll(m_hlRanges); m_hlRanges.clear(); return true; } void KateSearchBar::updateHighlightColors() { const QColor foregroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor &searchColor = m_view->renderer()->config()->searchHighlightColor(); const QColor &replaceColor = m_view->renderer()->config()->replaceHighlightColor(); // init match attribute highlightMatchAttribute->setForeground(foregroundColor); highlightMatchAttribute->setBackground(searchColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateMouseIn)->setBackground(searchColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateMouseIn)->setForeground(foregroundColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateCaretIn)->setBackground(searchColor); highlightMatchAttribute->dynamicAttribute(Attribute::ActivateCaretIn)->setForeground(foregroundColor); // init replacement attribute highlightReplacementAttribute->setBackground(replaceColor); highlightReplacementAttribute->setForeground(foregroundColor); } void KateSearchBar::showEvent(QShowEvent *event) { // Update init cursor if (m_incUi != nullptr) { m_incInitCursor = m_view->cursorPosition(); } updateSelectionOnly(); KateViewBarWidget::showEvent(event); } void KateSearchBar::updateSelectionOnly() { if (m_powerUi == nullptr) { return; } // Re-init "Selection only" checkbox if power search bar open const bool selected = m_view->selection(); bool selectionOnly = selected; if (selected) { Range const &selection = m_view->selectionRange(); selectionOnly = !selection.onSingleLine(); } m_powerUi->selectionOnly->setChecked(selectionOnly); } void KateSearchBar::updateIncInitCursor() { if (m_incUi == nullptr) { return; } // Update init cursor m_incInitCursor = m_view->cursorPosition(); } void KateSearchBar::onPowerPatternContextMenuRequest(const QPoint &pos) { const bool FOR_PATTERN = true; showExtendedContextMenu(FOR_PATTERN, pos); } void KateSearchBar::onPowerPatternContextMenuRequest() { onPowerPatternContextMenuRequest(m_powerUi->pattern->mapFromGlobal(QCursor::pos())); } void KateSearchBar::onPowerReplacmentContextMenuRequest(const QPoint &pos) { const bool FOR_REPLACEMENT = false; showExtendedContextMenu(FOR_REPLACEMENT, pos); } void KateSearchBar::onPowerReplacmentContextMenuRequest() { onPowerReplacmentContextMenuRequest(m_powerUi->replacement->mapFromGlobal(QCursor::pos())); } void KateSearchBar::onPowerCancelFindOrReplace() { m_cancelFindOrReplace = true; } bool KateSearchBar::isPower() const { return m_powerUi != nullptr; } void KateSearchBar::slotReadWriteChanged() { if (!KateSearchBar::isPower()) { return; } // perhaps disable/enable m_powerUi->replaceNext->setEnabled(m_view->doc()->isReadWrite() && isPatternValid()); m_powerUi->replaceAll->setEnabled(m_view->doc()->isReadWrite() && isPatternValid()); } diff --git a/src/swapfile/kateswapfile.cpp b/src/swapfile/kateswapfile.cpp index 5c48990f..ef797c41 100644 --- a/src/swapfile/kateswapfile.cpp +++ b/src/swapfile/kateswapfile.cpp @@ -1,662 +1,662 @@ /* This file is part of the Kate project. * * Copyright (C) 2010-2018 Dominik Haumann * Copyright (C) 2010 Diana-Victoria Tiriplica * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "kateconfig.h" #include "katepartdebug.h" #include "kateswapdiffcreator.h" #include "kateswapfile.h" #include "kateundomanager.h" #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #endif // swap file version header const static char swapFileVersionString[] = "Kate Swap File 2.0"; // tokens for swap files const static qint8 EA_StartEditing = 'S'; const static qint8 EA_FinishEditing = 'E'; const static qint8 EA_WrapLine = 'W'; const static qint8 EA_UnwrapLine = 'U'; const static qint8 EA_InsertText = 'I'; const static qint8 EA_RemoveText = 'R'; namespace Kate { QTimer *SwapFile::s_timer = nullptr; SwapFile::SwapFile(KTextEditor::DocumentPrivate *document) : QObject(document) , m_document(document) , m_trackingEnabled(false) , m_recovered(false) , m_needSync(false) { // fixed version of serialisation m_stream.setVersion(QDataStream::Qt_4_6); // connect the timer connect(syncTimer(), SIGNAL(timeout()), this, SLOT(writeFileToDisk()), Qt::DirectConnection); // connecting the signals connect(&m_document->buffer(), SIGNAL(saved(QString)), this, SLOT(fileSaved(QString))); connect(&m_document->buffer(), SIGNAL(loaded(QString, bool)), this, SLOT(fileLoaded(QString))); connect(m_document, SIGNAL(configChanged()), this, SLOT(configChanged())); // tracking on! setTrackingEnabled(true); } SwapFile::~SwapFile() { // only remove swap file after data recovery (bug #304576) if (!shouldRecover()) { removeSwapFile(); } } void SwapFile::configChanged() { } void SwapFile::setTrackingEnabled(bool enable) { if (m_trackingEnabled == enable) { return; } m_trackingEnabled = enable; TextBuffer &buffer = m_document->buffer(); if (m_trackingEnabled) { connect(&buffer, SIGNAL(editingStarted()), this, SLOT(startEditing())); connect(&buffer, SIGNAL(editingFinished()), this, SLOT(finishEditing())); connect(m_document, SIGNAL(modifiedChanged(KTextEditor::Document *)), this, SLOT(modifiedChanged())); connect(&buffer, SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor))); connect(&buffer, SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int))); connect(&buffer, SIGNAL(textInserted(KTextEditor::Cursor, QString)), this, SLOT(insertText(KTextEditor::Cursor, QString))); connect(&buffer, SIGNAL(textRemoved(KTextEditor::Range, QString)), this, SLOT(removeText(KTextEditor::Range))); } else { disconnect(&buffer, SIGNAL(editingStarted()), this, SLOT(startEditing())); disconnect(&buffer, SIGNAL(editingFinished()), this, SLOT(finishEditing())); disconnect(m_document, SIGNAL(modifiedChanged(KTextEditor::Document *)), this, SLOT(modifiedChanged())); disconnect(&buffer, SIGNAL(lineWrapped(KTextEditor::Cursor)), this, SLOT(wrapLine(KTextEditor::Cursor))); disconnect(&buffer, SIGNAL(lineUnwrapped(int)), this, SLOT(unwrapLine(int))); disconnect(&buffer, SIGNAL(textInserted(KTextEditor::Cursor, QString)), this, SLOT(insertText(KTextEditor::Cursor, QString))); disconnect(&buffer, SIGNAL(textRemoved(KTextEditor::Range, QString)), this, SLOT(removeText(KTextEditor::Range))); } } void SwapFile::fileClosed() { // remove old swap file, file is now closed if (!shouldRecover()) { removeSwapFile(); } else { m_document->setReadWrite(true); } // purge filename updateFileName(); } KTextEditor::DocumentPrivate *SwapFile::document() { return m_document; } bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const { // read and check header QByteArray header; stream >> header; if (header != swapFileVersionString) { qCWarning(LOG_KTE) << "Can't open swap file, wrong version"; return false; } // read checksum QByteArray checksum; stream >> checksum; // qCDebug(LOG_KTE) << "DIGEST:" << checksum << m_document->checksum(); if (checkDigest && checksum != m_document->checksum()) { qCWarning(LOG_KTE) << "Can't recover from swap file, checksum of document has changed"; return false; } return true; } void SwapFile::fileLoaded(const QString &) { // look for swap file if (!updateFileName()) { return; } if (!m_swapfile.exists()) { // qCDebug(LOG_KTE) << "No swap file"; return; } if (!QFileInfo(m_swapfile).isReadable()) { qCWarning(LOG_KTE) << "Can't open swap file (missing permissions)"; return; } // sanity check QFile peekFile(fileName()); if (peekFile.open(QIODevice::ReadOnly)) { QDataStream stream(&peekFile); if (!isValidSwapFile(stream, true)) { removeSwapFile(); return; } peekFile.close(); } else { qCWarning(LOG_KTE) << "Can't open swap file:" << fileName(); return; } // show swap file message m_document->setReadWrite(false); showSwapFileMessage(); } void SwapFile::modifiedChanged() { if (!m_document->isModified() && !shouldRecover()) { m_needSync = false; // the file is not modified and we are not in recover mode removeSwapFile(); } } void SwapFile::recover() { m_document->setReadWrite(true); // if isOpen() returns true, the swap file likely changed already (appended data) // Example: The document was falsely marked as writable and the user changed // text even though the recover bar was visible. In this case, a replay of // the swap file across wrong document content would happen -> certainly wrong if (m_swapfile.isOpen()) { qCWarning(LOG_KTE) << "Attempt to recover an already modified document. Aborting"; removeSwapFile(); return; } // if the file doesn't exist, abort (user might have deleted it, or use two editor instances) if (!m_swapfile.open(QIODevice::ReadOnly)) { qCWarning(LOG_KTE) << "Can't open swap file"; return; } // remember that the file has recovered m_recovered = true; // open data stream m_stream.setDevice(&m_swapfile); // replay the swap file bool success = recover(m_stream); // close swap file m_stream.setDevice(nullptr); m_swapfile.close(); if (!success) { removeSwapFile(); } // recover can also be called through the KTE::RecoveryInterface. // Make sure, the message is hidden in this case as well. if (m_swapMessage) { m_swapMessage->deleteLater(); } } bool SwapFile::recover(QDataStream &stream, bool checkDigest) { if (!isValidSwapFile(stream, checkDigest)) { return false; } // disconnect current signals setTrackingEnabled(false); // needed to set undo/redo cursors in a sane way bool firstEditInGroup = false; KTextEditor::Cursor undoCursor = KTextEditor::Cursor::invalid(); KTextEditor::Cursor redoCursor = KTextEditor::Cursor::invalid(); // replay swapfile bool editRunning = false; bool brokenSwapFile = false; while (!stream.atEnd()) { if (brokenSwapFile) { break; } qint8 type; stream >> type; switch (type) { - case EA_StartEditing: { - m_document->editStart(); - editRunning = true; - firstEditInGroup = true; - undoCursor = KTextEditor::Cursor::invalid(); - redoCursor = KTextEditor::Cursor::invalid(); - break; + case EA_StartEditing: { + m_document->editStart(); + editRunning = true; + firstEditInGroup = true; + undoCursor = KTextEditor::Cursor::invalid(); + redoCursor = KTextEditor::Cursor::invalid(); + break; + } + case EA_FinishEditing: { + m_document->editEnd(); + + // empty editStart() / editEnd() groups exist: only set cursor if required + if (!firstEditInGroup) { + // set undo/redo cursor of last KateUndoGroup of the undo manager + m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor); + m_document->undoManager()->undoSafePoint(); } - case EA_FinishEditing: { - m_document->editEnd(); - - // empty editStart() / editEnd() groups exist: only set cursor if required - if (!firstEditInGroup) { - // set undo/redo cursor of last KateUndoGroup of the undo manager - m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor); - m_document->undoManager()->undoSafePoint(); - } - firstEditInGroup = false; - editRunning = false; + firstEditInGroup = false; + editRunning = false; + break; + } + case EA_WrapLine: { + if (!editRunning) { + brokenSwapFile = true; break; } - case EA_WrapLine: { - if (!editRunning) { - brokenSwapFile = true; - break; - } - int line = 0, column = 0; - stream >> line >> column; + int line = 0, column = 0; + stream >> line >> column; - // emulate buffer unwrapLine with document - m_document->editWrapLine(line, column, true); + // emulate buffer unwrapLine with document + m_document->editWrapLine(line, column, true); - // track undo/redo cursor - if (firstEditInGroup) { - firstEditInGroup = false; - undoCursor = KTextEditor::Cursor(line, column); - } - redoCursor = KTextEditor::Cursor(line + 1, 0); + // track undo/redo cursor + if (firstEditInGroup) { + firstEditInGroup = false; + undoCursor = KTextEditor::Cursor(line, column); + } + redoCursor = KTextEditor::Cursor(line + 1, 0); + break; + } + case EA_UnwrapLine: { + if (!editRunning) { + brokenSwapFile = true; break; } - case EA_UnwrapLine: { - if (!editRunning) { - brokenSwapFile = true; - break; - } - int line = 0; - stream >> line; + int line = 0; + stream >> line; - // assert valid line - Q_ASSERT(line > 0); + // assert valid line + Q_ASSERT(line > 0); - const int undoColumn = m_document->lineLength(line - 1); + const int undoColumn = m_document->lineLength(line - 1); - // emulate buffer unwrapLine with document - m_document->editUnWrapLine(line - 1, true, 0); + // emulate buffer unwrapLine with document + m_document->editUnWrapLine(line - 1, true, 0); - // track undo/redo cursor - if (firstEditInGroup) { - firstEditInGroup = false; - undoCursor = KTextEditor::Cursor(line, 0); - } - redoCursor = KTextEditor::Cursor(line - 1, undoColumn); + // track undo/redo cursor + if (firstEditInGroup) { + firstEditInGroup = false; + undoCursor = KTextEditor::Cursor(line, 0); + } + redoCursor = KTextEditor::Cursor(line - 1, undoColumn); + break; + } + case EA_InsertText: { + if (!editRunning) { + brokenSwapFile = true; break; } - case EA_InsertText: { - if (!editRunning) { - brokenSwapFile = true; - break; - } - - int line, column; - QByteArray text; - stream >> line >> column >> text; - m_document->insertText(KTextEditor::Cursor(line, column), QString::fromUtf8(text.data(), text.size())); - - // track undo/redo cursor - if (firstEditInGroup) { - firstEditInGroup = false; - undoCursor = KTextEditor::Cursor(line, column); - } - redoCursor = KTextEditor::Cursor(line, column + text.size()); - break; + int line, column; + QByteArray text; + stream >> line >> column >> text; + m_document->insertText(KTextEditor::Cursor(line, column), QString::fromUtf8(text.data(), text.size())); + + // track undo/redo cursor + if (firstEditInGroup) { + firstEditInGroup = false; + undoCursor = KTextEditor::Cursor(line, column); } - case EA_RemoveText: { - if (!editRunning) { - brokenSwapFile = true; - break; - } - - int line, startColumn, endColumn; - stream >> line >> startColumn >> endColumn; - m_document->removeText(KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn))); - - // track undo/redo cursor - if (firstEditInGroup) { - firstEditInGroup = false; - undoCursor = KTextEditor::Cursor(line, endColumn); - } - redoCursor = KTextEditor::Cursor(line, startColumn); + redoCursor = KTextEditor::Cursor(line, column + text.size()); + break; + } + case EA_RemoveText: { + if (!editRunning) { + brokenSwapFile = true; break; } - default: { - qCWarning(LOG_KTE) << "Unknown type:" << type; + + int line, startColumn, endColumn; + stream >> line >> startColumn >> endColumn; + m_document->removeText(KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn))); + + // track undo/redo cursor + if (firstEditInGroup) { + firstEditInGroup = false; + undoCursor = KTextEditor::Cursor(line, endColumn); } + redoCursor = KTextEditor::Cursor(line, startColumn); + + break; + } + default: { + qCWarning(LOG_KTE) << "Unknown type:" << type; + } } } // balanced editStart and editEnd? if (editRunning) { brokenSwapFile = true; m_document->editEnd(); } // warn the user if the swap file is not complete if (brokenSwapFile) { qCWarning(LOG_KTE) << "Some data might be lost"; } else { // set sane final cursor, if possible KTextEditor::View *view = m_document->activeView(); redoCursor = m_document->undoManager()->lastRedoCursor(); if (view && redoCursor.isValid()) { view->setCursorPosition(redoCursor); } } // reconnect the signals setTrackingEnabled(true); return true; } void SwapFile::fileSaved(const QString &) { m_needSync = false; // remove old swap file (e.g. if a file A was "saved as" B) removeSwapFile(); // set the name for the new swap file updateFileName(); } void SwapFile::startEditing() { // no swap file, no work if (m_swapfile.fileName().isEmpty()) { return; } // if swap file doesn't exists, open it in WriteOnly mode // if it does, append the data to the existing swap file, // in case you recover and start editing again if (!m_swapfile.exists()) { // create path if not there if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory && !QDir(KateDocumentConfig::global()->swapDirectory()).exists()) { QDir().mkpath(KateDocumentConfig::global()->swapDirectory()); } m_swapfile.open(QIODevice::WriteOnly); m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); m_stream.setDevice(&m_swapfile); // write file header m_stream << QByteArray(swapFileVersionString); // write checksum m_stream << m_document->checksum(); } else if (m_stream.device() == nullptr) { m_swapfile.open(QIODevice::Append); m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); m_stream.setDevice(&m_swapfile); } // format: qint8 m_stream << EA_StartEditing; } void SwapFile::finishEditing() { // skip if not open if (!m_swapfile.isOpen()) { return; } // write the file to the disk every 15 seconds (default) // skip this if we disabled that if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) { // important: we store the interval as seconds, start wants milliseconds! syncTimer()->start(m_document->config()->swapSyncInterval() * 1000); } // format: qint8 m_stream << EA_FinishEditing; m_swapfile.flush(); } void SwapFile::wrapLine(const KTextEditor::Cursor &position) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int, int m_stream << EA_WrapLine << position.line() << position.column(); m_needSync = true; } void SwapFile::unwrapLine(int line) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int m_stream << EA_UnwrapLine << line; m_needSync = true; } void SwapFile::insertText(const KTextEditor::Cursor &position, const QString &text) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int, int, bytearray m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8(); m_needSync = true; } void SwapFile::removeText(const KTextEditor::Range &range) { // skip if not open if (!m_swapfile.isOpen()) { return; } // format: qint8, int, int, int Q_ASSERT(range.start().line() == range.end().line()); m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column(); m_needSync = true; } bool SwapFile::shouldRecover() const { // should not recover if the file has already recovered in another view if (m_recovered) { return false; } return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr; } void SwapFile::discard() { m_document->setReadWrite(true); removeSwapFile(); // discard can also be called through the KTE::RecoveryInterface. // Make sure, the message is hidden in this case as well. if (m_swapMessage) { m_swapMessage->deleteLater(); } } void SwapFile::removeSwapFile() { if (!m_swapfile.fileName().isEmpty() && m_swapfile.exists()) { m_stream.setDevice(nullptr); m_swapfile.close(); m_swapfile.remove(); } } bool SwapFile::updateFileName() { // first clear filename m_swapfile.setFileName(QString()); // get the new path QString path = fileName(); if (path.isNull()) { return false; } m_swapfile.setFileName(path); return true; } QString SwapFile::fileName() { const QUrl &url = m_document->url(); if (url.isEmpty() || !url.isLocalFile()) { return QString(); } const QString fullLocalPath(url.toLocalFile()); QString path; if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) { path = KateDocumentConfig::global()->swapDirectory(); path.append(QLatin1Char('/')); // append the sha1 sum of the full path + filename, to avoid "too long" paths created path.append(QString::fromLatin1(QCryptographicHash::hash(fullLocalPath.toUtf8(), QCryptographicHash::Sha1).toHex())); path.append(QLatin1Char('-')); path.append(QFileInfo(fullLocalPath).fileName()); path.append(QLatin1String(".kate-swp")); } else { path = fullLocalPath; int poz = path.lastIndexOf(QLatin1Char('/')); path.insert(poz + 1, QLatin1Char('.')); path.append(QLatin1String(".kate-swp")); } return path; } QTimer *SwapFile::syncTimer() { if (s_timer == nullptr) { s_timer = new QTimer(QApplication::instance()); s_timer->setSingleShot(true); } return s_timer; } void SwapFile::writeFileToDisk() { if (m_needSync) { m_needSync = false; #ifndef Q_OS_WIN // ensure that the file is written to disk #if HAVE_FDATASYNC fdatasync(m_swapfile.handle()); #else fsync(m_swapfile.handle()); #endif #endif } } void SwapFile::showSwapFileMessage() { m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly."), KTextEditor::Message::Warning); m_swapMessage->setWordWrap(true); QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split")), i18n("View Changes"), nullptr); QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo")), i18n("Recover Data"), nullptr); QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard"), nullptr); m_swapMessage->addAction(diffAction, false); m_swapMessage->addAction(recoverAction); m_swapMessage->addAction(discardAction); connect(diffAction, SIGNAL(triggered()), SLOT(showDiff())); connect(recoverAction, SIGNAL(triggered()), SLOT(recover()), Qt::QueuedConnection); connect(discardAction, SIGNAL(triggered()), SLOT(discard()), Qt::QueuedConnection); m_document->postMessage(m_swapMessage); } void SwapFile::showDiff() { // the diff creator deletes itself through deleteLater() when it's done SwapDiffCreator *diffCreator = new SwapDiffCreator(this); diffCreator->viewDiff(); } } diff --git a/src/utils/kateconfig.cpp b/src/utils/kateconfig.cpp index 04fc1432..fc3d725a 100644 --- a/src/utils/kateconfig.cpp +++ b/src/utils/kateconfig.cpp @@ -1,1448 +1,1448 @@ /* This file is part of the KDE libraries Copyright (C) 2007, 2008 Matthew Woehlke Copyright (C) 2003 Christoph Cullmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateconfig.h" #include "katedefaultcolors.h" #include "katedocument.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katerenderer.h" #include "kateschema.h" #include "kateview.h" #include #include #include #include #include #include // BEGIN KateConfig KateConfig::KateConfig(const KateConfig *parent) : m_parent(parent) , m_configKeys(m_parent ? nullptr : new QStringList()) , m_configKeyToEntry(m_parent ? nullptr : new QHash()) { } KateConfig::~KateConfig() { } void KateConfig::addConfigEntry(ConfigEntry &&entry) { /** * shall only be called for toplevel config */ Q_ASSERT(isGlobal()); /** * there shall be no gaps in the entries * we might later want to use a vector */ Q_ASSERT(m_configEntries.size() == static_cast(entry.enumKey)); /** * add new element */ m_configEntries.emplace(entry.enumKey, entry); } void KateConfig::finalizeConfigEntries() { /** * shall only be called for toplevel config */ Q_ASSERT(isGlobal()); /** * compute list of all config keys + register map from key => config entry * * we skip entries without a command name, these config entries are not exposed ATM * */ for (const auto &entry : m_configEntries) { if (!entry.second.commandName.isEmpty()) { Q_ASSERT_X(!m_configKeys->contains(entry.second.commandName), "finalizeConfigEntries", (QLatin1String("KEY NOT UNIQUE: ") + entry.second.commandName).toLocal8Bit().constData()); m_configKeys->append(entry.second.commandName); m_configKeyToEntry->insert(entry.second.commandName, &entry.second); } } } void KateConfig::readConfigEntries(const KConfigGroup &config) { configStart(); // read all config entries, even the ones ATM not set in this config object but known in the toplevel one for (const auto &entry : fullConfigEntries()) { setValue(entry.second.enumKey, config.readEntry(entry.second.configKey, entry.second.defaultValue)); } configEnd(); } void KateConfig::writeConfigEntries(KConfigGroup &config) const { // write all config entries, even the ones ATM not set in this config object but known in the toplevel one for (const auto &entry : fullConfigEntries()) { config.writeEntry(entry.second.configKey, value(entry.second.enumKey)); } } void KateConfig::configStart() { configSessionNumber++; if (configSessionNumber > 1) { return; } configIsRunning = true; } void KateConfig::configEnd() { if (configSessionNumber == 0) { return; } configSessionNumber--; if (configSessionNumber > 0) { return; } configIsRunning = false; updateConfig(); } QVariant KateConfig::value(const int key) const { // first: local lookup const auto it = m_configEntries.find(key); if (it != m_configEntries.end()) { return it->second.value; } // else: fallback to parent config, if any if (m_parent) { return m_parent->value(key); } // if we arrive here, the key was invalid! => programming error // for release builds, we just return invalid variant Q_ASSERT(false); return QVariant(); } bool KateConfig::setValue(const int key, const QVariant &value) { // check: is this key known at all? const auto &knownEntries = fullConfigEntries(); const auto knownIt = knownEntries.find(key); if (knownIt == knownEntries.end()) { // if we arrive here, the key was invalid! => programming error // for release builds, we just fail to set the value Q_ASSERT(false); return false; } // validator set? use it, if not accepting, abort setting if (knownIt->second.validator && !knownIt->second.validator(value)) { return false; } // check if value already there for this config auto valueIt = m_configEntries.find(key); if (valueIt != m_configEntries.end()) { // skip any work if value is equal if (valueIt->second.value == value) { return true; } // else: alter value and be done configStart(); valueIt->second.value = value; configEnd(); return true; } // if not in this hash, we must copy the known entry and adjust the value configStart(); auto res = m_configEntries.emplace(key, knownIt->second); res.first->second.value = value; configEnd(); return true; } QVariant KateConfig::value(const QString &key) const { /** * check if we know this key, if not, return invalid variant */ const auto &knownEntries = fullConfigKeyToEntry(); const auto it = knownEntries.find(key); if (it == knownEntries.end()) { return QVariant(); } /** * key known, dispatch to normal value() function with enum */ return value(it.value()->enumKey); } bool KateConfig::setValue(const QString &key, const QVariant &value) { /** * check if we know this key, if not, ignore the set */ const auto &knownEntries = fullConfigKeyToEntry(); const auto it = knownEntries.find(key); if (it == knownEntries.end()) { return false; } /** * key known, dispatch to normal setValue() function with enum */ return setValue(it.value()->enumKey, value); } // END // BEGIN HelperFunctions KateGlobalConfig *KateGlobalConfig::s_global = nullptr; KateDocumentConfig *KateDocumentConfig::s_global = nullptr; KateViewConfig *KateViewConfig::s_global = nullptr; KateRendererConfig *KateRendererConfig::s_global = nullptr; /** * validate if an encoding is ok * @param name encoding name * @return encoding ok? */ static bool isEncodingOk(const QString &name) { bool found = false; auto codec = KCharsets::charsets()->codecForName(name, found); return found && codec; } static bool inBounds(const int min, const QVariant &value, const int max) { const int val = value.toInt(); return (val >= min) && (val <= max); } static bool isPositive(const QVariant &value) { bool ok; value.toUInt(&ok); return ok; } // END // BEGIN KateGlobalConfig KateGlobalConfig::KateGlobalConfig() { /** * register this as our global instance */ Q_ASSERT(isGlobal()); s_global = this; /** * init all known config entries */ addConfigEntry(ConfigEntry(EncodingProberType, "Encoding Prober Type", QString(), KEncodingProber::Universal)); addConfigEntry(ConfigEntry(FallbackEncoding, "Fallback Encoding", QString(), QStringLiteral("ISO 8859-15"), [](const QVariant &value) { return isEncodingOk(value.toString()); })); /** * finalize the entries, e.g. hashs them */ finalizeConfigEntries(); /** * init with defaults from config or really hardcoded ones */ KConfigGroup cg(KTextEditor::EditorPrivate::config(), "KTextEditor Editor"); readConfig(cg); } void KateGlobalConfig::readConfig(const KConfigGroup &config) { /** * start config update group */ configStart(); /** * read generic entries */ readConfigEntries(config); /** * end config update group, might trigger updateConfig() */ configEnd(); } void KateGlobalConfig::writeConfig(KConfigGroup &config) { /** * write generic entries */ writeConfigEntries(config); } void KateGlobalConfig::updateConfig() { // write config KConfigGroup cg(KTextEditor::EditorPrivate::config(), "KTextEditor Editor"); writeConfig(cg); KTextEditor::EditorPrivate::config()->sync(); } QTextCodec *KateGlobalConfig::fallbackCodec() const { /** * query stored encoding, always fallback to ISO 8859-15 if nothing valid set */ const auto encoding = value(FallbackEncoding).toString(); if (encoding.isEmpty()) { return QTextCodec::codecForName("ISO 8859-15"); } /** * use configured encoding */ return KCharsets::charsets()->codecForName(encoding); } // END // BEGIN KateDocumentConfig KateDocumentConfig::KateDocumentConfig() { /** * register this as our global instance */ Q_ASSERT(isGlobal()); s_global = this; /** * init all known config entries */ addConfigEntry(ConfigEntry(TabWidth, "Tab Width", QStringLiteral("tab-width"), 4, [](const QVariant &value) { return value.toInt() >= 1; })); addConfigEntry(ConfigEntry(IndentationWidth, "Indentation Width", QStringLiteral("indent-width"), 4, [](const QVariant &value) { return value.toInt() >= 1; })); addConfigEntry(ConfigEntry(OnTheFlySpellCheck, "On-The-Fly Spellcheck", QStringLiteral("on-the-fly-spellcheck"), false)); addConfigEntry(ConfigEntry(IndentOnTextPaste, "Indent On Text Paste", QStringLiteral("indent-pasted-text"), false)); addConfigEntry(ConfigEntry(ReplaceTabsWithSpaces, "ReplaceTabsDyn", QStringLiteral("replace-tabs"), true)); addConfigEntry(ConfigEntry(BackupOnSaveLocal, "Backup Local", QStringLiteral("backup-on-save-local"), false)); addConfigEntry(ConfigEntry(BackupOnSaveRemote, "Backup Remote", QStringLiteral("backup-on-save-remote"), false)); addConfigEntry(ConfigEntry(BackupOnSavePrefix, "Backup Prefix", QStringLiteral("backup-on-save-prefix"), QString())); addConfigEntry(ConfigEntry(BackupOnSaveSuffix, "Backup Suffix", QStringLiteral("backup-on-save-suffix"), QStringLiteral("~"))); addConfigEntry(ConfigEntry(IndentationMode, "Indentation Mode", QString(), QStringLiteral("normal"))); addConfigEntry(ConfigEntry(TabHandlingMode, "Tab Handling", QString(), KateDocumentConfig::tabSmart)); addConfigEntry(ConfigEntry(StaticWordWrap, "Word Wrap", QString(), false)); addConfigEntry(ConfigEntry(StaticWordWrapColumn, "Word Wrap Column", QString(), 80, [](const QVariant &value) { return value.toInt() >= 1; })); addConfigEntry(ConfigEntry(PageUpDownMovesCursor, "PageUp/PageDown Moves Cursor", QString(), false)); addConfigEntry(ConfigEntry(SmartHome, "Smart Home", QString(), true)); addConfigEntry(ConfigEntry(ShowTabs, "Show Tabs", QString(), true)); addConfigEntry(ConfigEntry(IndentOnTab, "Indent On Tab", QString(), true)); addConfigEntry(ConfigEntry(KeepExtraSpaces, "Keep Extra Spaces", QString(), false)); addConfigEntry(ConfigEntry(BackspaceIndents, "Indent On Backspace", QString(), true)); addConfigEntry(ConfigEntry(ShowSpacesMode, "Show Spaces", QString(), KateDocumentConfig::None)); addConfigEntry(ConfigEntry(TrailingMarkerSize, "Trailing Marker Size", QString(), 1)); addConfigEntry(ConfigEntry(RemoveSpacesMode, "Remove Spaces", QString(), 0)); addConfigEntry(ConfigEntry(NewlineAtEOF, "Newline at End of File", QString(), true)); addConfigEntry(ConfigEntry(OverwriteMode, "Overwrite Mode", QString(), false)); addConfigEntry(ConfigEntry(Encoding, "Encoding", QString(), QStringLiteral("UTF-8"), [](const QVariant &value) { return isEncodingOk(value.toString()); })); addConfigEntry(ConfigEntry(EndOfLine, "End of Line", QString(), 0)); addConfigEntry(ConfigEntry(AllowEndOfLineDetection, "Allow End of Line Detection", QString(), true)); addConfigEntry(ConfigEntry(ByteOrderMark, "BOM", QString(), false)); addConfigEntry(ConfigEntry(SwapFile, "Swap File Mode", QString(), KateDocumentConfig::EnableSwapFile)); addConfigEntry(ConfigEntry(SwapFileDirectory, "Swap Directory", QString(), QString())); addConfigEntry(ConfigEntry(SwapFileSyncInterval, "Swap Sync Interval", QString(), 15)); addConfigEntry(ConfigEntry(LineLengthLimit, "Line Length Limit", QString(), 10000)); /** * finalize the entries, e.g. hashs them */ finalizeConfigEntries(); /** * init with defaults from config or really hardcoded ones */ KConfigGroup cg(KTextEditor::EditorPrivate::config(), "KTextEditor Document"); readConfig(cg); } KateDocumentConfig::KateDocumentConfig(KTextEditor::DocumentPrivate *doc) : KateConfig(s_global) , m_doc(doc) { /** * per document config doesn't read stuff per default */ } void KateDocumentConfig::readConfig(const KConfigGroup &config) { /** * start config update group */ configStart(); /** * read generic entries */ readConfigEntries(config); /** * fixup sonnet config, see KateSpellCheckConfigTab::apply(), too * WARNING: this is slightly hackish, but it's currently the only way to * do it, see also the KTextEdit class */ if (isGlobal()) { const QSettings settings(QStringLiteral("KDE"), QStringLiteral("Sonnet")); setOnTheFlySpellCheck(settings.value(QStringLiteral("checkerEnabledByDefault"), false).toBool()); } /** * backwards compatibility mappings * convert stuff, old entries deleted in writeConfig */ if (const int backupFlags = config.readEntry("Backup Flags", 0)) { setBackupOnSaveLocal(backupFlags & 0x1); setBackupOnSaveRemote(backupFlags & 0x2); } /** * end config update group, might trigger updateConfig() */ configEnd(); } void KateDocumentConfig::writeConfig(KConfigGroup &config) { /** * write generic entries */ writeConfigEntries(config); /** * backwards compatibility mappings * here we remove old entries we converted on readConfig */ config.deleteEntry("Backup Flags"); } void KateDocumentConfig::updateConfig() { if (m_doc) { m_doc->updateConfig(); return; } if (isGlobal()) { for (int z = 0; z < KTextEditor::EditorPrivate::self()->kateDocuments().size(); ++z) { (KTextEditor::EditorPrivate::self()->kateDocuments())[z]->updateConfig(); } // write config KConfigGroup cg(KTextEditor::EditorPrivate::config(), "KTextEditor Document"); writeConfig(cg); KTextEditor::EditorPrivate::config()->sync(); } } QTextCodec *KateDocumentConfig::codec() const { /** * query stored encoding, always fallback to UTF-8 if nothing valid set */ const auto encoding = value(Encoding).toString(); if (encoding.isEmpty()) { return QTextCodec::codecForName("UTF-8"); } /** * use configured encoding */ return KCharsets::charsets()->codecForName(encoding); } QString KateDocumentConfig::eolString() { switch (eol()) { - case KateDocumentConfig::eolDos: - return QStringLiteral("\r\n"); + case KateDocumentConfig::eolDos: + return QStringLiteral("\r\n"); - case KateDocumentConfig::eolMac: - return QStringLiteral("\r"); + case KateDocumentConfig::eolMac: + return QStringLiteral("\r"); - default: - return QStringLiteral("\n"); + default: + return QStringLiteral("\n"); } } // END // BEGIN KateViewConfig KateViewConfig::KateViewConfig() { s_global = this; // Init all known config entries // NOTE: Ensure to keep the same order as listed in enum ConfigEntryTypes or it will later assert! // addConfigEntry(ConfigEntry(, , , , [])) addConfigEntry(ConfigEntry(AllowMarkMenu, "Allow Mark Menu", QStringLiteral("allow-mark-menu"), true)); addConfigEntry(ConfigEntry(AutoBrackets, "Auto Brackets", QStringLiteral("auto-brackets"), false)); addConfigEntry(ConfigEntry(AutoCenterLines, "Auto Center Lines", QStringLiteral("auto-center-lines"), 0)); addConfigEntry(ConfigEntry(AutomaticCompletionInvocation, "Auto Completion", QString(), true)); addConfigEntry(ConfigEntry(BackspaceRemoveComposedCharacters, "Backspace Remove Composed Characters", QString(), false)); addConfigEntry(ConfigEntry(BookmarkSorting, "Bookmark Menu Sorting", QString(), 0)); addConfigEntry(ConfigEntry(CharsToEncloseSelection, "Chars To Enclose Selection", QStringLiteral("enclose-selection"), QString())); addConfigEntry(ConfigEntry(DefaultMarkType, "Default Mark Type", QStringLiteral("default-mark-type"), KTextEditor::MarkInterface::markType01, [](const QVariant &value) { return isPositive(value); })); addConfigEntry(ConfigEntry(DynWordWrapAlignIndent, "Dynamic Word Wrap Align Indent", QString(), 80, [](const QVariant &value) { return inBounds(1, value, 100); })); addConfigEntry(ConfigEntry(DynWordWrapIndicators, "Dynamic Word Wrap Indicators", QString(), 1, [](const QVariant &value) { return inBounds(1, value, 3); })); addConfigEntry(ConfigEntry(DynWrapAtStaticMarker, "Dynamic Word Wrap At Static Marker", QString(), false)); addConfigEntry(ConfigEntry(DynamicWordWrap, "Dynamic Word Wrap", QStringLiteral("dynamic-word-wrap"), true)); addConfigEntry(ConfigEntry(FoldFirstLine, "Fold First Line", QString(), false)); addConfigEntry(ConfigEntry(InputMode, "Input Mode", QString(), 0, [](const QVariant &value) { return isPositive(value); })); addConfigEntry(ConfigEntry(KeywordCompletion, "Keyword Completion", QStringLiteral("keyword-completion"), true)); addConfigEntry(ConfigEntry(MaxHistorySize, "Maximum Search History Size", QString(), 100, [](const QVariant &value) { return inBounds(0, value, 999); })); addConfigEntry(ConfigEntry(MousePasteAtCursorPosition, "Mouse Paste At Cursor Position", QString(), false)); addConfigEntry(ConfigEntry(PersistentSelection, "Persistent Selection", QStringLiteral("persistent-selectionq"), false)); addConfigEntry(ConfigEntry(ScrollBarMiniMapWidth, "Scroll Bar Mini Map Width", QString(), 60, [](const QVariant &value) { return inBounds(0, value, 999); })); addConfigEntry(ConfigEntry(ScrollPastEnd, "Scroll Past End", QString(), false)); addConfigEntry(ConfigEntry(SearchFlags, "Search/Replace Flags", QString(), IncFromCursor | PowerMatchCase | PowerModePlainText)); addConfigEntry(ConfigEntry(ShowFoldingBar, "Folding Bar", QStringLiteral("folding-bar"), true)); addConfigEntry(ConfigEntry(ShowFoldingPreview, "Folding Preview", QStringLiteral("folding-preview"), true)); addConfigEntry(ConfigEntry(ShowIconBar, "Icon Bar", QStringLiteral("icon-bar"), false)); addConfigEntry(ConfigEntry(ShowLineCount, "Show Line Count", QString(), false)); addConfigEntry(ConfigEntry(ShowLineModification, "Line Modification", QStringLiteral("modification-markers"), false)); addConfigEntry(ConfigEntry(ShowLineNumbers, "Line Numbers", QStringLiteral("line-numbers"), false)); addConfigEntry(ConfigEntry(ShowScrollBarMarks, "Scroll Bar Marks", QString(), false)); addConfigEntry(ConfigEntry(ShowScrollBarMiniMap, "Scroll Bar MiniMap", QStringLiteral("scrollbar-minimap"), true)); addConfigEntry(ConfigEntry(ShowScrollBarMiniMapAll, "Scroll Bar Mini Map All", QString(), true)); addConfigEntry(ConfigEntry(ShowScrollBarPreview, "Scroll Bar Preview", QStringLiteral("scrollbar-preview"), true)); addConfigEntry(ConfigEntry(ShowScrollbars, "Show Scrollbars", QString(), AlwaysOn, [](const QVariant &value) { return inBounds(0, value, 2); })); addConfigEntry(ConfigEntry(ShowWordCount, "Show Word Count", QString(), false)); addConfigEntry(ConfigEntry(TextDragAndDrop, "Text Drag And Drop", QString(), true)); addConfigEntry(ConfigEntry(SmartCopyCut, "Smart Copy Cut", QString(), false)); addConfigEntry(ConfigEntry(UserSetsOfCharsToEncloseSelection, "User Sets Of Chars To Enclose Selection", QString(), QStringList())); addConfigEntry(ConfigEntry(ViInputModeStealKeys, "Vi Input Mode Steal Keys", QString(), false)); addConfigEntry(ConfigEntry(ViRelativeLineNumbers, "Vi Relative Line Numbers", QString(), false)); addConfigEntry(ConfigEntry(WordCompletion, "Word Completion", QString(), true)); addConfigEntry(ConfigEntry(WordCompletionMinimalWordLength, "Word Completion Minimal Word Length", QString(), 3, [](const QVariant &value) { return inBounds(0, value, 99); })); addConfigEntry(ConfigEntry(WordCompletionRemoveTail, "Word Completion Remove Tail", QString(), true)); // Never forget to finalize or the becomes not available finalizeConfigEntries(); // init with defaults from config or really hardcoded ones KConfigGroup config(KTextEditor::EditorPrivate::config(), "KTextEditor View"); readConfig(config); } KateViewConfig::KateViewConfig(KTextEditor::ViewPrivate *view) : KateConfig(s_global) , m_view(view) { } KateViewConfig::~KateViewConfig() { } void KateViewConfig::readConfig(const KConfigGroup &config) { configStart(); // read generic entries readConfigEntries(config); configEnd(); } void KateViewConfig::writeConfig(KConfigGroup &config) { // write generic entries writeConfigEntries(config); } void KateViewConfig::updateConfig() { if (m_view) { m_view->updateConfig(); return; } if (isGlobal()) { const auto allViews = KTextEditor::EditorPrivate::self()->views(); for (KTextEditor::ViewPrivate *view : allViews) { view->updateConfig(); } // write config KConfigGroup cg(KTextEditor::EditorPrivate::config(), "KTextEditor View"); writeConfig(cg); KTextEditor::EditorPrivate::config()->sync(); } } // END // BEGIN KateRendererConfig KateRendererConfig::KateRendererConfig() : m_lineMarkerColor(KTextEditor::MarkInterface::reservedMarkersCount()) , m_schemaSet(false) , m_fontSet(false) , m_wordWrapMarkerSet(false) , m_showIndentationLinesSet(false) , m_showWholeBracketExpressionSet(false) , m_backgroundColorSet(false) , m_selectionColorSet(false) , m_highlightedLineColorSet(false) , m_highlightedBracketColorSet(false) , m_wordWrapMarkerColorSet(false) , m_tabMarkerColorSet(false) , m_indentationLineColorSet(false) , m_iconBarColorSet(false) , m_foldingColorSet(false) , m_lineNumberColorSet(false) , m_currentLineNumberColorSet(false) , m_separatorColorSet(false) , m_spellingMistakeLineColorSet(false) , m_templateColorsSet(false) , m_modifiedLineColorSet(false) , m_savedLineColorSet(false) , m_searchHighlightColorSet(false) , m_replaceHighlightColorSet(false) , m_lineMarkerColorSet(m_lineMarkerColor.size()) { // init bitarray m_lineMarkerColorSet.fill(true); s_global = this; // init with defaults from config or really hardcoded ones KConfigGroup config(KTextEditor::EditorPrivate::config(), "KTextEditor Renderer"); readConfig(config); } KateRendererConfig::KateRendererConfig(KateRenderer *renderer) : KateConfig(s_global) , m_lineMarkerColor(KTextEditor::MarkInterface::reservedMarkersCount()) , m_schemaSet(false) , m_fontSet(false) , m_wordWrapMarkerSet(false) , m_showIndentationLinesSet(false) , m_showWholeBracketExpressionSet(false) , m_backgroundColorSet(false) , m_selectionColorSet(false) , m_highlightedLineColorSet(false) , m_highlightedBracketColorSet(false) , m_wordWrapMarkerColorSet(false) , m_tabMarkerColorSet(false) , m_indentationLineColorSet(false) , m_iconBarColorSet(false) , m_foldingColorSet(false) , m_lineNumberColorSet(false) , m_currentLineNumberColorSet(false) , m_separatorColorSet(false) , m_spellingMistakeLineColorSet(false) , m_templateColorsSet(false) , m_modifiedLineColorSet(false) , m_savedLineColorSet(false) , m_searchHighlightColorSet(false) , m_replaceHighlightColorSet(false) , m_lineMarkerColorSet(m_lineMarkerColor.size()) , m_renderer(renderer) { // init bitarray m_lineMarkerColorSet.fill(false); } KateRendererConfig::~KateRendererConfig() { } namespace { const char KEY_SCHEMA[] = "Schema"; const char KEY_WORD_WRAP_MARKER[] = "Word Wrap Marker"; const char KEY_SHOW_INDENTATION_LINES[] = "Show Indentation Lines"; const char KEY_SHOW_WHOLE_BRACKET_EXPRESSION[] = "Show Whole Bracket Expression"; const char KEY_ANIMATE_BRACKET_MATCHING[] = "Animate Bracket Matching"; } void KateRendererConfig::readConfig(const KConfigGroup &config) { configStart(); // read generic entries readConfigEntries(config); // "Normal" Schema MUST BE THERE, see global kateschemarc setSchema(config.readEntry(KEY_SCHEMA, "Normal")); setWordWrapMarker(config.readEntry(KEY_WORD_WRAP_MARKER, false)); setShowIndentationLines(config.readEntry(KEY_SHOW_INDENTATION_LINES, false)); setShowWholeBracketExpression(config.readEntry(KEY_SHOW_WHOLE_BRACKET_EXPRESSION, false)); setAnimateBracketMatching(config.readEntry(KEY_ANIMATE_BRACKET_MATCHING, false)); configEnd(); } void KateRendererConfig::writeConfig(KConfigGroup &config) { // write generic entries writeConfigEntries(config); config.writeEntry(KEY_SCHEMA, schema()); config.writeEntry(KEY_WORD_WRAP_MARKER, wordWrapMarker()); config.writeEntry(KEY_SHOW_INDENTATION_LINES, showIndentationLines()); config.writeEntry(KEY_SHOW_WHOLE_BRACKET_EXPRESSION, showWholeBracketExpression()); config.writeEntry(KEY_ANIMATE_BRACKET_MATCHING, animateBracketMatching()); } void KateRendererConfig::updateConfig() { if (m_renderer) { m_renderer->updateConfig(); return; } if (isGlobal()) { for (int z = 0; z < KTextEditor::EditorPrivate::self()->views().size(); ++z) { (KTextEditor::EditorPrivate::self()->views())[z]->renderer()->updateConfig(); } // write config KConfigGroup cg(KTextEditor::EditorPrivate::config(), "KTextEditor Renderer"); writeConfig(cg); KTextEditor::EditorPrivate::config()->sync(); } } const QString &KateRendererConfig::schema() const { if (m_schemaSet || isGlobal()) { return m_schema; } return s_global->schema(); } void KateRendererConfig::setSchema(const QString &schema) { if (m_schemaSet && m_schema == schema) { return; } configStart(); m_schemaSet = true; m_schema = schema; setSchemaInternal(schema); configEnd(); } void KateRendererConfig::reloadSchema() { if (isGlobal()) { setSchemaInternal(m_schema); const auto allViews = KTextEditor::EditorPrivate::self()->views(); for (KTextEditor::ViewPrivate *view : allViews) { view->renderer()->config()->reloadSchema(); } } else if (m_renderer && m_schemaSet) { setSchemaInternal(m_schema); } // trigger renderer/view update if (m_renderer) { m_renderer->updateConfig(); } } void KateRendererConfig::setSchemaInternal(const QString &schema) { m_schemaSet = true; m_schema = schema; KConfigGroup config = KTextEditor::EditorPrivate::self()->schemaManager()->schema(schema); // use global color instance, creation is expensive! const KateDefaultColors &colors(KTextEditor::EditorPrivate::self()->defaultColors()); m_backgroundColor = config.readEntry("Color Background", colors.color(Kate::Background)); m_backgroundColorSet = true; m_selectionColor = config.readEntry("Color Selection", colors.color(Kate::SelectionBackground)); m_selectionColorSet = true; m_highlightedLineColor = config.readEntry("Color Highlighted Line", colors.color(Kate::HighlightedLineBackground)); m_highlightedLineColorSet = true; m_highlightedBracketColor = config.readEntry("Color Highlighted Bracket", colors.color(Kate::HighlightedBracket)); m_highlightedBracketColorSet = true; m_wordWrapMarkerColor = config.readEntry("Color Word Wrap Marker", colors.color(Kate::WordWrapMarker)); m_wordWrapMarkerColorSet = true; m_tabMarkerColor = config.readEntry("Color Tab Marker", colors.color(Kate::TabMarker)); m_tabMarkerColorSet = true; m_indentationLineColor = config.readEntry("Color Indentation Line", colors.color(Kate::IndentationLine)); m_indentationLineColorSet = true; m_iconBarColor = config.readEntry("Color Icon Bar", colors.color(Kate::IconBar)); m_iconBarColorSet = true; m_foldingColor = config.readEntry("Color Code Folding", colors.color(Kate::CodeFolding)); m_foldingColorSet = true; m_lineNumberColor = config.readEntry("Color Line Number", colors.color(Kate::LineNumber)); m_lineNumberColorSet = true; m_currentLineNumberColor = config.readEntry("Color Current Line Number", colors.color(Kate::CurrentLineNumber)); m_currentLineNumberColorSet = true; m_separatorColor = config.readEntry("Color Separator", colors.color(Kate::Separator)); m_separatorColorSet = true; m_spellingMistakeLineColor = config.readEntry("Color Spelling Mistake Line", colors.color(Kate::SpellingMistakeLine)); m_spellingMistakeLineColorSet = true; m_modifiedLineColor = config.readEntry("Color Modified Lines", colors.color(Kate::ModifiedLine)); m_modifiedLineColorSet = true; m_savedLineColor = config.readEntry("Color Saved Lines", colors.color(Kate::SavedLine)); m_savedLineColorSet = true; m_searchHighlightColor = config.readEntry("Color Search Highlight", colors.color(Kate::SearchHighlight)); m_searchHighlightColorSet = true; m_replaceHighlightColor = config.readEntry("Color Replace Highlight", colors.color(Kate::ReplaceHighlight)); m_replaceHighlightColorSet = true; for (int i = Kate::FIRST_MARK; i <= Kate::LAST_MARK; i++) { QColor col = config.readEntry(QStringLiteral("Color MarkType %1").arg(i + 1), colors.mark(i)); m_lineMarkerColorSet[i] = true; m_lineMarkerColor[i] = col; } setFontWithDroppedStyleName(config.readEntry("Font", QFontDatabase::systemFont(QFontDatabase::FixedFont))); m_templateBackgroundColor = config.readEntry(QStringLiteral("Color Template Background"), colors.color(Kate::TemplateBackground)); m_templateFocusedEditablePlaceholderColor = config.readEntry(QStringLiteral("Color Template Focused Editable Placeholder"), colors.color(Kate::TemplateFocusedEditablePlaceholder)); m_templateEditablePlaceholderColor = config.readEntry(QStringLiteral("Color Template Editable Placeholder"), colors.color(Kate::TemplateEditablePlaceholder)); m_templateNotEditablePlaceholderColor = config.readEntry(QStringLiteral("Color Template Not Editable Placeholder"), colors.color(Kate::TemplateNotEditablePlaceholder)); m_templateColorsSet = true; } const QFont &KateRendererConfig::baseFont() const { if (m_fontSet || isGlobal()) { return m_font; } return s_global->baseFont(); } void KateRendererConfig::setFont(const QFont &font) { if (m_fontSet && m_font == font) { return; } configStart(); setFontWithDroppedStyleName(font); configEnd(); } void KateRendererConfig::setFontWithDroppedStyleName(const QFont &font) { /** * Drop styleName, otherwise stuff like bold/italic/... won't work as style! */ m_font = font; m_font.setStyleName(QString()); m_fontSet = true; } bool KateRendererConfig::wordWrapMarker() const { if (m_wordWrapMarkerSet || isGlobal()) { return m_wordWrapMarker; } return s_global->wordWrapMarker(); } void KateRendererConfig::setWordWrapMarker(bool on) { if (m_wordWrapMarkerSet && m_wordWrapMarker == on) { return; } configStart(); m_wordWrapMarkerSet = true; m_wordWrapMarker = on; configEnd(); } const QColor &KateRendererConfig::backgroundColor() const { if (m_backgroundColorSet || isGlobal()) { return m_backgroundColor; } return s_global->backgroundColor(); } void KateRendererConfig::setBackgroundColor(const QColor &col) { if (m_backgroundColorSet && m_backgroundColor == col) { return; } configStart(); m_backgroundColorSet = true; m_backgroundColor = col; configEnd(); } const QColor &KateRendererConfig::selectionColor() const { if (m_selectionColorSet || isGlobal()) { return m_selectionColor; } return s_global->selectionColor(); } void KateRendererConfig::setSelectionColor(const QColor &col) { if (m_selectionColorSet && m_selectionColor == col) { return; } configStart(); m_selectionColorSet = true; m_selectionColor = col; configEnd(); } const QColor &KateRendererConfig::highlightedLineColor() const { if (m_highlightedLineColorSet || isGlobal()) { return m_highlightedLineColor; } return s_global->highlightedLineColor(); } void KateRendererConfig::setHighlightedLineColor(const QColor &col) { if (m_highlightedLineColorSet && m_highlightedLineColor == col) { return; } configStart(); m_highlightedLineColorSet = true; m_highlightedLineColor = col; configEnd(); } const QColor &KateRendererConfig::lineMarkerColor(KTextEditor::MarkInterface::MarkTypes type) const { int index = 0; if (type > 0) { while ((type >> index++) ^ 1) { } } index -= 1; if (index < 0 || index >= KTextEditor::MarkInterface::reservedMarkersCount()) { static QColor dummy; return dummy; } if (m_lineMarkerColorSet[index] || isGlobal()) { return m_lineMarkerColor[index]; } return s_global->lineMarkerColor(type); } void KateRendererConfig::setLineMarkerColor(const QColor &col, KTextEditor::MarkInterface::MarkTypes type) { int index = static_cast(log(static_cast(type)) / log(2.0)); Q_ASSERT(index >= 0 && index < KTextEditor::MarkInterface::reservedMarkersCount()); if (m_lineMarkerColorSet[index] && m_lineMarkerColor[index] == col) { return; } configStart(); m_lineMarkerColorSet[index] = true; m_lineMarkerColor[index] = col; configEnd(); } const QColor &KateRendererConfig::highlightedBracketColor() const { if (m_highlightedBracketColorSet || isGlobal()) { return m_highlightedBracketColor; } return s_global->highlightedBracketColor(); } void KateRendererConfig::setHighlightedBracketColor(const QColor &col) { if (m_highlightedBracketColorSet && m_highlightedBracketColor == col) { return; } configStart(); m_highlightedBracketColorSet = true; m_highlightedBracketColor = col; configEnd(); } const QColor &KateRendererConfig::wordWrapMarkerColor() const { if (m_wordWrapMarkerColorSet || isGlobal()) { return m_wordWrapMarkerColor; } return s_global->wordWrapMarkerColor(); } void KateRendererConfig::setWordWrapMarkerColor(const QColor &col) { if (m_wordWrapMarkerColorSet && m_wordWrapMarkerColor == col) { return; } configStart(); m_wordWrapMarkerColorSet = true; m_wordWrapMarkerColor = col; configEnd(); } const QColor &KateRendererConfig::tabMarkerColor() const { if (m_tabMarkerColorSet || isGlobal()) { return m_tabMarkerColor; } return s_global->tabMarkerColor(); } void KateRendererConfig::setTabMarkerColor(const QColor &col) { if (m_tabMarkerColorSet && m_tabMarkerColor == col) { return; } configStart(); m_tabMarkerColorSet = true; m_tabMarkerColor = col; configEnd(); } const QColor &KateRendererConfig::indentationLineColor() const { if (m_indentationLineColorSet || isGlobal()) { return m_indentationLineColor; } return s_global->indentationLineColor(); } void KateRendererConfig::setIndentationLineColor(const QColor &col) { if (m_indentationLineColorSet && m_indentationLineColor == col) { return; } configStart(); m_indentationLineColorSet = true; m_indentationLineColor = col; configEnd(); } const QColor &KateRendererConfig::iconBarColor() const { if (m_iconBarColorSet || isGlobal()) { return m_iconBarColor; } return s_global->iconBarColor(); } void KateRendererConfig::setIconBarColor(const QColor &col) { if (m_iconBarColorSet && m_iconBarColor == col) { return; } configStart(); m_iconBarColorSet = true; m_iconBarColor = col; configEnd(); } const QColor &KateRendererConfig::foldingColor() const { if (m_foldingColorSet || isGlobal()) { return m_foldingColor; } return s_global->foldingColor(); } void KateRendererConfig::setFoldingColor(const QColor &col) { if (m_foldingColorSet && m_foldingColor == col) { return; } configStart(); m_foldingColorSet = true; m_foldingColor = col; configEnd(); } const QColor &KateRendererConfig::templateBackgroundColor() const { if (m_templateColorsSet || isGlobal()) { return m_templateBackgroundColor; } return s_global->templateBackgroundColor(); } const QColor &KateRendererConfig::templateEditablePlaceholderColor() const { if (m_templateColorsSet || isGlobal()) { return m_templateEditablePlaceholderColor; } return s_global->templateEditablePlaceholderColor(); } const QColor &KateRendererConfig::templateFocusedEditablePlaceholderColor() const { if (m_templateColorsSet || isGlobal()) { return m_templateFocusedEditablePlaceholderColor; } return s_global->templateFocusedEditablePlaceholderColor(); } const QColor &KateRendererConfig::templateNotEditablePlaceholderColor() const { if (m_templateColorsSet || isGlobal()) { return m_templateNotEditablePlaceholderColor; } return s_global->templateNotEditablePlaceholderColor(); } const QColor &KateRendererConfig::lineNumberColor() const { if (m_lineNumberColorSet || isGlobal()) { return m_lineNumberColor; } return s_global->lineNumberColor(); } void KateRendererConfig::setLineNumberColor(const QColor &col) { if (m_lineNumberColorSet && m_lineNumberColor == col) { return; } configStart(); m_lineNumberColorSet = true; m_lineNumberColor = col; configEnd(); } const QColor &KateRendererConfig::currentLineNumberColor() const { if (m_currentLineNumberColorSet || isGlobal()) { return m_currentLineNumberColor; } return s_global->currentLineNumberColor(); } void KateRendererConfig::setCurrentLineNumberColor(const QColor &col) { if (m_currentLineNumberColorSet && m_currentLineNumberColor == col) { return; } configStart(); m_currentLineNumberColorSet = true; m_currentLineNumberColor = col; configEnd(); } const QColor &KateRendererConfig::separatorColor() const { if (m_separatorColorSet || isGlobal()) { return m_separatorColor; } return s_global->separatorColor(); } void KateRendererConfig::setSeparatorColor(const QColor &col) { if (m_separatorColorSet && m_separatorColor == col) { return; } configStart(); m_separatorColorSet = true; m_separatorColor = col; configEnd(); } const QColor &KateRendererConfig::spellingMistakeLineColor() const { if (m_spellingMistakeLineColorSet || isGlobal()) { return m_spellingMistakeLineColor; } return s_global->spellingMistakeLineColor(); } void KateRendererConfig::setSpellingMistakeLineColor(const QColor &col) { if (m_spellingMistakeLineColorSet && m_spellingMistakeLineColor == col) { return; } configStart(); m_spellingMistakeLineColorSet = true; m_spellingMistakeLineColor = col; configEnd(); } const QColor &KateRendererConfig::modifiedLineColor() const { if (m_modifiedLineColorSet || isGlobal()) { return m_modifiedLineColor; } return s_global->modifiedLineColor(); } void KateRendererConfig::setModifiedLineColor(const QColor &col) { if (m_modifiedLineColorSet && m_modifiedLineColor == col) { return; } configStart(); m_modifiedLineColorSet = true; m_modifiedLineColor = col; configEnd(); } const QColor &KateRendererConfig::savedLineColor() const { if (m_savedLineColorSet || isGlobal()) { return m_savedLineColor; } return s_global->savedLineColor(); } void KateRendererConfig::setSavedLineColor(const QColor &col) { if (m_savedLineColorSet && m_savedLineColor == col) { return; } configStart(); m_savedLineColorSet = true; m_savedLineColor = col; configEnd(); } const QColor &KateRendererConfig::searchHighlightColor() const { if (m_searchHighlightColorSet || isGlobal()) { return m_searchHighlightColor; } return s_global->searchHighlightColor(); } void KateRendererConfig::setSearchHighlightColor(const QColor &col) { if (m_searchHighlightColorSet && m_searchHighlightColor == col) { return; } configStart(); m_searchHighlightColorSet = true; m_searchHighlightColor = col; configEnd(); } const QColor &KateRendererConfig::replaceHighlightColor() const { if (m_replaceHighlightColorSet || isGlobal()) { return m_replaceHighlightColor; } return s_global->replaceHighlightColor(); } void KateRendererConfig::setReplaceHighlightColor(const QColor &col) { if (m_replaceHighlightColorSet && m_replaceHighlightColor == col) { return; } configStart(); m_replaceHighlightColorSet = true; m_replaceHighlightColor = col; configEnd(); } bool KateRendererConfig::showIndentationLines() const { if (m_showIndentationLinesSet || isGlobal()) { return m_showIndentationLines; } return s_global->showIndentationLines(); } void KateRendererConfig::setShowIndentationLines(bool on) { if (m_showIndentationLinesSet && m_showIndentationLines == on) { return; } configStart(); m_showIndentationLinesSet = true; m_showIndentationLines = on; configEnd(); } bool KateRendererConfig::showWholeBracketExpression() const { if (m_showWholeBracketExpressionSet || isGlobal()) { return m_showWholeBracketExpression; } return s_global->showWholeBracketExpression(); } void KateRendererConfig::setShowWholeBracketExpression(bool on) { if (m_showWholeBracketExpressionSet && m_showWholeBracketExpression == on) { return; } configStart(); m_showWholeBracketExpressionSet = true; m_showWholeBracketExpression = on; configEnd(); } bool KateRendererConfig::animateBracketMatching() const { return s_global->m_animateBracketMatching; } void KateRendererConfig::setAnimateBracketMatching(bool on) { if (!isGlobal()) { s_global->setAnimateBracketMatching(on); } else if (on != m_animateBracketMatching) { configStart(); m_animateBracketMatching = on; configEnd(); } } // END diff --git a/src/utils/katedefaultcolors.cpp b/src/utils/katedefaultcolors.cpp index 924a447c..547bb911 100644 --- a/src/utils/katedefaultcolors.cpp +++ b/src/utils/katedefaultcolors.cpp @@ -1,141 +1,141 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2014 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katedefaultcolors.h" #include "kateglobal.h" #include using namespace Kate; KateDefaultColors::KateDefaultColors() /** * for unit testing: avoid global configs! */ : m_view(QPalette::Active, KColorScheme::View, KSharedConfig::openConfig(KTextEditor::EditorPrivate::unitTestMode() ? QStringLiteral("unittestmoderc") : QString(), KTextEditor::EditorPrivate::unitTestMode() ? KConfig::SimpleConfig : KConfig::FullConfig)) , m_window(QPalette::Active, KColorScheme::Window, KSharedConfig::openConfig(KTextEditor::EditorPrivate::unitTestMode() ? QStringLiteral("unittestmoderc") : QString(), KTextEditor::EditorPrivate::unitTestMode() ? KConfig::SimpleConfig : KConfig::FullConfig)) , m_selection(QPalette::Active, KColorScheme::Selection, KSharedConfig::openConfig(KTextEditor::EditorPrivate::unitTestMode() ? QStringLiteral("unittestmoderc") : QString(), KTextEditor::EditorPrivate::unitTestMode() ? KConfig::SimpleConfig : KConfig::FullConfig)) , m_inactiveSelection(QPalette::Inactive, KColorScheme::Selection, KSharedConfig::openConfig(KTextEditor::EditorPrivate::unitTestMode() ? QStringLiteral("unittestmoderc") : QString(), KTextEditor::EditorPrivate::unitTestMode() ? KConfig::SimpleConfig : KConfig::FullConfig)) /** * init our colors */ , m_background(m_view.background().color()) , m_foreground(m_view.foreground().color()) , m_backgroundLuma(KColorUtils::luma(m_background)) , m_foregroundLuma(KColorUtils::luma(m_foreground)) { } QColor KateDefaultColors::color(ColorRole role) const { switch (role) { - case Background: - return m_background; - case SelectionBackground: - return m_selection.background().color(); - case HighlightedLineBackground: - return m_view.background(KColorScheme::AlternateBackground).color(); - case HighlightedBracket: - return KColorUtils::tint(m_background, m_view.decoration(KColorScheme::HoverColor).color()); - case WordWrapMarker: - return KColorUtils::shade(m_background, m_backgroundLuma > 0.3 ? -0.15 : 0.03); - case TabMarker: - return KColorUtils::shade(m_background, m_backgroundLuma > 0.7 ? -0.35 : 0.3); - case IndentationLine: - return KColorUtils::shade(m_background, m_backgroundLuma > 0.7 ? -0.35 : 0.3); - case IconBar: - return m_window.background().color(); - case CodeFolding: - return m_inactiveSelection.background().color(); - case LineNumber: - case CurrentLineNumber: - return m_window.foreground().color(); - case Separator: - return m_view.foreground(KColorScheme::InactiveText).color(); - case SpellingMistakeLine: - return m_view.foreground(KColorScheme::NegativeText).color(); - case ModifiedLine: - return m_view.background(KColorScheme::NegativeBackground).color(); - case SavedLine: - return m_view.background(KColorScheme::PositiveBackground).color(); - case SearchHighlight: - return adaptToScheme(Qt::yellow, BackgroundColor); - case ReplaceHighlight: - return adaptToScheme(Qt::green, BackgroundColor); - case TemplateBackground: - return m_window.background().color(); - case TemplateFocusedEditablePlaceholder: - return m_view.background(KColorScheme::PositiveBackground).color(); - case TemplateEditablePlaceholder: - return m_view.background(KColorScheme::PositiveBackground).color(); - case TemplateNotEditablePlaceholder: - return m_view.background(KColorScheme::NegativeBackground).color(); + case Background: + return m_background; + case SelectionBackground: + return m_selection.background().color(); + case HighlightedLineBackground: + return m_view.background(KColorScheme::AlternateBackground).color(); + case HighlightedBracket: + return KColorUtils::tint(m_background, m_view.decoration(KColorScheme::HoverColor).color()); + case WordWrapMarker: + return KColorUtils::shade(m_background, m_backgroundLuma > 0.3 ? -0.15 : 0.03); + case TabMarker: + return KColorUtils::shade(m_background, m_backgroundLuma > 0.7 ? -0.35 : 0.3); + case IndentationLine: + return KColorUtils::shade(m_background, m_backgroundLuma > 0.7 ? -0.35 : 0.3); + case IconBar: + return m_window.background().color(); + case CodeFolding: + return m_inactiveSelection.background().color(); + case LineNumber: + case CurrentLineNumber: + return m_window.foreground().color(); + case Separator: + return m_view.foreground(KColorScheme::InactiveText).color(); + case SpellingMistakeLine: + return m_view.foreground(KColorScheme::NegativeText).color(); + case ModifiedLine: + return m_view.background(KColorScheme::NegativeBackground).color(); + case SavedLine: + return m_view.background(KColorScheme::PositiveBackground).color(); + case SearchHighlight: + return adaptToScheme(Qt::yellow, BackgroundColor); + case ReplaceHighlight: + return adaptToScheme(Qt::green, BackgroundColor); + case TemplateBackground: + return m_window.background().color(); + case TemplateFocusedEditablePlaceholder: + return m_view.background(KColorScheme::PositiveBackground).color(); + case TemplateEditablePlaceholder: + return m_view.background(KColorScheme::PositiveBackground).color(); + case TemplateNotEditablePlaceholder: + return m_view.background(KColorScheme::NegativeBackground).color(); } qFatal("Unhandled color requested: %d\n", role); return QColor(); } QColor KateDefaultColors::mark(Mark mark) const { // note: the mark color is used as background color at very low opacity (around 0.1) // hence, make sure the color returned here has a high saturation switch (mark) { - case Bookmark: - return adaptToScheme(Qt::blue, BackgroundColor); - case ActiveBreakpoint: - return adaptToScheme(Qt::red, BackgroundColor); - case ReachedBreakpoint: - return adaptToScheme(Qt::yellow, BackgroundColor); - case DisabledBreakpoint: - return adaptToScheme(Qt::magenta, BackgroundColor); - case Execution: - return adaptToScheme(Qt::gray, BackgroundColor); - case Warning: - return m_view.foreground(KColorScheme::NeutralText).color(); - case Error: - return m_view.foreground(KColorScheme::NegativeText).color(); + case Bookmark: + return adaptToScheme(Qt::blue, BackgroundColor); + case ActiveBreakpoint: + return adaptToScheme(Qt::red, BackgroundColor); + case ReachedBreakpoint: + return adaptToScheme(Qt::yellow, BackgroundColor); + case DisabledBreakpoint: + return adaptToScheme(Qt::magenta, BackgroundColor); + case Execution: + return adaptToScheme(Qt::gray, BackgroundColor); + case Warning: + return m_view.foreground(KColorScheme::NeutralText).color(); + case Error: + return m_view.foreground(KColorScheme::NegativeText).color(); } qFatal("Unhandled color for mark requested: %d\n", mark); return QColor(); } QColor KateDefaultColors::mark(int i) const { Q_ASSERT(i >= FIRST_MARK && i <= LAST_MARK); return mark(static_cast(i)); } QColor KateDefaultColors::adaptToScheme(const QColor &color, ColorType type) const { if (m_foregroundLuma > m_backgroundLuma) { // for dark color schemes, produce a fitting color by tinting with the foreground/background color return KColorUtils::tint(type == ForegroundColor ? m_foreground : m_background, color, 0.5); } return color; } diff --git a/src/utils/kateglobal.cpp b/src/utils/kateglobal.cpp index 3fb46a2c..71d0cd68 100644 --- a/src/utils/kateglobal.cpp +++ b/src/utils/kateglobal.cpp @@ -1,560 +1,560 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2001-2010 Christoph Cullmann * Copyright (C) 2009 Erlend Hamberg * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateglobal.h" #include "config.h" #include #include "katebuffer.h" #include "katecmd.h" #include "katecmds.h" #include "kateconfig.h" #include "katedefaultcolors.h" #include "katedocument.h" #include "katehighlightingcmds.h" #include "katekeywordcompletion.h" #include "katemodemanager.h" #include "katepartdebug.h" #include "katerenderer.h" #include "kateschema.h" #include "kateschemaconfig.h" #include "katescriptmanager.h" #include "katesedcmd.h" #include "katevariableexpansionmanager.h" #include "kateview.h" #include "katewordcompletion.h" #include "spellcheck/spellcheck.h" #include "katenormalinputmodefactory.h" #include "kateviinputmodefactory.h" #include #include #include #include #include #include #include #include #include #include #include #if LIBGIT2_FOUND #include #endif // BEGIN unit test mode static bool kateUnitTestMode = false; void KTextEditor::EditorPrivate::enableUnitTestMode() { kateUnitTestMode = true; } bool KTextEditor::EditorPrivate::unitTestMode() { return kateUnitTestMode; } // END unit test mode KTextEditor::EditorPrivate::EditorPrivate(QPointer &staticInstance) : KTextEditor::Editor(this) , m_aboutData(QStringLiteral("katepart"), i18n("Kate Part"), QStringLiteral(KTEXTEDITOR_VERSION_STRING), i18n("Embeddable editor component"), KAboutLicense::LGPL_V2, i18n("(c) 2000-2019 The Kate Authors"), QString(), QStringLiteral("https://kate-editor.org")) , m_dummyApplication(nullptr) , m_application(&m_dummyApplication) , m_dummyMainWindow(nullptr) , m_defaultColors(new KateDefaultColors()) , m_searchHistoryModel(nullptr) , m_replaceHistoryModel(nullptr) { // remember this staticInstance = this; // init libgit2, we require at least 0.22 which has this function! #if LIBGIT2_FOUND git_libgit2_init(); #endif /** * register some datatypes */ qRegisterMetaType("KTextEditor::Cursor"); qRegisterMetaType("KTextEditor::Document*"); qRegisterMetaType("KTextEditor::View*"); // // fill about data // m_aboutData.addAuthor(i18n("Christoph Cullmann"), i18n("Maintainer"), QStringLiteral("cullmann@kde.org"), QStringLiteral("https://cullmann.io")); m_aboutData.addAuthor(i18n("Dominik Haumann"), i18n("Core Developer"), QStringLiteral("dhaumann@kde.org")); m_aboutData.addAuthor(i18n("Milian Wolff"), i18n("Core Developer"), QStringLiteral("mail@milianw.de"), QStringLiteral("http://milianw.de")); m_aboutData.addAuthor(i18n("Joseph Wenninger"), i18n("Core Developer"), QStringLiteral("jowenn@kde.org"), QStringLiteral("http://stud3.tuwien.ac.at/~e9925371")); m_aboutData.addAuthor(i18n("Erlend Hamberg"), i18n("Vi Input Mode"), QStringLiteral("ehamberg@gmail.com"), QStringLiteral("https://hamberg.no/erlend")); m_aboutData.addAuthor(i18n("Bernhard Beschow"), i18n("Developer"), QStringLiteral("bbeschow@cs.tu-berlin.de"), QStringLiteral("https://user.cs.tu-berlin.de/~bbeschow")); m_aboutData.addAuthor(i18n("Anders Lund"), i18n("Core Developer"), QStringLiteral("anders@alweb.dk"), QStringLiteral("https://alweb.dk")); m_aboutData.addAuthor(i18n("Michel Ludwig"), i18n("On-the-fly spell checking"), QStringLiteral("michel.ludwig@kdemail.net")); m_aboutData.addAuthor(i18n("Pascal Létourneau"), i18n("Large scale bug fixing"), QStringLiteral("pascal.letourneau@gmail.com")); m_aboutData.addAuthor(i18n("Hamish Rodda"), i18n("Core Developer"), QStringLiteral("rodda@kde.org")); m_aboutData.addAuthor(i18n("Waldo Bastian"), i18n("The cool buffersystem"), QStringLiteral("bastian@kde.org")); m_aboutData.addAuthor(i18n("Charles Samuels"), i18n("The Editing Commands"), QStringLiteral("charles@kde.org")); m_aboutData.addAuthor(i18n("Matt Newell"), i18n("Testing, ..."), QStringLiteral("newellm@proaxis.com")); m_aboutData.addAuthor(i18n("Michael Bartl"), i18n("Former Core Developer"), QStringLiteral("michael.bartl1@chello.at")); m_aboutData.addAuthor(i18n("Michael McCallum"), i18n("Core Developer"), QStringLiteral("gholam@xtra.co.nz")); m_aboutData.addAuthor(i18n("Michael Koch"), i18n("KWrite port to KParts"), QStringLiteral("koch@kde.org")); m_aboutData.addAuthor(i18n("Christian Gebauer"), QString(), QStringLiteral("gebauer@kde.org")); m_aboutData.addAuthor(i18n("Simon Hausmann"), QString(), QStringLiteral("hausmann@kde.org")); m_aboutData.addAuthor(i18n("Glen Parker"), i18n("KWrite Undo History, Kspell integration"), QStringLiteral("glenebob@nwlink.com")); m_aboutData.addAuthor(i18n("Scott Manson"), i18n("KWrite XML Syntax highlighting support"), QStringLiteral("sdmanson@alltel.net")); m_aboutData.addAuthor(i18n("John Firebaugh"), i18n("Patches and more"), QStringLiteral("jfirebaugh@kde.org")); m_aboutData.addAuthor(i18n("Andreas Kling"), i18n("Developer"), QStringLiteral("kling@impul.se")); m_aboutData.addAuthor(i18n("Mirko Stocker"), i18n("Various bugfixes"), QStringLiteral("me@misto.ch"), QStringLiteral("https://misto.ch/")); m_aboutData.addAuthor(i18n("Matthew Woehlke"), i18n("Selection, KColorScheme integration"), QStringLiteral("mw_triad@users.sourceforge.net")); m_aboutData.addAuthor(i18n("Sebastian Pipping"), i18n("Search bar back- and front-end"), QStringLiteral("webmaster@hartwork.org"), QStringLiteral("https://hartwork.org/")); m_aboutData.addAuthor(i18n("Jochen Wilhelmy"), i18n("Original KWrite Author"), QStringLiteral("digisnap@cs.tu-berlin.de")); m_aboutData.addAuthor(i18n("Gerald Senarclens de Grancy"), i18n("QA and Scripting"), QStringLiteral("oss@senarclens.eu"), QStringLiteral("http://find-santa.eu/")); m_aboutData.addCredit(i18n("Matteo Merli"), i18n("Highlighting for RPM Spec-Files, Perl, Diff and more"), QStringLiteral("merlim@libero.it")); m_aboutData.addCredit(i18n("Rocky Scaletta"), i18n("Highlighting for VHDL"), QStringLiteral("rocky@purdue.edu")); m_aboutData.addCredit(i18n("Yury Lebedev"), i18n("Highlighting for SQL"), QString()); m_aboutData.addCredit(i18n("Chris Ross"), i18n("Highlighting for Ferite"), QString()); m_aboutData.addCredit(i18n("Nick Roux"), i18n("Highlighting for ILERPG"), QString()); m_aboutData.addCredit(i18n("Carsten Niehaus"), i18n("Highlighting for LaTeX"), QString()); m_aboutData.addCredit(i18n("Per Wigren"), i18n("Highlighting for Makefiles, Python"), QString()); m_aboutData.addCredit(i18n("Jan Fritz"), i18n("Highlighting for Python"), QString()); m_aboutData.addCredit(i18n("Daniel Naber")); m_aboutData.addCredit(i18n("Roland Pabel"), i18n("Highlighting for Scheme"), QString()); m_aboutData.addCredit(i18n("Cristi Dumitrescu"), i18n("PHP Keyword/Datatype list"), QString()); m_aboutData.addCredit(i18n("Carsten Pfeiffer"), i18n("Very nice help"), QString()); m_aboutData.addCredit(i18n("Bruno Massa"), i18n("Highlighting for Lua"), QStringLiteral("brmassa@gmail.com")); m_aboutData.addCredit(i18n("All people who have contributed and I have forgotten to mention")); m_aboutData.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); /** * set the new Kate mascot */ m_aboutData.setProgramLogo(QImage(QStringLiteral(":/ktexteditor/mascot.png"))); // // dir watch // m_dirWatch = new KDirWatch(); // // command manager // m_cmdManager = new KateCmd(); // // variable expansion manager // m_variableExpansionManager = new KateVariableExpansionManager(this); // // hl manager // m_hlManager = new KateHlManager(); // // mode man // m_modeManager = new KateModeManager(); // // schema man // m_schemaManager = new KateSchemaManager(); // // input mode factories // KateAbstractInputModeFactory *fact; fact = new KateNormalInputModeFactory(); m_inputModeFactories.insert(KTextEditor::View::NormalInputMode, fact); #if BUILD_VIMODE fact = new KateViInputModeFactory(); m_inputModeFactories.insert(KTextEditor::View::ViInputMode, fact); #endif // // spell check manager // m_spellCheckManager = new KateSpellCheckManager(); // config objects m_globalConfig = new KateGlobalConfig(); m_documentConfig = new KateDocumentConfig(); m_viewConfig = new KateViewConfig(); m_rendererConfig = new KateRendererConfig(); // create script manager (search scripts) m_scriptManager = KateScriptManager::self(); // // init the cmds // m_cmds.push_back(KateCommands::CoreCommands::self()); m_cmds.push_back(KateCommands::Character::self()); m_cmds.push_back(KateCommands::Date::self()); m_cmds.push_back(KateCommands::SedReplace::self()); m_cmds.push_back(KateCommands::Highlighting::self()); // global word completion model m_wordCompletionModel = new KateWordCompletionModel(this); // global keyword completion model m_keywordCompletionModel = new KateKeywordCompletionModel(this); // tap to QApplication object for color palette changes qApp->installEventFilter(this); } KTextEditor::EditorPrivate::~EditorPrivate() { delete m_globalConfig; delete m_documentConfig; delete m_viewConfig; delete m_rendererConfig; delete m_modeManager; delete m_schemaManager; delete m_dirWatch; // cu managers delete m_scriptManager; delete m_hlManager; delete m_spellCheckManager; // cu model delete m_wordCompletionModel; // delete variable expansion manager delete m_variableExpansionManager; m_variableExpansionManager = nullptr; // delete the commands before we delete the cmd manager qDeleteAll(m_cmds); delete m_cmdManager; qDeleteAll(m_inputModeFactories); // shutdown libgit2, we require at least 0.22 which has this function! #if LIBGIT2_FOUND git_libgit2_shutdown(); #endif } KTextEditor::Document *KTextEditor::EditorPrivate::createDocument(QObject *parent) { KTextEditor::DocumentPrivate *doc = new KTextEditor::DocumentPrivate(false, false, nullptr, parent); emit documentCreated(this, doc); return doc; } // END KTextEditor::Editor config stuff void KTextEditor::EditorPrivate::configDialog(QWidget *parent) { QPointer kd = new KPageDialog(parent); kd->setWindowTitle(i18n("Configure")); kd->setFaceType(KPageDialog::List); kd->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply | QDialogButtonBox::Help); QList editorPages; for (int i = 0; i < configPages(); ++i) { QFrame *page = new QFrame(); KTextEditor::ConfigPage *cp = configPage(i, page); KPageWidgetItem *item = kd->addPage(page, cp->name()); item->setHeader(cp->fullName()); item->setIcon(cp->icon()); QVBoxLayout *topLayout = new QVBoxLayout(page); topLayout->setContentsMargins(0, 0, 0, 0); connect(kd->button(QDialogButtonBox::Apply), SIGNAL(clicked()), cp, SLOT(apply())); topLayout->addWidget(cp); editorPages.append(cp); } if (kd->exec() && kd) { KateGlobalConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); KateViewConfig::global()->configStart(); KateRendererConfig::global()->configStart(); for (int i = 0; i < editorPages.count(); ++i) { editorPages.at(i)->apply(); } KateGlobalConfig::global()->configEnd(); KateDocumentConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); KateRendererConfig::global()->configEnd(); } delete kd; } int KTextEditor::EditorPrivate::configPages() const { return 4; } KTextEditor::ConfigPage *KTextEditor::EditorPrivate::configPage(int number, QWidget *parent) { switch (number) { - case 0: - return new KateViewDefaultsConfig(parent); + case 0: + return new KateViewDefaultsConfig(parent); - case 1: - return new KateSchemaConfigPage(parent); + case 1: + return new KateSchemaConfigPage(parent); - case 2: - return new KateEditConfigTab(parent); + case 2: + return new KateEditConfigTab(parent); - case 3: - return new KateSaveConfigTab(parent); + case 3: + return new KateSaveConfigTab(parent); - default: - break; + default: + break; } return nullptr; } /** * Cleanup the KTextEditor::EditorPrivate during QCoreApplication shutdown */ static void cleanupGlobal() { /** * delete if there */ delete KTextEditor::EditorPrivate::self(); } KTextEditor::EditorPrivate *KTextEditor::EditorPrivate::self() { /** * remember the static instance in a QPointer */ static bool inited = false; static QPointer staticInstance; /** * just return it, if already inited */ if (inited) { return staticInstance.data(); } /** * start init process */ inited = true; /** * now create the object and store it */ new KTextEditor::EditorPrivate(staticInstance); /** * register cleanup * let use be deleted during QCoreApplication shutdown */ qAddPostRoutine(cleanupGlobal); /** * return instance */ return staticInstance.data(); } void KTextEditor::EditorPrivate::registerDocument(KTextEditor::DocumentPrivate *doc) { Q_ASSERT(!m_documents.contains(doc)); m_documents.insert(doc, doc); } void KTextEditor::EditorPrivate::deregisterDocument(KTextEditor::DocumentPrivate *doc) { Q_ASSERT(m_documents.contains(doc)); m_documents.remove(doc); } void KTextEditor::EditorPrivate::registerView(KTextEditor::ViewPrivate *view) { Q_ASSERT(!m_views.contains(view)); m_views.insert(view); } void KTextEditor::EditorPrivate::deregisterView(KTextEditor::ViewPrivate *view) { Q_ASSERT(m_views.contains(view)); m_views.remove(view); } KTextEditor::Command *KTextEditor::EditorPrivate::queryCommand(const QString &cmd) const { return m_cmdManager->queryCommand(cmd); } QList KTextEditor::EditorPrivate::commands() const { return m_cmdManager->commands(); } QStringList KTextEditor::EditorPrivate::commandList() const { return m_cmdManager->commandList(); } KateVariableExpansionManager *KTextEditor::EditorPrivate::variableExpansionManager() { return m_variableExpansionManager; } void KTextEditor::EditorPrivate::updateColorPalette() { // update default color cache m_defaultColors.reset(new KateDefaultColors()); // reload the global schema (triggers reload for every view as well) m_rendererConfig->reloadSchema(); // force full update of all view caches and colors m_rendererConfig->updateConfig(); } void KTextEditor::EditorPrivate::copyToClipboard(const QString &text) { /** * empty => nop */ if (text.isEmpty()) { return; } /** * move to clipboard */ QApplication::clipboard()->setText(text, QClipboard::Clipboard); /** * LRU, kill potential duplicated, move new entry to top * cut after 10 entries */ m_clipboardHistory.removeOne(text); m_clipboardHistory.prepend(text); if (m_clipboardHistory.size() > 10) { m_clipboardHistory.removeLast(); } /** * notify about change */ emit clipboardHistoryChanged(); } bool KTextEditor::EditorPrivate::eventFilter(QObject *obj, QEvent *event) { if (obj == qApp && event->type() == QEvent::ApplicationPaletteChange) { // only update the color once for the event that belongs to the qApp updateColorPalette(); } return false; // always continue processing } QList KTextEditor::EditorPrivate::inputModeFactories() { return m_inputModeFactories.values(); } QStringListModel *KTextEditor::EditorPrivate::searchHistoryModel() { if (!m_searchHistoryModel) { KConfigGroup cg(KSharedConfig::openConfig(), "KTextEditor::Search"); const QStringList history = cg.readEntry(QStringLiteral("Search History"), QStringList()); m_searchHistoryModel = new QStringListModel(history, this); } return m_searchHistoryModel; } QStringListModel *KTextEditor::EditorPrivate::replaceHistoryModel() { if (!m_replaceHistoryModel) { KConfigGroup cg(KSharedConfig::openConfig(), "KTextEditor::Search"); const QStringList history = cg.readEntry(QStringLiteral("Replace History"), QStringList()); m_replaceHistoryModel = new QStringListModel(history, this); } return m_replaceHistoryModel; } void KTextEditor::EditorPrivate::saveSearchReplaceHistoryModels() { KConfigGroup cg(KSharedConfig::openConfig(), "KTextEditor::Search"); if (m_searchHistoryModel) { cg.writeEntry(QStringLiteral("Search History"), m_searchHistoryModel->stringList()); } if (m_replaceHistoryModel) { cg.writeEntry(QStringLiteral("Replace History"), m_replaceHistoryModel->stringList()); } } KSharedConfigPtr KTextEditor::EditorPrivate::config() { // use dummy config for unit tests! if (KTextEditor::EditorPrivate::unitTestMode()) { return KSharedConfig::openConfig(QStringLiteral("katepartrc-unittest"), KConfig::SimpleConfig, QStandardPaths::TempLocation); } // else: use application configuration, but try to transfer global settings on first use auto applicationConfig = KSharedConfig::openConfig(); if (!KConfigGroup(applicationConfig, QStringLiteral("KTextEditor Editor")).exists()) { auto globalConfig = KSharedConfig::openConfig(QStringLiteral("katepartrc")); for (const auto &group : {QStringLiteral("Editor"), QStringLiteral("Document"), QStringLiteral("View"), QStringLiteral("Renderer")}) { KConfigGroup origin(globalConfig, group); KConfigGroup destination(applicationConfig, QStringLiteral("KTextEditor ") + group); origin.copyTo(&destination); } } return applicationConfig; } diff --git a/src/utils/katevariableexpansionhelpers.cpp b/src/utils/katevariableexpansionhelpers.cpp index e3878695..8be40bd5 100644 --- a/src/utils/katevariableexpansionhelpers.cpp +++ b/src/utils/katevariableexpansionhelpers.cpp @@ -1,415 +1,415 @@ /* This file is part of the KDE project * * Copyright 2019 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katevariableexpansionhelpers.h" #include "kateglobal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * Find closing bracket for @p str starting a position @p pos. */ static int findClosing(QStringView str, int pos = 0) { const int len = str.size(); int nesting = 0; while (pos < len) { ++pos; const QChar c = str[pos]; if (c == QLatin1Char('}')) { if (nesting == 0) { return pos; } nesting--; } else if (c == QLatin1Char('{')) { nesting++; } } return -1; } namespace KateMacroExpander { QString expandMacro(const QString &input, KTextEditor::View *view) { QString output = input; QString oldStr; do { oldStr = output; const int startIndex = output.indexOf(QLatin1String("%{")); if (startIndex < 0) { break; } const int endIndex = findClosing(output, startIndex + 2); if (endIndex <= startIndex) { break; } const int varLen = endIndex - (startIndex + 2); QString variable = output.mid(startIndex + 2, varLen); variable = expandMacro(variable, view); if (KTextEditor::Editor::instance()->expandVariable(variable, view, variable)) { output.replace(startIndex, endIndex - startIndex + 1, variable); } } while (output != oldStr); // str comparison guards against infinite loop return output; } } class VariableItemModel : public QAbstractItemModel { public: VariableItemModel(QObject *parent = nullptr) : QAbstractItemModel(parent) { } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (parent.isValid() || row < 0 || row >= m_variables.size()) { return {}; } return createIndex(row, column); } QModelIndex parent(const QModelIndex &index) const override { Q_UNUSED(index) // flat list -> we never have parents return {}; } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_variables.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) return 3; // name | description | current value } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (!index.isValid()) { return {}; } const auto &var = m_variables[index.row()]; switch (role) { - case Qt::DisplayRole: { - const QString suffix = var.isPrefixMatch() ? i18n("") : QString(); - return QString(var.name() + suffix); - } - case Qt::ToolTipRole: - return var.description(); + case Qt::DisplayRole: { + const QString suffix = var.isPrefixMatch() ? i18n("") : QString(); + return QString(var.name() + suffix); + } + case Qt::ToolTipRole: + return var.description(); } return {}; } void setVariables(const QVector &variables) { beginResetModel(); m_variables = variables; endResetModel(); } private: QVector m_variables; }; class TextEditButton : public QToolButton { public: TextEditButton(QAction *showAction, QTextEdit *parent) : QToolButton(parent) { setAutoRaise(true); setDefaultAction(showAction); m_watched = parent->viewport(); m_watched->installEventFilter(this); show(); adjustPosition(m_watched->size()); } protected: void paintEvent(QPaintEvent *) override { // reimplement to have same behavior as actions in QLineEdits QStylePainter p(this); QStyleOptionToolButton opt; initStyleOption(&opt); opt.state = opt.state & ~QStyle::State_Raised; opt.state = opt.state & ~QStyle::State_MouseOver; opt.state = opt.state & ~QStyle::State_Sunken; p.drawComplexControl(QStyle::CC_ToolButton, opt); } public: bool eventFilter(QObject *watched, QEvent *event) override { if (watched == m_watched) { switch (event->type()) { - case QEvent::Resize: { - auto resizeEvent = static_cast(event); - adjustPosition(resizeEvent->size()); - } - default: - break; + case QEvent::Resize: { + auto resizeEvent = static_cast(event); + adjustPosition(resizeEvent->size()); + } + default: + break; } } return QToolButton::eventFilter(watched, event); } private: void adjustPosition(const QSize &parentSize) { QStyleOption sopt; sopt.initFrom(parentWidget()); const int topMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutTopMargin, &sopt, parentWidget()); const int rightMargin = 0; // style()->pixelMetric(QStyle::PM_LayoutRightMargin, &sopt, parentWidget()); if (isLeftToRight()) { move(parentSize.width() - width() - rightMargin, topMargin); } else { move(0, 0); } } private: QWidget *m_watched; }; KateVariableExpansionDialog::KateVariableExpansionDialog(QWidget *parent) : QDialog(parent, Qt::Tool) , m_showAction(new QAction(QIcon::fromTheme(QStringLiteral("code-context")), i18n("Insert variable"), this)) , m_variableModel(new VariableItemModel(this)) , m_listView(new QListView(this)) { setWindowTitle(i18n("Variables")); auto vbox = new QVBoxLayout(this); m_filterEdit = new QLineEdit(this); m_filterEdit->setPlaceholderText(i18n("Filter")); m_filterEdit->setFocus(); m_filterEdit->installEventFilter(this); vbox->addWidget(m_filterEdit); vbox->addWidget(m_listView); m_listView->setUniformItemSizes(true); m_filterModel = new QSortFilterProxyModel(this); m_filterModel->setFilterRole(Qt::DisplayRole); m_filterModel->setSortRole(Qt::DisplayRole); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setFilterKeyColumn(0); m_filterModel->setSourceModel(m_variableModel); m_listView->setModel(m_filterModel); connect(m_filterEdit, &QLineEdit::textChanged, m_filterModel, &QSortFilterProxyModel::setFilterWildcard); auto lblDescription = new QLabel(i18n("Please select a variable."), this); auto lblCurrentValue = new QLabel(this); vbox->addWidget(lblDescription); vbox->addWidget(lblCurrentValue); // react to selection changes connect(m_listView->selectionModel(), &QItemSelectionModel::currentRowChanged, [this, lblDescription, lblCurrentValue](const QModelIndex ¤t, const QModelIndex &) { if (current.isValid()) { const auto &var = m_variables[m_filterModel->mapToSource(current).row()]; lblDescription->setText(var.description()); if (var.isPrefixMatch()) { lblCurrentValue->setText(i18n("Current value: %1", var.name())); } else { auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); const auto value = var.evaluate(var.name(), activeView); lblCurrentValue->setText(i18n("Current value: %1", value)); } } else { lblDescription->setText(i18n("Please select a variable.")); lblCurrentValue->clear(); } }); // insert text on activation connect(m_listView, &QAbstractItemView::activated, [this, lblDescription, lblCurrentValue](const QModelIndex &index) { if (index.isValid()) { const auto &var = m_variables[m_filterModel->mapToSource(index).row()]; const auto name = QStringLiteral("%{") + var.name() + QLatin1Char('}'); if (parentWidget() && parentWidget()->window()) { auto currentWidget = parentWidget()->window()->focusWidget(); if (auto lineEdit = qobject_cast(currentWidget)) { lineEdit->insert(name); } else if (auto textEdit = qobject_cast(currentWidget)) { textEdit->insertPlainText(name); } } } }); // show dialog whenever the action is clicked connect(m_showAction, &QAction::triggered, [this]() { show(); activateWindow(); }); resize(400, 550); } KateVariableExpansionDialog::~KateVariableExpansionDialog() { for (auto it = m_textEditButtons.begin(); it != m_textEditButtons.end(); ++it) { if (it.value()) { delete it.value(); } } m_textEditButtons.clear(); } void KateVariableExpansionDialog::addVariable(const KTextEditor::Variable &variable) { Q_ASSERT(variable.isValid()); m_variables.push_back(variable); m_variableModel->setVariables(m_variables); } int KateVariableExpansionDialog::isEmpty() const { return m_variables.isEmpty(); } void KateVariableExpansionDialog::addWidget(QWidget *widget) { m_widgets.push_back(widget); widget->installEventFilter(this); connect(widget, &QObject::destroyed, this, &KateVariableExpansionDialog::onObjectDeleted); } void KateVariableExpansionDialog::onObjectDeleted(QObject *object) { m_widgets.removeAll(object); if (m_widgets.isEmpty()) { deleteLater(); } } bool KateVariableExpansionDialog::eventFilter(QObject *watched, QEvent *event) { // filter line edit if (watched == m_filterEdit) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp) || (keyEvent->key() == Qt::Key_PageDown) || (keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return); if (forward2list) { QCoreApplication::sendEvent(m_listView, event); return true; } } return QDialog::eventFilter(watched, event); } // tracked widgets (tooltips, adding/removing the showAction) switch (event->type()) { - case QEvent::FocusIn: { - if (auto lineEdit = qobject_cast(watched)) { - lineEdit->addAction(m_showAction, QLineEdit::TrailingPosition); - } else if (auto textEdit = qobject_cast(watched)) { - if (!m_textEditButtons.contains(textEdit)) { - m_textEditButtons[textEdit] = new TextEditButton(m_showAction, textEdit); - } - m_textEditButtons[textEdit]->raise(); - m_textEditButtons[textEdit]->show(); + case QEvent::FocusIn: { + if (auto lineEdit = qobject_cast(watched)) { + lineEdit->addAction(m_showAction, QLineEdit::TrailingPosition); + } else if (auto textEdit = qobject_cast(watched)) { + if (!m_textEditButtons.contains(textEdit)) { + m_textEditButtons[textEdit] = new TextEditButton(m_showAction, textEdit); } - break; + m_textEditButtons[textEdit]->raise(); + m_textEditButtons[textEdit]->show(); } - case QEvent::FocusOut: { - if (auto lineEdit = qobject_cast(watched)) { - lineEdit->removeAction(m_showAction); - } else if (auto textEdit = qobject_cast(watched)) { - if (m_textEditButtons.contains(textEdit)) { - delete m_textEditButtons[textEdit]; - m_textEditButtons.remove(textEdit); - } + break; + } + case QEvent::FocusOut: { + if (auto lineEdit = qobject_cast(watched)) { + lineEdit->removeAction(m_showAction); + } else if (auto textEdit = qobject_cast(watched)) { + if (m_textEditButtons.contains(textEdit)) { + delete m_textEditButtons[textEdit]; + m_textEditButtons.remove(textEdit); } - break; } - case QEvent::ToolTip: { - QString inputText; - if (auto lineEdit = qobject_cast(watched)) { - inputText = lineEdit->text(); - } - QString toolTip; - if (!inputText.isEmpty()) { - auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); - KTextEditor::Editor::instance()->expandText(inputText, activeView, toolTip); - } + break; + } + case QEvent::ToolTip: { + QString inputText; + if (auto lineEdit = qobject_cast(watched)) { + inputText = lineEdit->text(); + } + QString toolTip; + if (!inputText.isEmpty()) { + auto activeView = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView(); + KTextEditor::Editor::instance()->expandText(inputText, activeView, toolTip); + } - if (!toolTip.isEmpty()) { - auto helpEvent = static_cast(event); - QToolTip::showText(helpEvent->globalPos(), toolTip, qobject_cast(watched)); - event->accept(); - return true; - } - break; + if (!toolTip.isEmpty()) { + auto helpEvent = static_cast(event); + QToolTip::showText(helpEvent->globalPos(), toolTip, qobject_cast(watched)); + event->accept(); + return true; } - default: - break; + break; + } + default: + break; } // auto-hide on focus change auto parentWindow = parentWidget()->window(); const bool keepVisible = isActiveWindow() || m_widgets.contains(parentWindow->focusWidget()); if (!keepVisible) { hide(); } return QDialog::eventFilter(watched, event); } // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/src/utils/range.cpp b/src/utils/range.cpp index 62216011..93847706 100644 --- a/src/utils/range.cpp +++ b/src/utils/range.cpp @@ -1,120 +1,120 @@ /* This file is part of the KDE libraries * Copyright (C) 2016 Dominik Haumann * Copyright (C) 2007 Mirko Stocker * Copyright (C) 2003-2005 Hamish Rodda * Copyright (C) 2002 Christian Couder * Copyright (C) 2001, 2003 Christoph Cullmann * Copyright (C) 2001 Joseph Wenninger * Copyright (C) 1999 Jochen Wilhelmy * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "range.h" using namespace KTextEditor; Range Range::fromString(const QStringRef &str) Q_DECL_NOEXCEPT { const int startIndex = str.indexOf(QLatin1Char('[')); const int endIndex = str.indexOf(QLatin1Char(']')); const int closeIndex = str.indexOf(QLatin1Char(')')); // end of first cursor if (startIndex < 0 || endIndex < 0 || closeIndex < 0 || closeIndex < startIndex || endIndex < closeIndex || endIndex < startIndex) { return invalid(); } return Range(Cursor::fromString(str.mid(startIndex + 1, closeIndex - startIndex)), Cursor::fromString(str.mid(closeIndex + 2, endIndex - closeIndex - 2))); } void Range::setRange(const Range &range) Q_DECL_NOEXCEPT { m_start = range.start(); m_end = range.end(); } void Range::setRange(const Cursor &start, const Cursor &end) Q_DECL_NOEXCEPT { if (start > end) { setRange(Range(end, start)); } else { setRange(Range(start, end)); } } bool Range::confineToRange(const Range &range) Q_DECL_NOEXCEPT { if (start() < range.start()) if (end() > range.end()) { setRange(range); } else { setStart(range.start()); } else if (end() > range.end()) { setEnd(range.end()); } else { return false; } return true; } bool Range::expandToRange(const Range &range) Q_DECL_NOEXCEPT { if (start() > range.start()) if (end() < range.end()) { setRange(range); } else { setStart(range.start()); } else if (end() < range.end()) { setEnd(range.end()); } else { return false; } return true; } void Range::setBothLines(int line) Q_DECL_NOEXCEPT { setRange(Range(line, start().column(), line, end().column())); } void KTextEditor::Range::setBothColumns(int column) Q_DECL_NOEXCEPT { setRange(Range(start().line(), column, end().line(), column)); } namespace QTest { // Cursor: template specialization for QTest::toString() -template <> char *toString(const KTextEditor::Cursor &cursor) +template<> char *toString(const KTextEditor::Cursor &cursor) { QByteArray ba = "Cursor[" + QByteArray::number(cursor.line()) + ", " + QByteArray::number(cursor.column()) + ']'; return qstrdup(ba.data()); } // Range: template specialization for QTest::toString() -template <> char *toString(const KTextEditor::Range &range) +template<> char *toString(const KTextEditor::Range &range) { QByteArray ba = "Range["; ba += QByteArray::number(range.start().line()) + ", " + QByteArray::number(range.start().column()) + " - "; ba += QByteArray::number(range.end().line()) + ", " + QByteArray::number(range.end().column()); ba += ']'; return qstrdup(ba.data()); } } diff --git a/src/view/katemessagewidget.cpp b/src/view/katemessagewidget.cpp index eef6455b..ec091614 100644 --- a/src/view/katemessagewidget.cpp +++ b/src/view/katemessagewidget.cpp @@ -1,288 +1,288 @@ /* This file is part of the KDE and the Kate project * * Copyright (C) 2012 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katemessagewidget.h" #include "katepartdebug.h" #include #include #include #include #include #include #include #include static const int s_defaultAutoHideTime = 6 * 1000; KateMessageWidget::KateMessageWidget(QWidget *parent, bool applyFadeEffect) : QWidget(parent) , m_animation(nullptr) , m_autoHideTimer(new QTimer(this)) , m_autoHideTime(-1) { QVBoxLayout *l = new QVBoxLayout(); l->setContentsMargins(0, 0, 0, 0); m_messageWidget = new KMessageWidget(this); m_messageWidget->setCloseButtonVisible(false); l->addWidget(m_messageWidget); setLayout(l); // tell the widget to always use the minimum size. setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); // install event filter so we catch the end of the hide animation m_messageWidget->installEventFilter(this); // by default, hide widgets m_messageWidget->hide(); hide(); // create animation controller, and connect widgetHidden() to showNextMessage() m_animation = new KateAnimation(m_messageWidget, applyFadeEffect ? KateAnimation::FadeEffect : KateAnimation::GrowEffect); connect(m_animation, SIGNAL(widgetHidden()), this, SLOT(showNextMessage())); // setup autoHide timer details m_autoHideTimer->setSingleShot(true); connect(m_messageWidget, SIGNAL(linkHovered(QString)), SLOT(linkHovered(QString))); } void KateMessageWidget::showNextMessage() { // at this point, we should not have a currently shown message Q_ASSERT(m_currentMessage == nullptr); // if not message to show, just stop if (m_messageQueue.size() == 0) { hide(); return; } // track current message m_currentMessage = m_messageQueue[0]; // set text etc. m_messageWidget->setText(m_currentMessage->text()); m_messageWidget->setIcon(m_currentMessage->icon()); // connect textChanged() and iconChanged(), so it's possible to change this on the fly connect(m_currentMessage, SIGNAL(textChanged(QString)), m_messageWidget, SLOT(setText(QString)), Qt::UniqueConnection); connect(m_currentMessage, SIGNAL(iconChanged(QIcon)), m_messageWidget, SLOT(setIcon(QIcon)), Qt::UniqueConnection); // the enums values do not necessarily match, hence translate with switch switch (m_currentMessage->messageType()) { - case KTextEditor::Message::Positive: - m_messageWidget->setMessageType(KMessageWidget::Positive); - break; - case KTextEditor::Message::Information: - m_messageWidget->setMessageType(KMessageWidget::Information); - break; - case KTextEditor::Message::Warning: - m_messageWidget->setMessageType(KMessageWidget::Warning); - break; - case KTextEditor::Message::Error: - m_messageWidget->setMessageType(KMessageWidget::Error); - break; - default: - m_messageWidget->setMessageType(KMessageWidget::Information); - break; + case KTextEditor::Message::Positive: + m_messageWidget->setMessageType(KMessageWidget::Positive); + break; + case KTextEditor::Message::Information: + m_messageWidget->setMessageType(KMessageWidget::Information); + break; + case KTextEditor::Message::Warning: + m_messageWidget->setMessageType(KMessageWidget::Warning); + break; + case KTextEditor::Message::Error: + m_messageWidget->setMessageType(KMessageWidget::Error); + break; + default: + m_messageWidget->setMessageType(KMessageWidget::Information); + break; } // remove all actions from the message widget const auto messageWidgetActions = m_messageWidget->actions(); for (QAction *a : messageWidgetActions) { m_messageWidget->removeAction(a); } // add new actions to the message widget const auto m_currentMessageActions = m_currentMessage->actions(); for (QAction *a : m_currentMessageActions) { m_messageWidget->addAction(a); } // set word wrap of the message setWordWrap(m_currentMessage); // setup auto-hide timer, and start if requested m_autoHideTime = m_currentMessage->autoHide(); m_autoHideTimer->stop(); if (m_autoHideTime >= 0) { connect(m_autoHideTimer, SIGNAL(timeout()), m_currentMessage, SLOT(deleteLater()), Qt::UniqueConnection); if (m_currentMessage->autoHideMode() == KTextEditor::Message::Immediate) { m_autoHideTimer->start(m_autoHideTime == 0 ? s_defaultAutoHideTime : m_autoHideTime); } } // finally show show(); m_animation->show(); } void KateMessageWidget::setWordWrap(KTextEditor::Message *message) { // want word wrap anyway? -> ok if (message->wordWrap()) { m_messageWidget->setWordWrap(message->wordWrap()); return; } // word wrap not wanted, that's ok if a parent widget does not exist if (!parentWidget()) { m_messageWidget->setWordWrap(false); return; } // word wrap not wanted -> enable word wrap if it breaks the layout otherwise int margin = 0; if (parentWidget()->layout()) { // get left/right margin of the layout, since we need to subtract these int leftMargin = 0, rightMargin = 0; parentWidget()->layout()->getContentsMargins(&leftMargin, nullptr, &rightMargin, nullptr); margin = leftMargin + rightMargin; } // if word wrap enabled, first disable it if (m_messageWidget->wordWrap()) { m_messageWidget->setWordWrap(false); } // make sure the widget's size is up-to-date in its hidden state m_messageWidget->ensurePolished(); m_messageWidget->adjustSize(); // finally enable word wrap, if there is not enough free horizontal space const int freeSpace = (parentWidget()->width() - margin) - m_messageWidget->width(); if (freeSpace < 0) { // qCDebug(LOG_KTE) << "force word wrap to avoid breaking the layout" << freeSpace; m_messageWidget->setWordWrap(true); } } void KateMessageWidget::postMessage(KTextEditor::Message *message, QList> actions) { Q_ASSERT(!m_messageHash.contains(message)); m_messageHash[message] = std::move(actions); // insert message sorted after priority int i = 0; for (; i < m_messageQueue.count(); ++i) { if (message->priority() > m_messageQueue[i]->priority()) { break; } } // queue message m_messageQueue.insert(i, message); // catch if the message gets deleted connect(message, SIGNAL(closed(KTextEditor::Message *)), SLOT(messageDestroyed(KTextEditor::Message *))); if (i == 0 && !m_animation->isHideAnimationRunning()) { // if message has higher priority than the one currently shown, // then hide the current one and then show the new one. if (m_currentMessage) { // autoHide timer may be running for currently shown message, therefore // simply disconnect autoHide timer to all timeout() receivers disconnect(m_autoHideTimer, SIGNAL(timeout()), nullptr, nullptr); m_autoHideTimer->stop(); // if there is a current message, the message queue must contain 2 messages Q_ASSERT(m_messageQueue.size() > 1); Q_ASSERT(m_currentMessage == m_messageQueue[1]); // a bit unnice: disconnect textChanged() and iconChanged() signals of previously visible message disconnect(m_currentMessage, SIGNAL(textChanged(QString)), m_messageWidget, SLOT(setText(QString))); disconnect(m_currentMessage, SIGNAL(iconChanged(QIcon)), m_messageWidget, SLOT(setIcon(QIcon))); m_currentMessage = nullptr; m_animation->hide(); } else { showNextMessage(); } } } void KateMessageWidget::messageDestroyed(KTextEditor::Message *message) { // last moment when message is valid, since KTE::Message is already in // destructor we have to do the following: // 1. remove message from m_messageQueue, so we don't care about it anymore // 2. activate hide animation or show a new message() // remove widget from m_messageQueue int i = 0; for (; i < m_messageQueue.count(); ++i) { if (m_messageQueue[i] == message) { break; } } // the message must be in the list Q_ASSERT(i < m_messageQueue.count()); // remove message m_messageQueue.removeAt(i); // remove message from hash -> release QActions Q_ASSERT(m_messageHash.contains(message)); m_messageHash.remove(message); // if deleted message is the current message, launch hide animation if (message == m_currentMessage) { m_currentMessage = nullptr; m_animation->hide(); } } void KateMessageWidget::startAutoHideTimer() { // message does not want autohide, or timer already running if (!m_currentMessage // no message, nothing to do || m_autoHideTime < 0 // message does not want auto-hide || m_autoHideTimer->isActive() // auto-hide timer is already active || m_animation->isHideAnimationRunning() // widget is in hide animation phase || m_animation->isShowAnimationRunning() // widget is in show animation phase ) { return; } // safety checks: the message must still be valid Q_ASSERT(m_messageQueue.size()); Q_ASSERT(m_currentMessage->autoHide() == m_autoHideTime); // start autoHide timer as requested m_autoHideTimer->start(m_autoHideTime == 0 ? s_defaultAutoHideTime : m_autoHideTime); } void KateMessageWidget::linkHovered(const QString &link) { QToolTip::showText(QCursor::pos(), link, m_messageWidget); } QString KateMessageWidget::text() const { return m_messageWidget->text(); } diff --git a/src/view/katestatusbar.cpp b/src/view/katestatusbar.cpp index cfe78084..24d53659 100644 --- a/src/view/katestatusbar.cpp +++ b/src/view/katestatusbar.cpp @@ -1,588 +1,588 @@ /* This file is part of the KDE and the Kate project * * Copyright (C) 2013 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katestatusbar.h" #include "kateabstractinputmode.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katemodemanager.h" #include "katemodemenulist.h" #include "wordcounter.h" #include #include #include #include #include #include #include // BEGIN menu KateStatusBarOpenUpMenu::KateStatusBarOpenUpMenu(QWidget *parent) : QMenu(parent) { } KateStatusBarOpenUpMenu::~KateStatusBarOpenUpMenu() { } void KateStatusBarOpenUpMenu::setVisible(bool visibility) { if (visibility) { QRect geo = geometry(); QPoint pos = parentWidget()->mapToGlobal(QPoint(0, 0)); geo.moveTopLeft(QPoint(pos.x(), pos.y() - geo.height())); if (geo.top() < 0) geo.moveTop(0); setGeometry(geo); } QMenu::setVisible(visibility); } // END menu // BEGIN StatusBarButton StatusBarButton::StatusBarButton(KateStatusBar *parent, const QString &text /*= QString()*/) : QPushButton(text, parent) { setFlat(true); setFocusProxy(parent->m_view); setMinimumSize(QSize(1, minimumSizeHint().height())); } StatusBarButton::~StatusBarButton() { } // END StatusBarButton KateStatusBar::KateStatusBar(KTextEditor::ViewPrivate *view) : KateViewBarWidget(false) , m_view(view) , m_modifiedStatus(-1) , m_selectionMode(-1) , m_wordCounter(nullptr) { KAcceleratorManager::setNoAccel(this); setFocusProxy(m_view); /** * just add our status bar to central widget, full sized */ QHBoxLayout *topLayout = new QHBoxLayout(centralWidget()); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(4); /** * show modification state of the document * TODO Using a (StatusBar)Button is currently pointless but handy due to no "setIcon()" function in QLabel. * Add some useful action when button is clicked, e.g. save document or show tool-tip * or find a way to not show a "focus frame" when hovered by mouse */ m_modified = new StatusBarButton(this); topLayout->addWidget(m_modified); /** * show Line XXX, Column XXX */ m_cursorPosition = new StatusBarButton(this); topLayout->addWidget(m_cursorPosition); m_cursorPosition->setWhatsThis(i18n("Current cursor position. Click to go to a specific line.")); connect(m_cursorPosition, &StatusBarButton::clicked, m_view, &KTextEditor::ViewPrivate::gotoLine); // Separate the status line in a left and right part topLayout->addStretch(1); /** * show the current mode, like INSERT, OVERWRITE, VI + modifiers like [BLOCK] */ m_inputMode = new StatusBarButton(this); topLayout->addWidget(m_inputMode); m_inputMode->setWhatsThis(i18n("Insert mode and VI input mode indicator. Click to change the mode.")); connect(m_inputMode, &StatusBarButton::clicked, [=] { m_view->currentInputMode()->toggleInsert(); }); /** * Add dictionary button which allows user to switch dictionary of the document */ m_dictionary = new StatusBarButton(this); topLayout->addWidget(m_dictionary, 0); m_dictionary->setWhatsThis(i18n("Change dictionary")); m_dictionaryMenu = new KateStatusBarOpenUpMenu(m_dictionary); m_dictionaryMenu->addAction(m_view->action("tools_change_dictionary")); m_dictionaryMenu->addAction(m_view->action("tools_clear_dictionary_ranges")); m_dictionaryMenu->addAction(m_view->action("tools_toggle_automatic_spell_checking")); m_dictionaryMenu->addAction(m_view->action("tools_spelling_from_cursor")); m_dictionaryMenu->addAction(m_view->action("tools_spelling")); m_dictionaryMenu->addSeparator(); m_dictionaryGroup = new QActionGroup(m_dictionaryMenu); QMapIterator i(Sonnet::Speller().preferredDictionaries()); while (i.hasNext()) { i.next(); QAction *action = m_dictionaryGroup->addAction(i.key()); action->setData(i.value()); action->setToolTip(i.key()); action->setCheckable(true); m_dictionaryMenu->addAction(action); } m_dictionary->setMenu(m_dictionaryMenu); connect(m_dictionaryGroup, &QActionGroup::triggered, this, &KateStatusBar::changeDictionary); /** * allow to change indentation configuration */ m_tabsIndent = new StatusBarButton(this); topLayout->addWidget(m_tabsIndent); m_indentSettingsMenu = new KateStatusBarOpenUpMenu(m_tabsIndent); m_indentSettingsMenu->addSection(i18n("Tab Width")); m_tabGroup = new QActionGroup(this); addNumberAction(m_tabGroup, m_indentSettingsMenu, -1); addNumberAction(m_tabGroup, m_indentSettingsMenu, 8); addNumberAction(m_tabGroup, m_indentSettingsMenu, 4); addNumberAction(m_tabGroup, m_indentSettingsMenu, 2); connect(m_tabGroup, &QActionGroup::triggered, this, &KateStatusBar::slotTabGroup); m_indentSettingsMenu->addSection(i18n("Indentation Width")); m_indentGroup = new QActionGroup(this); addNumberAction(m_indentGroup, m_indentSettingsMenu, -1); addNumberAction(m_indentGroup, m_indentSettingsMenu, 8); addNumberAction(m_indentGroup, m_indentSettingsMenu, 4); addNumberAction(m_indentGroup, m_indentSettingsMenu, 2); connect(m_indentGroup, &QActionGroup::triggered, this, &KateStatusBar::slotIndentGroup); m_indentSettingsMenu->addSection(i18n("Indentation Mode")); QActionGroup *radioGroup = new QActionGroup(m_indentSettingsMenu); m_mixedAction = m_indentSettingsMenu->addAction(i18n("Tabulators && Spaces")); m_mixedAction->setCheckable(true); m_mixedAction->setActionGroup(radioGroup); m_hardAction = m_indentSettingsMenu->addAction(i18n("Tabulators")); m_hardAction->setCheckable(true); m_hardAction->setActionGroup(radioGroup); m_softAction = m_indentSettingsMenu->addAction(i18n("Spaces")); m_softAction->setCheckable(true); m_softAction->setActionGroup(radioGroup); connect(radioGroup, &QActionGroup::triggered, this, &KateStatusBar::slotIndentTabMode); m_tabsIndent->setMenu(m_indentSettingsMenu); /** * add encoding button which allows user to switch encoding of document * this will reuse the encoding action menu of the view */ m_encoding = new StatusBarButton(this); topLayout->addWidget(m_encoding); m_encoding->setMenu(m_view->encodingAction()->menu()); m_encoding->setWhatsThis(i18n("Encoding")); /** * load the mode menu, which contains a scrollable list + search bar. * This is an alternative menu to the mode action menu of the view. */ KateModeMenuList *modeMenuList = new KateModeMenuList(i18n("Mode"), this); modeMenuList->setWhatsThis(i18n("Here you can choose which mode should be used for the current document. This will influence the highlighting and folding being used, for example.")); modeMenuList->updateMenu(m_view->doc()); /** * add mode button which allows user to switch mode of document */ m_mode = new StatusBarButton(this); topLayout->addWidget(m_mode); modeMenuList->setButton(m_mode, KateModeMenuList::AlignHInverse, KateModeMenuList::AlignTop, KateModeMenuList::AutoUpdateTextButton(false)); m_mode->setMenu(modeMenuList); m_mode->setWhatsThis(i18n("Syntax highlighting")); // signals for the statusbar connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateStatusBar::cursorPositionChanged); connect(m_view, &KTextEditor::View::viewModeChanged, this, &KateStatusBar::viewModeChanged); connect(m_view, &KTextEditor::View::selectionChanged, this, &KateStatusBar::selectionChanged); connect(m_view->document(), &KTextEditor::DocumentPrivate::modifiedChanged, this, &KateStatusBar::modifiedChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::modifiedOnDisk, this, &KateStatusBar::modifiedChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::readWriteChanged, this, &KateStatusBar::modifiedChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::configChanged, this, &KateStatusBar::documentConfigChanged); connect(m_view->document(), &KTextEditor::DocumentPrivate::modeChanged, this, &KateStatusBar::modeChanged); connect(m_view, &KTextEditor::ViewPrivate::configChanged, this, &KateStatusBar::configChanged); connect(m_view->doc(), &KTextEditor::DocumentPrivate::defaultDictionaryChanged, this, &KateStatusBar::updateDictionary); connect(m_view->doc(), &KTextEditor::DocumentPrivate::dictionaryRangesPresent, this, &KateStatusBar::updateDictionary); connect(m_view, &KTextEditor::ViewPrivate::caretChangedRange, this, &KateStatusBar::updateDictionary); updateStatus(); toggleWordCount(KateViewConfig::global()->showWordCount()); } bool KateStatusBar::eventFilter(QObject *obj, QEvent *event) { return KateViewBarWidget::eventFilter(obj, event); } void KateStatusBar::contextMenuEvent(QContextMenuEvent *event) { // TODO Add option "Show Statusbar" and options to show/hide buttons of the status bar QMenu menu(this); if (childAt(event->pos()) == m_inputMode) { if (QAction *inputModesAction = m_view->actionCollection()->action(QStringLiteral("view_input_modes"))) { if (QMenu *inputModesMenu = inputModesAction->menu()) { const auto actions = inputModesMenu->actions(); for (int i = 0; i < actions.count(); ++i) { menu.addAction(actions.at(i)); } menu.addSeparator(); } } } QAction *showLines = menu.addAction(QStringLiteral("Show line count"), this, &KateStatusBar::toggleShowLines); showLines->setCheckable(true); showLines->setChecked(KateViewConfig::global()->showLineCount()); QAction *showWords = menu.addAction(QStringLiteral("Show word count"), this, &KateStatusBar::toggleShowWords); showWords->setCheckable(true); showWords->setChecked(KateViewConfig::global()->showWordCount()); menu.exec(event->globalPos()); } void KateStatusBar::toggleShowLines(bool checked) { KateViewConfig::global()->setValue(KateViewConfig::ShowLineCount, checked); } void KateStatusBar::toggleShowWords(bool checked) { KateViewConfig::global()->setShowWordCount(checked); } void KateStatusBar::updateStatus() { selectionChanged(); viewModeChanged(); cursorPositionChanged(); modifiedChanged(); documentConfigChanged(); modeChanged(); updateDictionary(); } void KateStatusBar::selectionChanged() { const unsigned int newSelectionMode = m_view->blockSelection(); if (newSelectionMode == m_selectionMode) { return; } // remember new mode and update info m_selectionMode = newSelectionMode; viewModeChanged(); } void KateStatusBar::viewModeChanged() { // prepend BLOCK for block selection mode QString text = m_view->viewModeHuman(); if (m_view->blockSelection()) text = i18n("[BLOCK] %1", text); m_inputMode->setText(text); } void KateStatusBar::cursorPositionChanged() { KTextEditor::Cursor position(m_view->cursorPositionVirtual()); // Update line/column label QString text; if (KateViewConfig::global()->showLineCount()) { text = i18n("Line %1 of %2, Column %3", QLocale().toString(position.line() + 1), QLocale().toString(m_view->doc()->lines()), QLocale().toString(position.column() + 1)); } else { text = i18n("Line %1, Column %2", QLocale().toString(position.line() + 1), QLocale().toString(position.column() + 1)); } if (m_wordCounter) { text.append(QLatin1String(", ") + m_wordCount); } m_cursorPosition->setText(text); } void KateStatusBar::updateDictionary() { QString newDict; // Check if at the current cursor position is a special dictionary in use KTextEditor::Cursor position(m_view->cursorPositionVirtual()); const QList> dictRanges = m_view->doc()->dictionaryRanges(); for (const auto &rangeDictPair : dictRanges) { const KTextEditor::MovingRange *range = rangeDictPair.first; if (range->contains(position) || range->end() == position) { newDict = rangeDictPair.second; break; } } // Check if the default dictionary is in use if (newDict.isEmpty()) { newDict = m_view->doc()->defaultDictionary(); if (newDict.isEmpty()) { newDict = Sonnet::Speller().defaultLanguage(); } } // Update button and menu only on a changed dictionary if (!m_dictionaryGroup->checkedAction() || (m_dictionaryGroup->checkedAction()->data().toString() != newDict) || m_dictionary->text().isEmpty()) { bool found = false; // Remove "-w_accents -variant_0" and such from dict-code to keep it small and clean m_dictionary->setText(newDict.section(QLatin1Char('-'), 0, 0)); // For maximum user clearness, change the checked menu option m_dictionaryGroup->blockSignals(true); for (auto a : m_dictionaryGroup->actions()) { if (a->data().toString() == newDict) { a->setChecked(true); found = true; break; } } if (!found) { // User has chose some other dictionary from combo box, we need to add that QString dictName = Sonnet::Speller().availableDictionaries().key(newDict); QAction *action = m_dictionaryGroup->addAction(dictName); action->setData(newDict); action->setCheckable(true); action->setChecked(true); m_dictionaryMenu->addAction(action); } m_dictionaryGroup->blockSignals(false); } } void KateStatusBar::modifiedChanged() { const bool mod = m_view->doc()->isModified(); const bool modOnHD = m_view->doc()->isModifiedOnDisc(); const bool readOnly = !m_view->doc()->isReadWrite(); /** * combine to modified status, update only if changed */ unsigned int newStatus = (unsigned int)mod | ((unsigned int)modOnHD << 1) | ((unsigned int)readOnly << 2); if (m_modifiedStatus == newStatus) return; m_modifiedStatus = newStatus; switch (m_modifiedStatus) { - case 0x0: - m_modified->setIcon(QIcon::fromTheme(QStringLiteral("text-plain"))); - m_modified->setWhatsThis(i18n("Meaning of current icon: Document was not modified since it was loaded")); - break; - - case 0x1: - case 0x5: - m_modified->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); - m_modified->setWhatsThis(i18n("Meaning of current icon: Document was modified since it was loaded")); - break; - - case 0x2: - case 0x6: - m_modified->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); - m_modified->setWhatsThis(i18n("Meaning of current icon: Document was modified or deleted by another program")); - break; - - case 0x3: - case 0x7: - m_modified->setIcon(QIcon(KIconUtils::addOverlay(QIcon::fromTheme(QStringLiteral("document-save")), QIcon(QStringLiteral("emblem-important")), Qt::TopLeftCorner))); - m_modified->setWhatsThis(QString()); - break; - - default: - m_modified->setIcon(QIcon::fromTheme(QStringLiteral("lock"))); - m_modified->setWhatsThis(i18n("Meaning of current icon: Document is in read-only mode")); - break; + case 0x0: + m_modified->setIcon(QIcon::fromTheme(QStringLiteral("text-plain"))); + m_modified->setWhatsThis(i18n("Meaning of current icon: Document was not modified since it was loaded")); + break; + + case 0x1: + case 0x5: + m_modified->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); + m_modified->setWhatsThis(i18n("Meaning of current icon: Document was modified since it was loaded")); + break; + + case 0x2: + case 0x6: + m_modified->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + m_modified->setWhatsThis(i18n("Meaning of current icon: Document was modified or deleted by another program")); + break; + + case 0x3: + case 0x7: + m_modified->setIcon(QIcon(KIconUtils::addOverlay(QIcon::fromTheme(QStringLiteral("document-save")), QIcon(QStringLiteral("emblem-important")), Qt::TopLeftCorner))); + m_modified->setWhatsThis(QString()); + break; + + default: + m_modified->setIcon(QIcon::fromTheme(QStringLiteral("lock"))); + m_modified->setWhatsThis(i18n("Meaning of current icon: Document is in read-only mode")); + break; } } void KateStatusBar::documentConfigChanged() { m_encoding->setText(m_view->document()->encoding()); KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); int tabWidth = config->tabWidth(); int indentationWidth = config->indentationWidth(); bool replaceTabsDyn = config->replaceTabsDyn(); static const KLocalizedString spacesOnly = ki18n("Soft Tabs: %1"); static const KLocalizedString spacesOnlyShowTabs = ki18n("Soft Tabs: %1 (%2)"); static const KLocalizedString tabsOnly = ki18n("Tab Size: %1"); static const KLocalizedString tabSpacesMixed = ki18n("Indent/Tab: %1/%2"); if (!replaceTabsDyn) { if (tabWidth == indentationWidth) { m_tabsIndent->setText(tabsOnly.subs(tabWidth).toString()); m_tabGroup->setEnabled(false); m_hardAction->setChecked(true); } else { m_tabsIndent->setText(tabSpacesMixed.subs(indentationWidth).subs(tabWidth).toString()); m_tabGroup->setEnabled(true); m_mixedAction->setChecked(true); } } else { if (tabWidth == indentationWidth) { m_tabsIndent->setText(spacesOnly.subs(indentationWidth).toString()); m_tabGroup->setEnabled(true); m_softAction->setChecked(true); } else { m_tabsIndent->setText(spacesOnlyShowTabs.subs(indentationWidth).subs(tabWidth).toString()); m_tabGroup->setEnabled(true); m_softAction->setChecked(true); } } updateGroup(m_tabGroup, tabWidth); updateGroup(m_indentGroup, indentationWidth); } void KateStatusBar::modeChanged() { m_mode->setText(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_view->document()->mode()).nameTranslated()); } void KateStatusBar::addNumberAction(QActionGroup *group, QMenu *menu, int data) { QAction *a; if (data != -1) { a = menu->addAction(QStringLiteral("%1").arg(data)); } else { a = menu->addAction(i18n("Other...")); } a->setData(data); a->setCheckable(true); a->setActionGroup(group); } void KateStatusBar::updateGroup(QActionGroup *group, int w) { QAction *m1 = nullptr; bool found = false; // linear search should be fast enough here, no additional hash for (QAction *action : group->actions()) { int val = action->data().toInt(); if (val == -1) m1 = action; if (val == w) { found = true; action->setChecked(true); } } if (found) { m1->setText(i18n("Other...")); } else { m1->setText(i18np("Other (%1)", "Other (%1)", w)); m1->setChecked(true); } } void KateStatusBar::slotTabGroup(QAction *a) { int val = a->data().toInt(); bool ok; KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); if (val == -1) { val = QInputDialog::getInt(this, i18n("Tab Width"), i18n("Please specify the wanted tab width:"), config->tabWidth(), 1, 16, 1, &ok); if (!ok) val = config->tabWidth(); } config->setTabWidth(val); } void KateStatusBar::slotIndentGroup(QAction *a) { int val = a->data().toInt(); bool ok; KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); if (val == -1) { val = QInputDialog::getInt(this, i18n("Indentation Width"), i18n("Please specify the wanted indentation width:"), config->indentationWidth(), 1, 16, 1, &ok); if (!ok) val = config->indentationWidth(); } config->configStart(); config->setIndentationWidth(val); if (m_hardAction->isChecked()) config->setTabWidth(val); config->configEnd(); } void KateStatusBar::slotIndentTabMode(QAction *a) { KateDocumentConfig *config = ((KTextEditor::DocumentPrivate *)m_view->document())->config(); if (a == m_softAction) { config->setReplaceTabsDyn(true); } else if (a == m_mixedAction) { if (config->replaceTabsDyn()) config->setReplaceTabsDyn(false); m_tabGroup->setEnabled(true); } else if (a == m_hardAction) { if (config->replaceTabsDyn()) { config->configStart(); config->setReplaceTabsDyn(false); config->setTabWidth(config->indentationWidth()); config->configEnd(); } else { config->setTabWidth(config->indentationWidth()); } m_tabGroup->setEnabled(false); } } void KateStatusBar::toggleWordCount(bool on) { if ((m_wordCounter != nullptr) == on) { return; } if (on) { m_wordCounter = new WordCounter(m_view); connect(m_wordCounter, &WordCounter::changed, this, &KateStatusBar::wordCountChanged); } else { delete m_wordCounter; m_wordCounter = nullptr; } wordCountChanged(0, 0, 0, 0); } void KateStatusBar::wordCountChanged(int wordsInDocument, int wordsInSelection, int charsInDocument, int charsInSelection) { if (m_wordCounter) { m_wordCount = i18nc("%1 and %3 are the selected words/chars count, %2 and %4 are the total words/chars count.", "Words %1/%2, Chars %3/%4", wordsInSelection, wordsInDocument, charsInSelection, charsInDocument); } else { m_wordCount.clear(); } cursorPositionChanged(); } void KateStatusBar::configChanged() { toggleWordCount(m_view->config()->showWordCount()); } void KateStatusBar::changeDictionary(QAction *action) { const QString dictionary = action->data().toString(); m_dictionary->setText(dictionary); // Code stolen from KateDictionaryBar::dictionaryChanged KTextEditor::Range selection = m_view->selectionRange(); if (selection.isValid() && !selection.isEmpty()) { m_view->doc()->setDictionary(dictionary, selection); } else { m_view->doc()->setDefaultDictionary(dictionary); } } diff --git a/src/view/kateview.cpp b/src/view/kateview.cpp index 7d278c7b..832a48dc 100644 --- a/src/view/kateview.cpp +++ b/src/view/kateview.cpp @@ -1,3935 +1,3935 @@ /* This file is part of the KDE libraries Copyright (C) 2009 Michel Ludwig Copyright (C) 2007 Mirko Stocker Copyright (C) 2003 Hamish Rodda Copyright (C) 2002 John Firebaugh Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001-2010 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // BEGIN includes #include "kateview.h" #include "export/exporter.h" #include "inlinenotedata.h" #include "kateabstractinputmode.h" #include "kateautoindent.h" #include "katebookmarks.h" #include "katebuffer.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "katedialogs.h" #include "katedocument.h" #include "kateglobal.h" #include "katehighlight.h" #include "katehighlightmenu.h" #include "katekeywordcompletion.h" #include "katelayoutcache.h" #include "katemessagewidget.h" #include "katemodemenu.h" #include "katepartdebug.h" #include "katerenderer.h" #include "kateschema.h" #include "katestatusbar.h" #include "katetemplatehandler.h" #include "katetextline.h" #include "kateundomanager.h" #include "kateviewhelpers.h" #include "kateviewinternal.h" #include "katewordcompletion.h" #include "printing/kateprinter.h" #include "script/katescriptaction.h" #include "script/katescriptmanager.h" #include "spellcheck/spellcheck.h" #include "spellcheck/spellcheckdialog.h" #include "spellcheck/spellingmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define VIEW_RANGE_DEBUG // END includes namespace { bool hasCommentInFirstLine(KTextEditor::DocumentPrivate *doc) { const Kate::TextLine &line = doc->kateTextLine(0); Q_ASSERT(line); return doc->isComment(0, line->firstChar()); } } void KTextEditor::ViewPrivate::blockFix(KTextEditor::Range &range) { if (range.start().column() > range.end().column()) { int tmp = range.start().column(); range.setStart(KTextEditor::Cursor(range.start().line(), range.end().column())); range.setEnd(KTextEditor::Cursor(range.end().line(), tmp)); } } KTextEditor::ViewPrivate::ViewPrivate(KTextEditor::DocumentPrivate *doc, QWidget *parent, KTextEditor::MainWindow *mainWindow) : KTextEditor::View(this, parent) , m_completionWidget(nullptr) , m_annotationModel(nullptr) , m_hasWrap(false) , m_doc(doc) , m_textFolding(doc->buffer()) , m_config(new KateViewConfig(this)) , m_renderer(new KateRenderer(doc, m_textFolding, this)) , m_viewInternal(new KateViewInternal(this)) , m_spell(new KateSpellCheckDialog(this)) , m_bookmarks(new KateBookmarks(this)) , m_topSpacer(new QSpacerItem(0, 0)) , m_leftSpacer(new QSpacerItem(0, 0)) , m_rightSpacer(new QSpacerItem(0, 0)) , m_bottomSpacer(new QSpacerItem(0, 0)) , m_startingUp(true) , m_updatingDocumentConfig(false) , m_selection(m_doc->buffer(), KTextEditor::Range::invalid(), Kate::TextRange::ExpandLeft, Kate::TextRange::AllowEmpty) , blockSelect(false) , m_bottomViewBar(nullptr) , m_gotoBar(nullptr) , m_dictionaryBar(nullptr) , m_spellingMenu(new KateSpellingMenu(this)) , m_userContextMenuSet(false) , m_delayedUpdateTriggered(false) , m_lineToUpdateMin(-1) , m_lineToUpdateMax(-1) , m_mainWindow(mainWindow ? mainWindow : KTextEditor::EditorPrivate::self()->dummyMainWindow()) // use dummy window if no window there! , m_statusBar(nullptr) , m_temporaryAutomaticInvocationDisabled(false) , m_autoFoldedFirstLine(false) { // queued connect to collapse view updates for range changes, INIT THIS EARLY ENOUGH! connect(this, SIGNAL(delayedUpdateOfView()), this, SLOT(slotDelayedUpdateOfView()), Qt::QueuedConnection); KXMLGUIClient::setComponentName(KTextEditor::EditorPrivate::self()->aboutData().componentName(), KTextEditor::EditorPrivate::self()->aboutData().displayName()); // selection if for this view only and will invalidate if becoming empty m_selection.setView(this); // use z depth defined in moving ranges interface m_selection.setZDepth(-100000.0); KTextEditor::EditorPrivate::self()->registerView(this); /** * try to let the main window, if any, create a view bar for this view */ QWidget *bottomBarParent = m_mainWindow->createViewBar(this); m_bottomViewBar = new KateViewBar(bottomBarParent != nullptr, bottomBarParent ? bottomBarParent : this, this); // ugly workaround: // Force the layout to be left-to-right even on RTL desktop, as discussed // on the mailing list. This will cause the lines and icons panel to be on // the left, even for Arabic/Hebrew/Farsi/whatever users. setLayoutDirection(Qt::LeftToRight); m_bottomViewBar->installEventFilter(m_viewInternal); // add KateMessageWidget for KTE::MessageInterface immediately above view m_messageWidgets[KTextEditor::Message::AboveView] = new KateMessageWidget(this); m_messageWidgets[KTextEditor::Message::AboveView]->hide(); // add KateMessageWidget for KTE::MessageInterface immediately above view m_messageWidgets[KTextEditor::Message::BelowView] = new KateMessageWidget(this); m_messageWidgets[KTextEditor::Message::BelowView]->hide(); // add bottom viewbar... if (bottomBarParent) { m_mainWindow->addWidgetToViewBar(this, m_bottomViewBar); } // add layout for floating message widgets to KateViewInternal m_notificationLayout = new KateMessageLayout(m_viewInternal); m_notificationLayout->setContentsMargins(20, 20, 20, 20); m_viewInternal->setLayout(m_notificationLayout); // this really is needed :) m_viewInternal->updateView(); doc->addView(this); setFocusProxy(m_viewInternal); setFocusPolicy(Qt::StrongFocus); setXMLFile(QStringLiteral("katepart5ui.rc")); setupConnections(); setupActions(); // auto word completion new KateWordCompletionView(this, actionCollection()); // update the enabled state of the undo/redo actions... slotUpdateUndo(); /** * create the status bar of this view * do this after action creation, we use some of them! */ toggleStatusBar(); m_startingUp = false; updateConfig(); slotHlChanged(); KCursor::setAutoHideCursor(m_viewInternal, true); for (auto messageWidget : m_messageWidgets) { if (messageWidget) { // user interaction (scrolling) starts notification auto-hide timer connect(this, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate *)), messageWidget, SLOT(startAutoHideTimer())); // user interaction (cursor navigation) starts notification auto-hide timer connect(this, SIGNAL(cursorPositionChanged(KTextEditor::View *, KTextEditor::Cursor)), messageWidget, SLOT(startAutoHideTimer())); } } // folding restoration on reload connect(m_doc, SIGNAL(aboutToReload(KTextEditor::Document *)), SLOT(saveFoldingState())); connect(m_doc, SIGNAL(reloaded(KTextEditor::Document *)), SLOT(applyFoldingState())); connect(m_doc, &KTextEditor::DocumentPrivate::reloaded, this, &KTextEditor::ViewPrivate::slotDocumentReloaded); connect(m_doc, &KTextEditor::DocumentPrivate::aboutToReload, this, &KTextEditor::ViewPrivate::slotDocumentAboutToReload); // update highlights on scrolling and co connect(this, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate *)), this, SLOT(createHighlights())); // clear highlights on reload connect(m_doc, SIGNAL(aboutToReload(KTextEditor::Document *)), SLOT(clearHighlights())); // setup layout setupLayout(); } KTextEditor::ViewPrivate::~ViewPrivate() { // invalidate update signal m_delayedUpdateTriggered = false; // remove from xmlgui factory, to be safe if (factory()) { factory()->removeClient(this); } // delete internal view before view bar! delete m_viewInternal; /** * remove view bar again, if needed */ m_mainWindow->deleteViewBar(this); m_bottomViewBar = nullptr; doc()->removeView(this); delete m_renderer; delete m_config; KTextEditor::EditorPrivate::self()->deregisterView(this); } void KTextEditor::ViewPrivate::toggleStatusBar() { /** * if there, delete it */ if (m_statusBar) { bottomViewBar()->removePermanentBarWidget(m_statusBar); delete m_statusBar; m_statusBar = nullptr; emit statusBarEnabledChanged(this, false); return; } /** * else: create it */ m_statusBar = new KateStatusBar(this); bottomViewBar()->addPermanentBarWidget(m_statusBar); emit statusBarEnabledChanged(this, true); } void KTextEditor::ViewPrivate::setupLayout() { // delete old layout if any if (layout()) { delete layout(); /** * need to recreate spacers because they are deleted with * their belonging layout */ m_topSpacer = new QSpacerItem(0, 0); m_leftSpacer = new QSpacerItem(0, 0); m_rightSpacer = new QSpacerItem(0, 0); m_bottomSpacer = new QSpacerItem(0, 0); } // set margins QStyleOptionFrame opt; opt.initFrom(this); opt.frameShape = QFrame::StyledPanel; opt.state |= QStyle::State_Sunken; const int margin = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, this); m_topSpacer->changeSize(0, margin, QSizePolicy::Minimum, QSizePolicy::Fixed); m_leftSpacer->changeSize(margin, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); m_rightSpacer->changeSize(margin, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); m_bottomSpacer->changeSize(0, margin, QSizePolicy::Minimum, QSizePolicy::Fixed); // define layout QGridLayout *layout = new QGridLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); const bool frameAroundContents = style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &opt, this); if (frameAroundContents) { // top message widget layout->addWidget(m_messageWidgets[KTextEditor::Message::AboveView], 0, 0, 1, 5); // top spacer layout->addItem(m_topSpacer, 1, 0, 1, 4); // left spacer layout->addItem(m_leftSpacer, 2, 0, 1, 1); // left border layout->addWidget(m_viewInternal->m_leftBorder, 2, 1, 1, 1); // view layout->addWidget(m_viewInternal, 2, 2, 1, 1); // right spacer layout->addItem(m_rightSpacer, 2, 3, 1, 1); // bottom spacer layout->addItem(m_bottomSpacer, 3, 0, 1, 4); // vertical scrollbar layout->addWidget(m_viewInternal->m_lineScroll, 1, 4, 3, 1); // horizontal scrollbar layout->addWidget(m_viewInternal->m_columnScroll, 4, 0, 1, 4); // dummy layout->addWidget(m_viewInternal->m_dummy, 4, 4, 1, 1); // bottom message layout->addWidget(m_messageWidgets[KTextEditor::Message::BelowView], 5, 0, 1, 5); // bottom viewbar if (m_bottomViewBar->parentWidget() == this) { layout->addWidget(m_bottomViewBar, 6, 0, 1, 5); } // stretch layout->setColumnStretch(2, 1); layout->setRowStretch(2, 1); // adjust scrollbar background m_viewInternal->m_lineScroll->setBackgroundRole(QPalette::Window); m_viewInternal->m_lineScroll->setAutoFillBackground(false); m_viewInternal->m_columnScroll->setBackgroundRole(QPalette::Window); m_viewInternal->m_columnScroll->setAutoFillBackground(false); } else { // top message widget layout->addWidget(m_messageWidgets[KTextEditor::Message::AboveView], 0, 0, 1, 5); // top spacer layout->addItem(m_topSpacer, 1, 0, 1, 5); // left spacer layout->addItem(m_leftSpacer, 2, 0, 1, 1); // left border layout->addWidget(m_viewInternal->m_leftBorder, 2, 1, 1, 1); // view layout->addWidget(m_viewInternal, 2, 2, 1, 1); // vertical scrollbar layout->addWidget(m_viewInternal->m_lineScroll, 2, 3, 1, 1); // right spacer layout->addItem(m_rightSpacer, 2, 4, 1, 1); // horizontal scrollbar layout->addWidget(m_viewInternal->m_columnScroll, 3, 1, 1, 2); // dummy layout->addWidget(m_viewInternal->m_dummy, 3, 3, 1, 1); // bottom spacer layout->addItem(m_bottomSpacer, 4, 0, 1, 5); // bottom message layout->addWidget(m_messageWidgets[KTextEditor::Message::BelowView], 5, 0, 1, 5); // bottom viewbar if (m_bottomViewBar->parentWidget() == this) { layout->addWidget(m_bottomViewBar, 6, 0, 1, 5); } // stretch layout->setColumnStretch(2, 1); layout->setRowStretch(2, 1); // adjust scrollbar background m_viewInternal->m_lineScroll->setBackgroundRole(QPalette::Base); m_viewInternal->m_lineScroll->setAutoFillBackground(true); m_viewInternal->m_columnScroll->setBackgroundRole(QPalette::Base); m_viewInternal->m_columnScroll->setAutoFillBackground(true); } } void KTextEditor::ViewPrivate::setupConnections() { connect(m_doc, SIGNAL(undoChanged()), this, SLOT(slotUpdateUndo())); connect(m_doc, SIGNAL(highlightingModeChanged(KTextEditor::Document *)), this, SLOT(slotHlChanged())); connect(m_doc, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); connect(m_viewInternal, SIGNAL(dropEventPass(QDropEvent *)), this, SIGNAL(dropEventPass(QDropEvent *))); connect(m_doc, SIGNAL(annotationModelChanged(KTextEditor::AnnotationModel *, KTextEditor::AnnotationModel *)), m_viewInternal->m_leftBorder, SLOT(annotationModelChanged(KTextEditor::AnnotationModel *, KTextEditor::AnnotationModel *))); } void KTextEditor::ViewPrivate::goToPreviousEditingPosition() { auto c = doc()->lastEditingPosition(KTextEditor::DocumentPrivate::Previous, cursorPosition()); if (c.isValid()) { setCursorPosition(c); } } void KTextEditor::ViewPrivate::goToNextEditingPosition() { auto c = doc()->lastEditingPosition(KTextEditor::DocumentPrivate::Next, cursorPosition()); if (c.isValid()) { setCursorPosition(c); } } void KTextEditor::ViewPrivate::setupActions() { KActionCollection *ac = actionCollection(); QAction *a; m_toggleWriteLock = nullptr; m_cut = a = ac->addAction(KStandardAction::Cut, this, SLOT(cut())); a->setWhatsThis(i18n("Cut the selected text and move it to the clipboard")); m_paste = a = ac->addAction(KStandardAction::Paste, this, SLOT(paste())); a->setWhatsThis(i18n("Paste previously copied or cut clipboard contents")); m_copy = a = ac->addAction(KStandardAction::Copy, this, SLOT(copy())); a->setWhatsThis(i18n("Use this command to copy the currently selected text to the system clipboard.")); m_pasteMenu = ac->addAction(QStringLiteral("edit_paste_menu"), new KatePasteMenu(i18n("Clipboard &History"), this)); connect(KTextEditor::EditorPrivate::self(), SIGNAL(clipboardHistoryChanged()), this, SLOT(slotClipboardHistoryChanged())); if (!doc()->readOnly()) { a = ac->addAction(KStandardAction::Save, m_doc, SLOT(documentSave())); a->setWhatsThis(i18n("Save the current document")); a = m_editUndo = ac->addAction(KStandardAction::Undo, m_doc, SLOT(undo())); a->setWhatsThis(i18n("Revert the most recent editing actions")); a = m_editRedo = ac->addAction(KStandardAction::Redo, m_doc, SLOT(redo())); a->setWhatsThis(i18n("Revert the most recent undo operation")); // Tools > Scripts // stored inside scoped pointer to ensure we destruct it early enough as it does internal cleanups of other child objects m_scriptActionMenu.reset(new KateScriptActionMenu(this, i18n("&Scripts"))); ac->addAction(QStringLiteral("tools_scripts"), m_scriptActionMenu.data()); a = ac->addAction(QStringLiteral("tools_apply_wordwrap")); a->setText(i18n("Apply &Word Wrap")); 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")); a->setText(i18n("&Clean Indentation")); a->setWhatsThis( i18n("Use this to clean the indentation of a selected block of text (only tabs/only spaces).

" "You can configure whether tabs should be honored and used or replaced with spaces, in the configuration dialog.")); connect(a, SIGNAL(triggered(bool)), SLOT(cleanIndent())); a = ac->addAction(QStringLiteral("tools_align")); a->setText(i18n("&Align")); a->setWhatsThis(i18n("Use this to align the current line or block of text to its proper indent level.")); connect(a, SIGNAL(triggered(bool)), SLOT(align())); a = ac->addAction(QStringLiteral("tools_comment")); a->setText(i18n("C&omment")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_D)); a->setWhatsThis( i18n("This command comments out the current line or a selected block of text.

" "The characters for single/multiple line comments are defined within the language's highlighting.")); connect(a, SIGNAL(triggered(bool)), SLOT(comment())); a = ac->addAction(QStringLiteral("Previous Editing Line")); a->setText(i18n("Go to previous editing line")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(a, SIGNAL(triggered(bool)), SLOT(goToPreviousEditingPosition())); a = ac->addAction(QStringLiteral("Next Editing Line")); a->setText(i18n("Go to next editing line")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_E)); connect(a, SIGNAL(triggered(bool)), SLOT(goToNextEditingPosition())); a = ac->addAction(QStringLiteral("tools_uncomment")); a->setText(i18n("Unco&mment")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_D)); a->setWhatsThis( i18n("This command removes comments from the current line or a selected block of text.

" "The characters for single/multiple line comments are defined within the language's highlighting.")); connect(a, SIGNAL(triggered(bool)), SLOT(uncomment())); a = ac->addAction(QStringLiteral("tools_toggle_comment")); a->setText(i18n("Toggle Comment")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Slash)); connect(a, SIGNAL(triggered(bool)), SLOT(toggleComment())); a = m_toggleWriteLock = new KToggleAction(i18n("&Read Only Mode"), this); a->setWhatsThis(i18n("Lock/unlock the document for writing")); a->setChecked(!doc()->isReadWrite()); connect(a, SIGNAL(triggered(bool)), SLOT(toggleWriteLock())); ac->addAction(QStringLiteral("tools_toggle_write_lock"), a); a = ac->addAction(QStringLiteral("tools_uppercase")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-text-uppercase"))); a->setText(i18n("Uppercase")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_U)); a->setWhatsThis( i18n("Convert the selection to uppercase, or the character to the " "right of the cursor if no text is selected.")); connect(a, SIGNAL(triggered(bool)), SLOT(uppercase())); a = ac->addAction(QStringLiteral("tools_lowercase")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-text-lowercase"))); a->setText(i18n("Lowercase")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_U)); a->setWhatsThis( i18n("Convert the selection to lowercase, or the character to the " "right of the cursor if no text is selected.")); connect(a, SIGNAL(triggered(bool)), SLOT(lowercase())); a = ac->addAction(QStringLiteral("tools_capitalize")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-text-capitalize"))); a->setText(i18n("Capitalize")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_U)); a->setWhatsThis( i18n("Capitalize the selection, or the word under the " "cursor if no text is selected.")); connect(a, SIGNAL(triggered(bool)), SLOT(capitalize())); a = ac->addAction(QStringLiteral("tools_join_lines")); a->setText(i18n("Join Lines")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_J)); connect(a, SIGNAL(triggered(bool)), SLOT(joinLines())); a = ac->addAction(QStringLiteral("tools_invoke_code_completion")); a->setText(i18n("Invoke Code Completion")); a->setWhatsThis(i18n("Manually invoke command completion, usually by using a shortcut bound to this action.")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Space)); connect(a, SIGNAL(triggered(bool)), SLOT(userInvokedCompletion())); } else { m_cut->setEnabled(false); m_paste->setEnabled(false); m_pasteMenu->setEnabled(false); m_editUndo = nullptr; m_editRedo = nullptr; } a = ac->addAction(KStandardAction::Print, this, SLOT(print())); a->setWhatsThis(i18n("Print the current document.")); a = ac->addAction(KStandardAction::PrintPreview, this, SLOT(printPreview())); a->setWhatsThis(i18n("Show print preview of current document")); a = ac->addAction(QStringLiteral("file_reload")); a->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); a->setText(i18n("Reloa&d")); ac->setDefaultShortcuts(a, KStandardShortcut::reload()); a->setWhatsThis(i18n("Reload the current document from disk.")); connect(a, SIGNAL(triggered(bool)), SLOT(reloadFile())); a = ac->addAction(KStandardAction::SaveAs, m_doc, SLOT(documentSaveAs())); a->setWhatsThis(i18n("Save the current document to disk, with a name of your choice.")); a = new KateViewEncodingAction(m_doc, this, i18n("Save As with Encoding..."), this, true /* special mode for save as */); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); ac->addAction(QStringLiteral("file_save_as_with_encoding"), a); a = ac->addAction(QStringLiteral("file_save_copy_as")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); a->setText(i18n("Save &Copy As...")); a->setWhatsThis(i18n("Save a copy of the current document to disk.")); connect(a, SIGNAL(triggered(bool)), m_doc, SLOT(documentSaveCopyAs())); a = ac->addAction(KStandardAction::GotoLine, this, SLOT(gotoLine())); a->setWhatsThis(i18n("This command opens a dialog and lets you choose a line that you want the cursor to move to.")); a = ac->addAction(QStringLiteral("modified_line_up")); a->setText(i18n("Move to Previous Modified Line")); a->setWhatsThis(i18n("Move upwards to the previous modified line.")); connect(a, SIGNAL(triggered(bool)), SLOT(toPrevModifiedLine())); a = ac->addAction(QStringLiteral("modified_line_down")); a->setText(i18n("Move to Next Modified Line")); a->setWhatsThis(i18n("Move downwards to the next modified line.")); connect(a, SIGNAL(triggered(bool)), SLOT(toNextModifiedLine())); a = ac->addAction(QStringLiteral("set_confdlg")); a->setText(i18n("&Configure Editor...")); a->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); a->setWhatsThis(i18n("Configure various aspects of this editor.")); connect(a, SIGNAL(triggered(bool)), SLOT(slotConfigDialog())); m_modeAction = new KateModeMenu(i18n("&Mode"), this); ac->addAction(QStringLiteral("tools_mode"), m_modeAction); m_modeAction->setWhatsThis(i18n("Here you can choose which mode should be used for the current document. This will influence the highlighting and folding being used, for example.")); m_modeAction->updateMenu(m_doc); KateHighlightingMenu *menu = new KateHighlightingMenu(i18n("&Highlighting"), this); ac->addAction(QStringLiteral("tools_highlighting"), menu); menu->setWhatsThis(i18n("Here you can choose how the current document should be highlighted.")); menu->updateMenu(m_doc); KateViewSchemaAction *schemaMenu = new KateViewSchemaAction(i18n("&Schema"), this); ac->addAction(QStringLiteral("view_schemas"), schemaMenu); schemaMenu->updateMenu(this); // indentation menu KateViewIndentationAction *indentMenu = new KateViewIndentationAction(m_doc, i18n("&Indentation"), this); ac->addAction(QStringLiteral("tools_indentation"), indentMenu); m_selectAll = a = ac->addAction(KStandardAction::SelectAll, this, SLOT(selectAll())); a->setWhatsThis(i18n("Select the entire text of the current document.")); m_deSelect = a = ac->addAction(KStandardAction::Deselect, this, SLOT(clearSelection())); a->setWhatsThis(i18n("If you have selected something within the current document, this will no longer be selected.")); a = ac->addAction(QStringLiteral("view_inc_font_sizes")); a->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); a->setText(i18n("Enlarge Font")); ac->setDefaultShortcuts(a, KStandardShortcut::zoomIn()); a->setWhatsThis(i18n("This increases the display font size.")); connect(a, SIGNAL(triggered(bool)), m_viewInternal, SLOT(slotIncFontSizes())); a = ac->addAction(QStringLiteral("view_dec_font_sizes")); a->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); a->setText(i18n("Shrink Font")); ac->setDefaultShortcuts(a, KStandardShortcut::zoomOut()); a->setWhatsThis(i18n("This decreases the display font size.")); connect(a, SIGNAL(triggered(bool)), m_viewInternal, SLOT(slotDecFontSizes())); a = ac->addAction(QStringLiteral("view_reset_font_sizes")); a->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); a->setText(i18n("Reset Font Size")); ac->setDefaultShortcuts(a, KStandardShortcut::shortcut(KStandardShortcut::ActualSize)); a->setWhatsThis(i18n("This resets the display font size.")); connect(a, SIGNAL(triggered(bool)), m_viewInternal, SLOT(slotResetFontSizes())); a = m_toggleBlockSelection = new KToggleAction(i18n("Bl&ock Selection Mode"), this); ac->addAction(QStringLiteral("set_verticalSelect"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_B)); a->setWhatsThis(i18n("This command allows switching between the normal (line based) selection mode and the block selection mode.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleBlockSelection())); a = ac->addAction(QStringLiteral("switch_next_input_mode")); a->setText(i18n("Switch to next Input Mode")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_V)); a->setWhatsThis(i18n("Switch to the next input mode.")); connect(a, SIGNAL(triggered(bool)), SLOT(cycleInputMode())); a = m_toggleInsert = new KToggleAction(i18n("Overwr&ite Mode"), this); ac->addAction(QStringLiteral("set_insert"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Insert)); a->setWhatsThis(i18n("Choose whether you want the text you type to be inserted or to overwrite existing text.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleInsert())); KToggleAction *toggleAction; 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.

" "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); ac->addAction(QStringLiteral("dynamic_word_wrap_indicators"), a); a->setWhatsThis(i18n("Choose when the Dynamic Word Wrap Indicators should be displayed")); connect(m_setDynWrapIndicators, SIGNAL(triggered(int)), this, SLOT(setDynWrapIndicators(int))); const QStringList list2 {i18n("&Off"), i18n("Follow &Line Numbers"), i18n("&Always On")}; m_setDynWrapIndicators->setItems(list2); m_setDynWrapIndicators->setEnabled(m_toggleDynWrap->isChecked()); // only synced on real change, later a = toggleAction = new KToggleAction(i18n("Static Word Wrap"), this); ac->addAction(QStringLiteral("view_static_word_wrap"), a); a->setWhatsThis(i18n("If this option is checked, the text lines will be wrapped at the column defined in the editing properties.")); connect(a, &KToggleAction::triggered, [=] { if (m_doc) { m_doc->setWordWrap(!m_doc->wordWrap()); } }); a = toggleAction = m_toggleWWMarker = new KToggleAction(i18n("Show Static &Word Wrap Marker"), this); ac->addAction(QStringLiteral("view_word_wrap_marker"), a); a->setWhatsThis( i18n("Show/hide the Word Wrap Marker, a vertical line drawn at the word " "wrap column as defined in the editing properties")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleWWMarker())); a = toggleAction = m_toggleFoldingMarkers = new KToggleAction(i18n("Show Folding &Markers"), this); ac->addAction(QStringLiteral("view_folding_markers"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F9)); a->setWhatsThis(i18n("You can choose if the codefolding marks should be shown, if codefolding is possible.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleFoldingMarkers())); a = m_toggleIconBar = toggleAction = new KToggleAction(i18n("Show &Icon Border"), this); ac->addAction(QStringLiteral("view_border"), a); a->setWhatsThis(i18n("Show/hide the icon border.

The icon border shows bookmark symbols, for instance.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleIconBorder())); a = toggleAction = m_toggleLineNumbers = new KToggleAction(i18n("Show &Line Numbers"), this); ac->addAction(QStringLiteral("view_line_numbers"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F11)); a->setWhatsThis(i18n("Show/hide the line numbers on the left hand side of the view.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleLineNumbersOn())); a = m_toggleScrollBarMarks = toggleAction = new KToggleAction(i18n("Show Scroll&bar Marks"), this); ac->addAction(QStringLiteral("view_scrollbar_marks"), a); a->setWhatsThis(i18n("Show/hide the marks on the vertical scrollbar.

The marks show bookmarks, for instance.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleScrollBarMarks())); a = m_toggleScrollBarMiniMap = toggleAction = new KToggleAction(i18n("Show Scrollbar Mini-Map"), this); ac->addAction(QStringLiteral("view_scrollbar_minimap"), a); a->setWhatsThis(i18n("Show/hide the mini-map on the vertical scrollbar.

The mini-map shows an overview of the whole document.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleScrollBarMiniMap())); a = m_doc->autoReloadToggleAction(); ac->addAction(QStringLiteral("view_auto_reload"), a); // a = m_toggleScrollBarMiniMapAll = toggleAction = new KToggleAction(i18n("Show the whole document in the Mini-Map"), this); // ac->addAction(QLatin1String("view_scrollbar_minimap_all"), a); // a->setWhatsThis(i18n("Display the whole document in the mini-map.

With this option set the whole document will be visible in the mini-map.")); // connect(a, SIGNAL(triggered(bool)), SLOT(toggleScrollBarMiniMapAll())); // connect(m_toggleScrollBarMiniMap, SIGNAL(triggered(bool)), m_toggleScrollBarMiniMapAll, SLOT(setEnabled(bool))); a = m_toggleNPSpaces = new KToggleAction(i18n("Show Non-Printable Spaces"), this); ac->addAction(QStringLiteral("view_non_printable_spaces"), a); a->setWhatsThis(i18n("Show/hide bounding box around non-printable spaces")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleNPSpaces())); a = m_switchCmdLine = ac->addAction(QStringLiteral("switch_to_cmd_line")); a->setText(i18n("Switch to Command Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F7)); a->setWhatsThis(i18n("Show/hide the command line on the bottom of the view.")); connect(a, SIGNAL(triggered(bool)), SLOT(switchToCmdLine())); KActionMenu *am = new KActionMenu(i18n("Input Modes"), this); m_inputModeActions = new QActionGroup(am); ac->addAction(QStringLiteral("view_input_modes"), am); auto switchInputModeAction = ac->action(QStringLiteral("switch_next_input_mode")); am->addAction(switchInputModeAction); am->addSeparator(); for (KateAbstractInputMode *mode : qAsConst(m_viewInternal->m_inputModes)) { a = new QAction(mode->viewInputModeHuman(), m_inputModeActions); am->addAction(a); a->setWhatsThis(i18n("Activate/deactivate %1", mode->viewInputModeHuman())); const InputMode im = mode->viewInputMode(); a->setData(static_cast(im)); a->setCheckable(true); if (im == m_config->inputMode()) a->setChecked(true); connect(a, SIGNAL(triggered()), SLOT(toggleInputMode())); } a = m_setEndOfLine = new KSelectAction(i18n("&End of Line"), this); ac->addAction(QStringLiteral("set_eol"), a); a->setWhatsThis(i18n("Choose which line endings should be used, when you save the document")); const QStringList list {i18nc("@item:inmenu End of Line", "&UNIX"), i18nc("@item:inmenu End of Line", "&Windows/DOS"), i18nc("@item:inmenu End of Line", "&Macintosh")}; m_setEndOfLine->setItems(list); m_setEndOfLine->setCurrentItem(doc()->config()->eol()); connect(m_setEndOfLine, SIGNAL(triggered(int)), this, SLOT(setEol(int))); a = m_addBom = new KToggleAction(i18n("Add &Byte Order Mark (BOM)"), this); m_addBom->setChecked(doc()->config()->bom()); ac->addAction(QStringLiteral("add_bom"), a); a->setWhatsThis(i18n("Enable/disable adding of byte order marks for UTF-8/UTF-16 encoded files while saving")); connect(m_addBom, SIGNAL(triggered(bool)), this, SLOT(setAddBom(bool))); // encoding menu m_encodingAction = new KateViewEncodingAction(m_doc, this, i18n("E&ncoding"), this); ac->addAction(QStringLiteral("set_encoding"), m_encodingAction); a = ac->addAction(KStandardAction::Find, this, SLOT(find())); a->setWhatsThis(i18n("Look up the first occurrence of a piece of text or regular expression.")); addAction(a); a = ac->addAction(QStringLiteral("edit_find_selected")); a->setText(i18n("Find Selected")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_H)); a->setWhatsThis(i18n("Finds next occurrence of selected text.")); connect(a, SIGNAL(triggered(bool)), SLOT(findSelectedForwards())); a = ac->addAction(QStringLiteral("edit_find_selected_backwards")); a->setText(i18n("Find Selected Backwards")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_H)); a->setWhatsThis(i18n("Finds previous occurrence of selected text.")); connect(a, SIGNAL(triggered(bool)), SLOT(findSelectedBackwards())); a = ac->addAction(KStandardAction::FindNext, this, SLOT(findNext())); a->setWhatsThis(i18n("Look up the next occurrence of the search phrase.")); addAction(a); a = ac->addAction(KStandardAction::FindPrev, QStringLiteral("edit_find_prev"), this, SLOT(findPrevious())); a->setWhatsThis(i18n("Look up the previous occurrence of the search phrase.")); addAction(a); a = ac->addAction(KStandardAction::Replace, this, SLOT(replace())); a->setWhatsThis(i18n("Look up a piece of text or regular expression and replace the result with some given text.")); m_spell->createActions(ac); m_toggleOnTheFlySpellCheck = new KToggleAction(i18n("Automatic Spell Checking"), this); m_toggleOnTheFlySpellCheck->setWhatsThis(i18n("Enable/disable automatic spell checking")); connect(m_toggleOnTheFlySpellCheck, SIGNAL(triggered(bool)), SLOT(toggleOnTheFlySpellCheck(bool))); ac->addAction(QStringLiteral("tools_toggle_automatic_spell_checking"), m_toggleOnTheFlySpellCheck); ac->setDefaultShortcut(m_toggleOnTheFlySpellCheck, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_O)); a = ac->addAction(QStringLiteral("tools_change_dictionary")); a->setText(i18n("Change Dictionary...")); a->setWhatsThis(i18n("Change the dictionary that is used for spell checking.")); connect(a, SIGNAL(triggered()), SLOT(changeDictionary())); a = ac->addAction(QStringLiteral("tools_clear_dictionary_ranges")); a->setText(i18n("Clear Dictionary Ranges")); a->setEnabled(false); a->setWhatsThis(i18n("Remove all the separate dictionary ranges that were set for spell checking.")); connect(a, SIGNAL(triggered()), m_doc, SLOT(clearDictionaryRanges())); connect(m_doc, SIGNAL(dictionaryRangesPresent(bool)), a, SLOT(setEnabled(bool))); m_copyHtmlAction = ac->addAction(QStringLiteral("edit_copy_html"), this, SLOT(exportHtmlToClipboard())); m_copyHtmlAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_copyHtmlAction->setText(i18n("Copy as &HTML")); m_copyHtmlAction->setWhatsThis(i18n("Use this command to copy the currently selected text as HTML to the system clipboard.")); a = ac->addAction(QStringLiteral("file_export_html"), this, SLOT(exportHtmlToFile())); a->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); a->setText(i18n("E&xport as HTML...")); a->setWhatsThis( i18n("This command allows you to export the current document" " with all highlighting information into a HTML document.")); m_spellingMenu->createActions(ac); m_bookmarks->createActions(ac); slotSelectionChanged(); // Now setup the editing actions before adding the associated // widget and setting the shortcut context setupEditActions(); setupCodeFolding(); slotClipboardHistoryChanged(); ac->addAssociatedWidget(m_viewInternal); const auto actions = ac->actions(); for (QAction *action : actions) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } connect(this, SIGNAL(selectionChanged(KTextEditor::View *)), this, SLOT(slotSelectionChanged())); } void KTextEditor::ViewPrivate::slotConfigDialog() { // invoke config dialog, will auto-save configuration to katepartrc KTextEditor::EditorPrivate::self()->configDialog(this); } void KTextEditor::ViewPrivate::setupEditActions() { // If you add an editing action to this // function make sure to include the line // m_editActions << a after creating the action KActionCollection *ac = actionCollection(); QAction *a = ac->addAction(QStringLiteral("word_left")); a->setText(i18n("Move Word Left")); ac->setDefaultShortcuts(a, KStandardShortcut::backwardWord()); connect(a, SIGNAL(triggered(bool)), SLOT(wordLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("select_char_left")); a->setText(i18n("Select Character Left")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Left)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftCursorLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("select_word_left")); a->setText(i18n("Select Word Left")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_Left)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftWordLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("word_right")); a->setText(i18n("Move Word Right")); ac->setDefaultShortcuts(a, KStandardShortcut::forwardWord()); connect(a, SIGNAL(triggered(bool)), SLOT(wordRight())); m_editActions << a; a = ac->addAction(QStringLiteral("select_char_right")); a->setText(i18n("Select Character Right")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Right)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftCursorRight())); m_editActions << a; a = ac->addAction(QStringLiteral("select_word_right")); a->setText(i18n("Select Word Right")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_Right)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftWordRight())); m_editActions << a; a = ac->addAction(QStringLiteral("beginning_of_line")); a->setText(i18n("Move to Beginning of Line")); ac->setDefaultShortcuts(a, KStandardShortcut::beginningOfLine()); connect(a, SIGNAL(triggered(bool)), SLOT(home())); m_editActions << a; a = ac->addAction(QStringLiteral("beginning_of_document")); a->setText(i18n("Move to Beginning of Document")); ac->setDefaultShortcuts(a, KStandardShortcut::begin()); connect(a, SIGNAL(triggered(bool)), SLOT(top())); m_editActions << a; a = ac->addAction(QStringLiteral("select_beginning_of_line")); a->setText(i18n("Select to Beginning of Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftHome())); m_editActions << a; a = ac->addAction(QStringLiteral("select_beginning_of_document")); a->setText(i18n("Select to Beginning of Document")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftTop())); m_editActions << a; a = ac->addAction(QStringLiteral("end_of_line")); a->setText(i18n("Move to End of Line")); ac->setDefaultShortcuts(a, KStandardShortcut::endOfLine()); connect(a, SIGNAL(triggered(bool)), SLOT(end())); m_editActions << a; a = ac->addAction(QStringLiteral("end_of_document")); a->setText(i18n("Move to End of Document")); ac->setDefaultShortcuts(a, KStandardShortcut::end()); connect(a, SIGNAL(triggered(bool)), SLOT(bottom())); m_editActions << a; a = ac->addAction(QStringLiteral("select_end_of_line")); a->setText(i18n("Select to End of Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftEnd())); m_editActions << a; a = ac->addAction(QStringLiteral("select_end_of_document")); a->setText(i18n("Select to End of Document")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftBottom())); m_editActions << a; a = ac->addAction(QStringLiteral("select_line_up")); a->setText(i18n("Select to Previous Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Up)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftUp())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_line_up")); a->setText(i18n("Scroll Line Up")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Up)); connect(a, SIGNAL(triggered(bool)), SLOT(scrollUp())); m_editActions << a; a = ac->addAction(QStringLiteral("move_line_down")); a->setText(i18n("Move to Next Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Down)); connect(a, SIGNAL(triggered(bool)), SLOT(down())); m_editActions << a; a = ac->addAction(QStringLiteral("move_line_up")); a->setText(i18n("Move to Previous Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Up)); connect(a, SIGNAL(triggered(bool)), SLOT(up())); m_editActions << a; a = ac->addAction(QStringLiteral("move_cursor_right")); a->setText(i18n("Move Cursor Right")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Right)); connect(a, SIGNAL(triggered(bool)), SLOT(cursorRight())); m_editActions << a; a = ac->addAction(QStringLiteral("move_cursor_left")); a->setText(i18n("Move Cursor Left")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Left)); connect(a, SIGNAL(triggered(bool)), SLOT(cursorLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("select_line_down")); a->setText(i18n("Select to Next Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Down)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftDown())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_line_down")); a->setText(i18n("Scroll Line Down")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Down)); connect(a, SIGNAL(triggered(bool)), SLOT(scrollDown())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_page_up")); a->setText(i18n("Scroll Page Up")); ac->setDefaultShortcuts(a, KStandardShortcut::prior()); connect(a, SIGNAL(triggered(bool)), SLOT(pageUp())); m_editActions << a; a = ac->addAction(QStringLiteral("select_page_up")); a->setText(i18n("Select Page Up")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_PageUp)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftPageUp())); m_editActions << a; a = ac->addAction(QStringLiteral("move_top_of_view")); a->setText(i18n("Move to Top of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(topOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("select_top_of_view")); a->setText(i18n("Select to Top of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftTopOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_page_down")); a->setText(i18n("Scroll Page Down")); ac->setDefaultShortcuts(a, KStandardShortcut::next()); connect(a, SIGNAL(triggered(bool)), SLOT(pageDown())); m_editActions << a; a = ac->addAction(QStringLiteral("select_page_down")); a->setText(i18n("Select Page Down")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_PageDown)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftPageDown())); m_editActions << a; a = ac->addAction(QStringLiteral("move_bottom_of_view")); a->setText(i18n("Move to Bottom of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(bottomOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("select_bottom_of_view")); a->setText(i18n("Select to Bottom of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftBottomOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("to_matching_bracket")); a->setText(i18n("Move to Matching Bracket")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_6)); connect(a, SIGNAL(triggered(bool)), SLOT(toMatchingBracket())); // m_editActions << a; a = ac->addAction(QStringLiteral("select_matching_bracket")); a->setText(i18n("Select to Matching Bracket")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_6)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftToMatchingBracket())); // m_editActions << a; // anders: shortcuts doing any changes should not be created in read-only mode if (!doc()->readOnly()) { a = ac->addAction(QStringLiteral("transpose_char")); a->setText(i18n("Transpose Characters")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(a, SIGNAL(triggered(bool)), SLOT(transpose())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_line")); a->setText(i18n("Delete Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_K)); connect(a, SIGNAL(triggered(bool)), SLOT(killLine())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_word_left")); a->setText(i18n("Delete Word Left")); ac->setDefaultShortcuts(a, KStandardShortcut::deleteWordBack()); connect(a, SIGNAL(triggered(bool)), SLOT(deleteWordLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_word_right")); a->setText(i18n("Delete Word Right")); ac->setDefaultShortcuts(a, KStandardShortcut::deleteWordForward()); connect(a, SIGNAL(triggered(bool)), SLOT(deleteWordRight())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_next_character")); a->setText(i18n("Delete Next Character")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Delete)); connect(a, SIGNAL(triggered(bool)), SLOT(keyDelete())); m_editActions << a; a = ac->addAction(QStringLiteral("backspace")); a->setText(i18n("Backspace")); QList scuts; scuts << QKeySequence(Qt::Key_Backspace) << QKeySequence(Qt::SHIFT + Qt::Key_Backspace); ac->setDefaultShortcuts(a, scuts); connect(a, SIGNAL(triggered(bool)), SLOT(backspace())); m_editActions << a; a = ac->addAction(QStringLiteral("insert_tabulator")); a->setText(i18n("Insert Tab")); connect(a, SIGNAL(triggered(bool)), SLOT(insertTab())); m_editActions << a; a = ac->addAction(QStringLiteral("smart_newline")); a->setText(i18n("Insert Smart Newline")); a->setWhatsThis(i18n("Insert newline including leading characters of the current line which are not letters or numbers.")); scuts.clear(); scuts << QKeySequence(Qt::SHIFT + Qt::Key_Return) << QKeySequence(Qt::SHIFT + Qt::Key_Enter); ac->setDefaultShortcuts(a, scuts); connect(a, SIGNAL(triggered(bool)), SLOT(smartNewline())); m_editActions << a; a = ac->addAction(QStringLiteral("no_indent_newline")); a->setText(i18n("Insert a non-indented Newline")); a->setWhatsThis(i18n("Insert a new line without indentation, regardless of indentation settings.")); scuts.clear(); scuts << QKeySequence(Qt::CTRL + Qt::Key_Return) << QKeySequence(Qt::CTRL + Qt::Key_Enter); ac->setDefaultShortcuts(a, scuts); connect(a, SIGNAL(triggered(bool)), SLOT(noIndentNewline())); m_editActions << a; a = ac->addAction(QStringLiteral("tools_indent")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-indent-more"))); a->setText(i18n("&Indent")); a->setWhatsThis( i18n("Use this to indent a selected block of text.

" "You can configure whether tabs should be honored and used or replaced with spaces, in the configuration dialog.")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_I)); connect(a, SIGNAL(triggered(bool)), SLOT(indent())); a = ac->addAction(QStringLiteral("tools_unindent")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-indent-less"))); a->setText(i18n("&Unindent")); a->setWhatsThis(i18n("Use this to unindent a selected block of text.")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_I)); connect(a, SIGNAL(triggered(bool)), SLOT(unIndent())); } if (hasFocus()) { slotGotFocus(); } else { slotLostFocus(); } } void KTextEditor::ViewPrivate::setupCodeFolding() { KActionCollection *ac = this->actionCollection(); QAction *a; a = ac->addAction(QStringLiteral("folding_toplevel")); a->setText(i18n("Fold Toplevel Nodes")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Minus)); connect(a, SIGNAL(triggered(bool)), SLOT(slotFoldToplevelNodes())); a = ac->addAction(QStringLiteral("folding_expandtoplevel")); a->setText(i18n("Unfold Toplevel Nodes")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Plus)); connect(a, SIGNAL(triggered(bool)), SLOT(slotExpandToplevelNodes())); /*a = ac->addAction(QLatin1String("folding_expandall")); a->setText(i18n("Unfold All Nodes")); connect(a, SIGNAL(triggered(bool)), doc()->foldingTree(), SLOT(expandAll())); a = ac->addAction(QLatin1String("folding_collapse_dsComment")); a->setText(i18n("Fold Multiline Comments")); connect(a, SIGNAL(triggered(bool)), doc()->foldingTree(), SLOT(collapseAll_dsComments())); */ a = ac->addAction(QStringLiteral("folding_toggle_current")); a->setText(i18n("Toggle Current Node")); connect(a, &QAction::triggered, this, &KTextEditor::ViewPrivate::slotToggleFolding); a = ac->addAction(QStringLiteral("folding_toggle_in_current")); a->setText(i18n("Toggle Contained Nodes")); connect(a, &QAction::triggered, this, &KTextEditor::ViewPrivate::slotToggleFoldingsInRange); } void KTextEditor::ViewPrivate::slotFoldToplevelNodes() { for (int line = 0; line < doc()->lines(); ++line) { if (textFolding().isLineVisible(line)) { foldLine(line); } } } void KTextEditor::ViewPrivate::slotExpandToplevelNodes() { const auto topLevelRanges(textFolding().foldingRangesForParentRange()); for (const auto &range : topLevelRanges) { textFolding().unfoldRange(range.first); } } void KTextEditor::ViewPrivate::slotToggleFolding() { int line = cursorPosition().line(); bool actionDone = false; while (!actionDone && (line > -1)) { actionDone = unfoldLine(line); if (!actionDone) { actionDone = foldLine(line--).isValid(); } } } void KTextEditor::ViewPrivate::slotToggleFoldingsInRange() { int line = cursorPosition().line(); while (!toggleFoldingsInRange(line) && (line > -1)) { --line; } } KTextEditor::Range KTextEditor::ViewPrivate::foldLine(int line) { KTextEditor::Range foldingRange = doc()->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { return foldingRange; } // Ensure not to fold the end marker to avoid a deceptive look, but only on token based folding Kate::TextLine startTextLine = doc()->buffer().plainLine(line); if (!startTextLine->markedAsFoldingStartIndentation()) { const int adjustedLine = foldingRange.end().line() - 1; foldingRange.setEnd(KTextEditor::Cursor(adjustedLine, doc()->buffer().plainLine(adjustedLine)->length())); } // Don't try to fold a single line, which can happens due to adjustment above // FIXME Avoid to offer such a folding marker if (!foldingRange.onSingleLine()) { textFolding().newFoldingRange(foldingRange, Kate::TextFolding::Folded); } return foldingRange; } bool KTextEditor::ViewPrivate::unfoldLine(int line) { bool actionDone = false; const KTextEditor::Cursor currentCursor = cursorPosition(); // ask the folding info for this line, if any folds are around! // auto = QVector> auto startingRanges = textFolding().foldingRangesStartingOnLine(line); for (int i = 0; i < startingRanges.size() && !actionDone; ++i) { // Avoid jumping view in case of a big unfold and ensure nice highlight of folding marker setCursorPosition(textFolding().foldingRange(startingRanges[i].first).start()); actionDone |= textFolding().unfoldRange(startingRanges[i].first); } if (!actionDone) { // Nothing unfolded? Restore old cursor position! setCursorPosition(currentCursor); } return actionDone; } bool KTextEditor::ViewPrivate::toggleFoldingOfLine(int line) { bool actionDone = unfoldLine(line); if (!actionDone) { actionDone = foldLine(line).isValid(); } return actionDone; } bool KTextEditor::ViewPrivate::toggleFoldingsInRange(int line) { KTextEditor::Range foldingRange = doc()->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { // Either line is not valid or there is no start range return false; } bool actionDone = false; // Track success const KTextEditor::Cursor currentCursor = cursorPosition(); // Don't be too eager but obliging! Only toggle containing ranges which are // visible -> Be done when the range is folded actionDone |= unfoldLine(line); if (!actionDone) { // Unfold all in range, but not the range itself for (int ln = foldingRange.start().line() + 1; ln < foldingRange.end().line(); ++ln) { actionDone |= unfoldLine(ln); } if (actionDone) { // In most cases we want now a not moved cursor setCursorPosition(currentCursor); } } if (!actionDone) { // Fold all in range, but not the range itself for (int ln = foldingRange.start().line() + 1; ln < foldingRange.end().line(); ++ln) { KTextEditor::Range fr = foldLine(ln); if (fr.isValid()) { // qMax to avoid infinite loop in case of range without content ln = qMax(ln, fr.end().line() - 1); actionDone = true; } } } if (!actionDone) { // At this point was an unfolded range clicked which contains no "childs" // We assume the user want to fold it by the wrong button, be obliging! actionDone |= foldLine(line).isValid(); } // At this point we should be always true return actionDone; } KTextEditor::View::ViewMode KTextEditor::ViewPrivate::viewMode() const { return currentInputMode()->viewMode(); } QString KTextEditor::ViewPrivate::viewModeHuman() const { QString currentMode = currentInputMode()->viewModeHuman(); /** * append read-only if needed */ if (!doc()->isReadWrite()) { currentMode = i18n("(R/O) %1", currentMode); } /** * return full mode */ return currentMode; } KTextEditor::View::InputMode KTextEditor::ViewPrivate::viewInputMode() const { return currentInputMode()->viewInputMode(); } QString KTextEditor::ViewPrivate::viewInputModeHuman() const { return currentInputMode()->viewInputModeHuman(); } void KTextEditor::ViewPrivate::setInputMode(KTextEditor::View::InputMode mode) { if (currentInputMode()->viewInputMode() == mode) { return; } if (!m_viewInternal->m_inputModes.contains(mode)) { return; } m_viewInternal->m_currentInputMode->deactivate(); m_viewInternal->m_currentInputMode = m_viewInternal->m_inputModes[mode]; m_viewInternal->m_currentInputMode->activate(); config()->setValue(KateViewConfig::InputMode, mode); // TODO: this could be called from read config procedure, so it's not a good idea to set a specific view mode here /* small duplication, but need to do this if not toggled by action */ const auto inputModeActions = m_inputModeActions->actions(); for (QAction *action : inputModeActions) { if (static_cast(action->data().toInt()) == mode) { action->setChecked(true); break; } } /* inform the rest of the system about the change */ emit viewInputModeChanged(this, mode); emit viewModeChanged(this, viewMode()); } void KTextEditor::ViewPrivate::slotDocumentAboutToReload() { if (doc()->isAutoReload()) { const int lastVisibleLine = m_viewInternal->endLine(); const int currentLine = cursorPosition().line(); m_gotoBottomAfterReload = (lastVisibleLine == currentLine) && (currentLine == doc()->lastLine()); if (!m_gotoBottomAfterReload) { // Ensure the view jumps not back when user scrolls around const int firstVisibleLine = 1 + lastVisibleLine - m_viewInternal->linesDisplayed(); const int newLine = qBound(firstVisibleLine, currentLine, lastVisibleLine); setCursorPositionVisual(KTextEditor::Cursor(newLine, cursorPosition().column())); } } else { m_gotoBottomAfterReload = false; } } void KTextEditor::ViewPrivate::slotDocumentReloaded() { if (m_gotoBottomAfterReload) { bottom(); } } void KTextEditor::ViewPrivate::slotGotFocus() { // qCDebug(LOG_KTE) << "KTextEditor::ViewPrivate::slotGotFocus"; currentInputMode()->gotFocus(); /** * update current view and scrollbars * it is needed for styles that implement different frame and scrollbar * rendering when focused */ update(); if (m_viewInternal->m_lineScroll->isVisible()) { m_viewInternal->m_lineScroll->update(); } if (m_viewInternal->m_columnScroll->isVisible()) { m_viewInternal->m_columnScroll->update(); } emit focusIn(this); } void KTextEditor::ViewPrivate::slotLostFocus() { // qCDebug(LOG_KTE) << "KTextEditor::ViewPrivate::slotLostFocus"; currentInputMode()->lostFocus(); /** * update current view and scrollbars * it is needed for styles that implement different frame and scrollbar * rendering when focused */ update(); if (m_viewInternal->m_lineScroll->isVisible()) { m_viewInternal->m_lineScroll->update(); } if (m_viewInternal->m_columnScroll->isVisible()) { m_viewInternal->m_columnScroll->update(); } emit focusOut(this); } void KTextEditor::ViewPrivate::setDynWrapIndicators(int mode) { config()->setValue(KateViewConfig::DynWordWrapIndicators, mode); } bool KTextEditor::ViewPrivate::isOverwriteMode() const { return doc()->config()->ovr(); } void KTextEditor::ViewPrivate::reloadFile() { // bookmarks and cursor positions are temporarily saved by the document doc()->documentReload(); } void KTextEditor::ViewPrivate::slotReadWriteChanged() { if (m_toggleWriteLock) { m_toggleWriteLock->setChecked(!doc()->isReadWrite()); } m_cut->setEnabled(doc()->isReadWrite() && (selection() || m_config->smartCopyCut())); m_paste->setEnabled(doc()->isReadWrite()); m_pasteMenu->setEnabled(doc()->isReadWrite() && !KTextEditor::EditorPrivate::self()->clipboardHistory().isEmpty()); m_setEndOfLine->setEnabled(doc()->isReadWrite()); static const auto l = {QStringLiteral("edit_replace"), QStringLiteral("tools_spelling"), QStringLiteral("tools_indent"), QStringLiteral("tools_unindent"), QStringLiteral("tools_cleanIndent"), QStringLiteral("tools_align"), QStringLiteral("tools_comment"), QStringLiteral("tools_uncomment"), QStringLiteral("tools_toggle_comment"), QStringLiteral("tools_uppercase"), QStringLiteral("tools_lowercase"), QStringLiteral("tools_capitalize"), QStringLiteral("tools_join_lines"), QStringLiteral("tools_apply_wordwrap"), QStringLiteral("tools_spelling_from_cursor"), QStringLiteral("tools_spelling_selection")}; for (const auto &action : l) { QAction *a = actionCollection()->action(action); if (a) { a->setEnabled(doc()->isReadWrite()); } } slotUpdateUndo(); currentInputMode()->readWriteChanged(doc()->isReadWrite()); // => view mode changed emit viewModeChanged(this, viewMode()); emit viewInputModeChanged(this, viewInputMode()); } void KTextEditor::ViewPrivate::slotClipboardHistoryChanged() { m_pasteMenu->setEnabled(doc()->isReadWrite() && !KTextEditor::EditorPrivate::self()->clipboardHistory().isEmpty()); } void KTextEditor::ViewPrivate::slotUpdateUndo() { if (doc()->readOnly()) { return; } m_editUndo->setEnabled(doc()->isReadWrite() && doc()->undoCount() > 0); m_editRedo->setEnabled(doc()->isReadWrite() && doc()->redoCount() > 0); } bool KTextEditor::ViewPrivate::setCursorPositionInternal(const KTextEditor::Cursor &position, uint tabwidth, bool calledExternally) { Kate::TextLine l = doc()->kateTextLine(position.line()); if (!l) { return false; } QString line_str = doc()->line(position.line()); int x = 0; int z = 0; for (; z < line_str.length() && z < position.column(); z++) { if (line_str[z] == QLatin1Char('\t')) { x += tabwidth - (x % tabwidth); } else { x++; } } if (blockSelection()) if (z < position.column()) { x += position.column() - z; } m_viewInternal->updateCursor(KTextEditor::Cursor(position.line(), x), false, calledExternally /* force center for external calls, see bug 408418 */, calledExternally); return true; } void KTextEditor::ViewPrivate::toggleInsert() { doc()->config()->setOvr(!doc()->config()->ovr()); m_toggleInsert->setChecked(isOverwriteMode()); emit viewModeChanged(this, viewMode()); emit viewInputModeChanged(this, viewInputMode()); } void KTextEditor::ViewPrivate::slotSaveCanceled(const QString &error) { if (!error.isEmpty()) { // happens when canceling a job KMessageBox::error(this, error); } } void KTextEditor::ViewPrivate::gotoLine() { gotoBar()->updateData(); bottomViewBar()->showBarWidget(gotoBar()); } void KTextEditor::ViewPrivate::changeDictionary() { dictionaryBar()->updateData(); bottomViewBar()->showBarWidget(dictionaryBar()); } void KTextEditor::ViewPrivate::joinLines() { int first = selectionRange().start().line(); int last = selectionRange().end().line(); // int left = doc()->line( last ).length() - doc()->selEndCol(); if (first == last) { first = cursorPosition().line(); last = first + 1; } doc()->joinLines(first, last); } void KTextEditor::ViewPrivate::readSessionConfig(const KConfigGroup &config, const QSet &flags) { Q_UNUSED(flags) // cursor position setCursorPositionInternal(KTextEditor::Cursor(config.readEntry("CursorLine", 0), config.readEntry("CursorColumn", 0))); m_config->setDynWordWrap(config.readEntry("Dynamic Word Wrap", false)); // restore text folding m_savedFoldingState = QJsonDocument::fromJson(config.readEntry("TextFolding", QByteArray())); applyFoldingState(); for (KateAbstractInputMode *mode : qAsConst(m_viewInternal->m_inputModes)) { mode->readSessionConfig(config); } } void KTextEditor::ViewPrivate::writeSessionConfig(KConfigGroup &config, const QSet &flags) { Q_UNUSED(flags) // cursor position config.writeEntry("CursorLine", cursorPosition().line()); config.writeEntry("CursorColumn", cursorPosition().column()); config.writeEntry("Dynamic Word Wrap", m_config->dynWordWrap()); // save text folding state saveFoldingState(); config.writeEntry("TextFolding", m_savedFoldingState.toJson(QJsonDocument::Compact)); m_savedFoldingState = QJsonDocument(); for (KateAbstractInputMode *mode : qAsConst(m_viewInternal->m_inputModes)) { mode->writeSessionConfig(config); } } int KTextEditor::ViewPrivate::getEol() const { return doc()->config()->eol(); } void KTextEditor::ViewPrivate::setEol(int eol) { if (!doc()->isReadWrite()) { return; } if (m_updatingDocumentConfig) { return; } if (eol != doc()->config()->eol()) { doc()->setModified(true); // mark modified (bug #143120) doc()->config()->setEol(eol); } } void KTextEditor::ViewPrivate::setAddBom(bool enabled) { if (!doc()->isReadWrite()) { return; } if (m_updatingDocumentConfig) { return; } doc()->config()->setBom(enabled); doc()->bomSetByUser(); } void KTextEditor::ViewPrivate::setIconBorder(bool enable) { config()->setValue(KateViewConfig::ShowIconBar, enable); } void KTextEditor::ViewPrivate::toggleIconBorder() { config()->setValue(KateViewConfig::ShowIconBar, !config()->iconBar()); } void KTextEditor::ViewPrivate::setLineNumbersOn(bool enable) { config()->setValue(KateViewConfig::ShowLineNumbers, enable); } void KTextEditor::ViewPrivate::toggleLineNumbersOn() { config()->setValue(KateViewConfig::ShowLineNumbers, !config()->lineNumbers()); } void KTextEditor::ViewPrivate::setScrollBarMarks(bool enable) { config()->setValue(KateViewConfig::ShowScrollBarMarks, enable); } void KTextEditor::ViewPrivate::toggleScrollBarMarks() { config()->setValue(KateViewConfig::ShowScrollBarMarks, !config()->scrollBarMarks()); } void KTextEditor::ViewPrivate::setScrollBarMiniMap(bool enable) { config()->setValue(KateViewConfig::ShowScrollBarMiniMap, enable); } void KTextEditor::ViewPrivate::toggleScrollBarMiniMap() { config()->setValue(KateViewConfig::ShowScrollBarMiniMap, !config()->scrollBarMiniMap()); } void KTextEditor::ViewPrivate::setScrollBarMiniMapAll(bool enable) { config()->setValue(KateViewConfig::ShowScrollBarMiniMapAll, enable); } void KTextEditor::ViewPrivate::toggleScrollBarMiniMapAll() { config()->setValue(KateViewConfig::ShowScrollBarMiniMapAll, !config()->scrollBarMiniMapAll()); } void KTextEditor::ViewPrivate::setScrollBarMiniMapWidth(int width) { config()->setValue(KateViewConfig::ScrollBarMiniMapWidth, width); } void KTextEditor::ViewPrivate::toggleDynWordWrap() { config()->setDynWordWrap(!config()->dynWordWrap()); } void KTextEditor::ViewPrivate::toggleWWMarker() { m_renderer->config()->setWordWrapMarker(!m_renderer->config()->wordWrapMarker()); } void KTextEditor::ViewPrivate::toggleNPSpaces() { m_renderer->setShowNonPrintableSpaces(!m_renderer->showNonPrintableSpaces()); m_viewInternal->update(); // force redraw } void KTextEditor::ViewPrivate::toggleWordCount(bool on) { config()->setShowWordCount(on); } void KTextEditor::ViewPrivate::setFoldingMarkersOn(bool enable) { config()->setValue(KateViewConfig::ShowFoldingBar, enable); } void KTextEditor::ViewPrivate::toggleFoldingMarkers() { config()->setValue(KateViewConfig::ShowFoldingBar, !config()->foldingBar()); } bool KTextEditor::ViewPrivate::iconBorder() { return m_viewInternal->m_leftBorder->iconBorderOn(); } bool KTextEditor::ViewPrivate::lineNumbersOn() { return m_viewInternal->m_leftBorder->lineNumbersOn(); } bool KTextEditor::ViewPrivate::scrollBarMarks() { return m_viewInternal->m_lineScroll->showMarks(); } bool KTextEditor::ViewPrivate::scrollBarMiniMap() { return m_viewInternal->m_lineScroll->showMiniMap(); } int KTextEditor::ViewPrivate::dynWrapIndicators() { return m_viewInternal->m_leftBorder->dynWrapIndicators(); } bool KTextEditor::ViewPrivate::foldingMarkersOn() { return m_viewInternal->m_leftBorder->foldingMarkersOn(); } void KTextEditor::ViewPrivate::toggleWriteLock() { doc()->setReadWrite(!doc()->isReadWrite()); } void KTextEditor::ViewPrivate::registerTextHintProvider(KTextEditor::TextHintProvider *provider) { m_viewInternal->registerTextHintProvider(provider); } void KTextEditor::ViewPrivate::unregisterTextHintProvider(KTextEditor::TextHintProvider *provider) { m_viewInternal->unregisterTextHintProvider(provider); } void KTextEditor::ViewPrivate::setTextHintDelay(int delay) { m_viewInternal->setTextHintDelay(delay); } int KTextEditor::ViewPrivate::textHintDelay() const { return m_viewInternal->textHintDelay(); } void KTextEditor::ViewPrivate::find() { currentInputMode()->find(); } void KTextEditor::ViewPrivate::findSelectedForwards() { currentInputMode()->findSelectedForwards(); } void KTextEditor::ViewPrivate::findSelectedBackwards() { currentInputMode()->findSelectedBackwards(); } void KTextEditor::ViewPrivate::replace() { currentInputMode()->findReplace(); } void KTextEditor::ViewPrivate::findNext() { currentInputMode()->findNext(); } void KTextEditor::ViewPrivate::findPrevious() { currentInputMode()->findPrevious(); } void KTextEditor::ViewPrivate::slotSelectionChanged() { m_copy->setEnabled(selection() || m_config->smartCopyCut()); m_deSelect->setEnabled(selection()); m_copyHtmlAction->setEnabled(selection()); // update highlighting of current selected word selectionChangedForHighlights(); if (doc()->readOnly()) { return; } m_cut->setEnabled(selection() || m_config->smartCopyCut()); } void KTextEditor::ViewPrivate::switchToCmdLine() { currentInputMode()->activateCommandLine(); } KateRenderer *KTextEditor::ViewPrivate::renderer() { return m_renderer; } void KTextEditor::ViewPrivate::updateConfig() { if (m_startingUp) { return; } // dyn. word wrap & markers if (m_hasWrap != config()->dynWordWrap()) { m_viewInternal->prepareForDynWrapChange(); m_hasWrap = config()->dynWordWrap(); m_viewInternal->dynWrapChanged(); m_setDynWrapIndicators->setEnabled(config()->dynWordWrap()); m_toggleDynWrap->setChecked(config()->dynWordWrap()); } m_viewInternal->m_leftBorder->setDynWrapIndicators(config()->dynWordWrapIndicators()); m_setDynWrapIndicators->setCurrentItem(config()->dynWordWrapIndicators()); // line numbers m_viewInternal->m_leftBorder->setLineNumbersOn(config()->lineNumbers()); m_toggleLineNumbers->setChecked(config()->lineNumbers()); // icon bar m_viewInternal->m_leftBorder->setIconBorderOn(config()->iconBar()); m_toggleIconBar->setChecked(config()->iconBar()); // scrollbar marks m_viewInternal->m_lineScroll->setShowMarks(config()->scrollBarMarks()); m_toggleScrollBarMarks->setChecked(config()->scrollBarMarks()); // scrollbar mini-map m_viewInternal->m_lineScroll->setShowMiniMap(config()->scrollBarMiniMap()); m_toggleScrollBarMiniMap->setChecked(config()->scrollBarMiniMap()); // scrollbar mini-map - (whole document) m_viewInternal->m_lineScroll->setMiniMapAll(config()->scrollBarMiniMapAll()); // m_toggleScrollBarMiniMapAll->setChecked( config()->scrollBarMiniMapAll() ); // scrollbar mini-map.width m_viewInternal->m_lineScroll->setMiniMapWidth(config()->scrollBarMiniMapWidth()); // misc edit m_toggleBlockSelection->setChecked(blockSelection()); m_toggleInsert->setChecked(isOverwriteMode()); updateFoldingConfig(); // bookmark m_bookmarks->setSorting((KateBookmarks::Sorting)config()->bookmarkSort()); m_viewInternal->setAutoCenterLines(config()->autoCenterLines()); for (KateAbstractInputMode *input : qAsConst(m_viewInternal->m_inputModes)) { input->updateConfig(); } setInputMode(config()->inputMode()); reflectOnTheFlySpellCheckStatus(doc()->isOnTheFlySpellCheckingEnabled()); // register/unregister word completion... bool wc = config()->wordCompletion(); if (wc != isCompletionModelRegistered(KTextEditor::EditorPrivate::self()->wordCompletionModel())) { if (wc) registerCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); else unregisterCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); } bool kc = config()->keywordCompletion(); if (kc != isCompletionModelRegistered(KTextEditor::EditorPrivate::self()->keywordCompletionModel())) { if (kc) registerCompletionModel(KTextEditor::EditorPrivate::self()->keywordCompletionModel()); else unregisterCompletionModel(KTextEditor::EditorPrivate::self()->keywordCompletionModel()); } m_cut->setEnabled(doc()->isReadWrite() && (selection() || m_config->smartCopyCut())); m_copy->setEnabled(selection() || m_config->smartCopyCut()); // if not disabled, update status bar if (m_statusBar) { m_statusBar->updateStatus(); } // now redraw... m_viewInternal->cache()->clear(); tagAll(); updateView(true); emit configChanged(); } void KTextEditor::ViewPrivate::updateDocumentConfig() { if (m_startingUp) { return; } m_updatingDocumentConfig = true; m_setEndOfLine->setCurrentItem(doc()->config()->eol()); m_addBom->setChecked(doc()->config()->bom()); m_updatingDocumentConfig = false; // maybe block selection or wrap-cursor mode changed ensureCursorColumnValid(); // first change this m_renderer->setTabWidth(doc()->config()->tabWidth()); m_renderer->setIndentWidth(doc()->config()->indentationWidth()); // now redraw... m_viewInternal->cache()->clear(); tagAll(); updateView(true); } void KTextEditor::ViewPrivate::updateRendererConfig() { if (m_startingUp) { return; } m_toggleWWMarker->setChecked(m_renderer->config()->wordWrapMarker()); m_viewInternal->updateBracketMarkAttributes(); m_viewInternal->updateBracketMarks(); // now redraw... m_viewInternal->cache()->clear(); tagAll(); m_viewInternal->updateView(true); // update the left border right, for example linenumbers m_viewInternal->m_leftBorder->updateFont(); m_viewInternal->m_leftBorder->repaint(); m_viewInternal->m_lineScroll->queuePixmapUpdate(); currentInputMode()->updateRendererConfig(); // @@ showIndentLines is not cached anymore. // m_renderer->setShowIndentLines (m_renderer->config()->showIndentationLines()); emit configChanged(); } void KTextEditor::ViewPrivate::updateFoldingConfig() { // folding bar m_viewInternal->m_leftBorder->setFoldingMarkersOn(config()->foldingBar()); m_toggleFoldingMarkers->setChecked(config()->foldingBar()); if (hasCommentInFirstLine(m_doc)) { if (config()->foldFirstLine() && !m_autoFoldedFirstLine) { foldLine(0); m_autoFoldedFirstLine = true; } else if (!config()->foldFirstLine() && m_autoFoldedFirstLine) { unfoldLine(0); m_autoFoldedFirstLine = false; } } else { m_autoFoldedFirstLine = false; } #if 0 // FIXME: FOLDING const QStringList l = { QStringLiteral("folding_toplevel") , QStringLiteral("folding_expandtoplevel") , QStringLiteral("folding_toggle_current") , QStringLiteral("folding_toggle_in_current") }; QAction *a = 0; for (int z = 0; z < l.size(); z++) if ((a = actionCollection()->action(l[z].toAscii().constData()))) { a->setEnabled(doc()->highlight() && doc()->highlight()->allowsFolding()); } #endif } void KTextEditor::ViewPrivate::ensureCursorColumnValid() { KTextEditor::Cursor c = m_viewInternal->cursorPosition(); // make sure the cursor is valid: // - in block selection mode or if wrap cursor is off, the column is arbitrary // - otherwise: it's bounded by the line length if (!blockSelection() && wrapCursor() && (!c.isValid() || c.column() > doc()->lineLength(c.line()))) { c.setColumn(doc()->kateTextLine(cursorPosition().line())->length()); setCursorPosition(c); } } // BEGIN EDIT STUFF void KTextEditor::ViewPrivate::editStart() { m_viewInternal->editStart(); } void KTextEditor::ViewPrivate::editEnd(int editTagLineStart, int editTagLineEnd, bool tagFrom) { m_viewInternal->editEnd(editTagLineStart, editTagLineEnd, tagFrom); } void KTextEditor::ViewPrivate::editSetCursor(const KTextEditor::Cursor &cursor) { m_viewInternal->editSetCursor(cursor); } // END // BEGIN TAG & CLEAR bool KTextEditor::ViewPrivate::tagLine(const KTextEditor::Cursor &virtualCursor) { return m_viewInternal->tagLine(virtualCursor); } bool KTextEditor::ViewPrivate::tagRange(const KTextEditor::Range &range, bool realLines) { return m_viewInternal->tagRange(range, realLines); } bool KTextEditor::ViewPrivate::tagLines(int start, int end, bool realLines) { return m_viewInternal->tagLines(start, end, realLines); } bool KTextEditor::ViewPrivate::tagLines(KTextEditor::Cursor start, KTextEditor::Cursor end, bool realCursors) { return m_viewInternal->tagLines(start, end, realCursors); } void KTextEditor::ViewPrivate::tagAll() { m_viewInternal->tagAll(); } void KTextEditor::ViewPrivate::clear() { m_viewInternal->clear(); } void KTextEditor::ViewPrivate::repaintText(bool paintOnlyDirty) { if (paintOnlyDirty) { m_viewInternal->updateDirty(); } else { m_viewInternal->update(); } } void KTextEditor::ViewPrivate::updateView(bool changed) { // qCDebug(LOG_KTE) << "KTextEditor::ViewPrivate::updateView"; m_viewInternal->updateView(changed); m_viewInternal->m_leftBorder->update(); } // END void KTextEditor::ViewPrivate::slotHlChanged() { KateHighlighting *hl = doc()->highlight(); bool ok(!hl->getCommentStart(0).isEmpty() || !hl->getCommentSingleLineStart(0).isEmpty()); if (actionCollection()->action(QStringLiteral("tools_comment"))) { actionCollection()->action(QStringLiteral("tools_comment"))->setEnabled(ok); } if (actionCollection()->action(QStringLiteral("tools_uncomment"))) { actionCollection()->action(QStringLiteral("tools_uncomment"))->setEnabled(ok); } if (actionCollection()->action(QStringLiteral("tools_toggle_comment"))) { actionCollection()->action(QStringLiteral("tools_toggle_comment"))->setEnabled(ok); } // show folding bar if "view defaults" says so, otherwise enable/disable only the menu entry updateFoldingConfig(); } int KTextEditor::ViewPrivate::virtualCursorColumn() const { return doc()->toVirtualColumn(m_viewInternal->cursorPosition()); } void KTextEditor::ViewPrivate::notifyMousePositionChanged(const KTextEditor::Cursor &newPosition) { emit mousePositionChanged(this, newPosition); } // BEGIN KTextEditor::SelectionInterface stuff bool KTextEditor::ViewPrivate::setSelection(const KTextEditor::Range &selection) { /** * anything to do? */ if (selection == m_selection) { return true; } /** * backup old range */ KTextEditor::Range oldSelection = m_selection; /** * set new range */ m_selection.setRange(selection.isEmpty() ? KTextEditor::Range::invalid() : selection); /** * trigger update of correct area */ tagSelection(oldSelection); repaintText(true); /** * emit holy signal */ emit selectionChanged(this); /** * be done */ return true; } bool KTextEditor::ViewPrivate::clearSelection() { return clearSelection(true); } bool KTextEditor::ViewPrivate::clearSelection(bool redraw, bool finishedChangingSelection) { /** * no selection, nothing to do... */ if (!selection()) { return false; } /** * backup old range */ KTextEditor::Range oldSelection = m_selection; /** * invalidate current selection */ m_selection.setRange(KTextEditor::Range::invalid()); /** * trigger update of correct area */ tagSelection(oldSelection); if (redraw) { repaintText(true); } /** * emit holy signal */ if (finishedChangingSelection) { emit selectionChanged(this); } /** * be done */ return true; } bool KTextEditor::ViewPrivate::selection() const { if (!wrapCursor()) { return m_selection != KTextEditor::Range::invalid(); } else { return m_selection.toRange().isValid(); } } QString KTextEditor::ViewPrivate::selectionText() const { return doc()->text(m_selection, blockSelect); } bool KTextEditor::ViewPrivate::removeSelectedText() { if (!selection()) { return false; } doc()->editStart(); // Optimization: clear selection before removing text KTextEditor::Range selection = m_selection; doc()->removeText(selection, blockSelect); // don't redraw the cleared selection - that's done in editEnd(). if (blockSelect) { int selectionColumn = qMin(doc()->toVirtualColumn(selection.start()), doc()->toVirtualColumn(selection.end())); KTextEditor::Range newSelection = selection; newSelection.setStart(KTextEditor::Cursor(newSelection.start().line(), doc()->fromVirtualColumn(newSelection.start().line(), selectionColumn))); newSelection.setEnd(KTextEditor::Cursor(newSelection.end().line(), doc()->fromVirtualColumn(newSelection.end().line(), selectionColumn))); setSelection(newSelection); setCursorPositionInternal(newSelection.start()); } else { clearSelection(false); } doc()->editEnd(); return true; } bool KTextEditor::ViewPrivate::selectAll() { setBlockSelection(false); top(); shiftBottom(); return true; } bool KTextEditor::ViewPrivate::cursorSelected(const KTextEditor::Cursor &cursor) { KTextEditor::Cursor ret = cursor; if ((!blockSelect) && (ret.column() < 0)) { ret.setColumn(0); } if (blockSelect) return cursor.line() >= m_selection.start().line() && ret.line() <= m_selection.end().line() && ret.column() >= m_selection.start().column() && ret.column() <= m_selection.end().column(); else { return m_selection.toRange().contains(cursor) || m_selection.end() == cursor; } } bool KTextEditor::ViewPrivate::lineSelected(int line) { return !blockSelect && m_selection.toRange().containsLine(line); } bool KTextEditor::ViewPrivate::lineEndSelected(const KTextEditor::Cursor &lineEndPos) { return (!blockSelect) && (lineEndPos.line() > m_selection.start().line() || (lineEndPos.line() == m_selection.start().line() && (m_selection.start().column() < lineEndPos.column() || lineEndPos.column() == -1))) && (lineEndPos.line() < m_selection.end().line() || (lineEndPos.line() == m_selection.end().line() && (lineEndPos.column() <= m_selection.end().column() && lineEndPos.column() != -1))); } bool KTextEditor::ViewPrivate::lineHasSelected(int line) { return selection() && m_selection.toRange().containsLine(line); } bool KTextEditor::ViewPrivate::lineIsSelection(int line) { return (line == m_selection.start().line() && line == m_selection.end().line()); } void KTextEditor::ViewPrivate::tagSelection(const KTextEditor::Range &oldSelection) { if (selection()) { if (oldSelection.start().line() == -1) { // We have to tag the whole lot if // 1) we have a selection, and: // a) it's new; or tagLines(m_selection, true); } else if (blockSelection() && (oldSelection.start().column() != m_selection.start().column() || oldSelection.end().column() != m_selection.end().column())) { // b) we're in block selection mode and the columns have changed tagLines(m_selection, true); tagLines(oldSelection, true); } else { if (oldSelection.start() != m_selection.start()) { if (oldSelection.start() < m_selection.start()) { tagLines(oldSelection.start(), m_selection.start(), true); } else { tagLines(m_selection.start(), oldSelection.start(), true); } } if (oldSelection.end() != m_selection.end()) { if (oldSelection.end() < m_selection.end()) { tagLines(oldSelection.end(), m_selection.end(), true); } else { tagLines(m_selection.end(), oldSelection.end(), true); } } } } else { // No more selection, clean up tagLines(oldSelection, true); } } void KTextEditor::ViewPrivate::selectWord(const KTextEditor::Cursor &cursor) { setSelection(doc()->wordRangeAt(cursor)); } void KTextEditor::ViewPrivate::selectLine(const KTextEditor::Cursor &cursor) { int line = cursor.line(); if (line + 1 >= doc()->lines()) { setSelection(KTextEditor::Range(line, 0, line, doc()->lineLength(line))); } else { setSelection(KTextEditor::Range(line, 0, line + 1, 0)); } } void KTextEditor::ViewPrivate::cut() { if (!selection() && !m_config->smartCopyCut()) { return; } copy(); if (!selection()) { selectLine(cursorPosition()); } removeSelectedText(); } void KTextEditor::ViewPrivate::copy() const { QString text; if (!selection()) { if (!m_config->smartCopyCut()) { return; } text = doc()->line(cursorPosition().line()) + QLatin1Char('\n'); m_viewInternal->moveEdge(KateViewInternal::left, false); } else { text = selectionText(); } // copy to clipboard and our history! KTextEditor::EditorPrivate::self()->copyToClipboard(text); } void KTextEditor::ViewPrivate::applyWordWrap() { 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 // BEGIN KTextEditor::BlockSelectionInterface stuff bool KTextEditor::ViewPrivate::blockSelection() const { return blockSelect; } bool KTextEditor::ViewPrivate::setBlockSelection(bool on) { if (on != blockSelect) { blockSelect = on; KTextEditor::Range oldSelection = m_selection; const bool hadSelection = clearSelection(false, false); setSelection(oldSelection); m_toggleBlockSelection->setChecked(blockSelection()); // when leaving block selection mode, if cursor is at an invalid position or past the end of the // line, move the cursor to the last column of the current line unless cursor wrapping is off ensureCursorColumnValid(); if (!hadSelection) { // emit selectionChanged() according to the KTextEditor::View api // documentation also if there is no selection around. This is needed, // as e.g. the Kate App status bar uses this signal to update the state // of the selection mode (block selection, line based selection) emit selectionChanged(this); } } return true; } bool KTextEditor::ViewPrivate::toggleBlockSelection() { m_toggleBlockSelection->setChecked(!blockSelect); return setBlockSelection(!blockSelect); } bool KTextEditor::ViewPrivate::wrapCursor() const { return !blockSelection(); } // END void KTextEditor::ViewPrivate::slotTextInserted(KTextEditor::View *view, const KTextEditor::Cursor &position, const QString &text) { emit textInserted(view, position, text); } bool KTextEditor::ViewPrivate::insertTemplateInternal(const KTextEditor::Cursor &c, const QString &templateString, const QString &script) { /** * no empty templates */ if (templateString.isEmpty()) { return false; } /** * not for read-only docs */ if (!doc()->isReadWrite()) { return false; } /** * only one handler maybe active at a time; store it in the document. * Clear it first to make sure at no time two handlers are active at once */ doc()->setActiveTemplateHandler(nullptr); doc()->setActiveTemplateHandler(new KateTemplateHandler(this, c, templateString, script, doc()->undoManager())); return true; } bool KTextEditor::ViewPrivate::tagLines(KTextEditor::Range range, bool realRange) { return tagLines(range.start(), range.end(), realRange); } void KTextEditor::ViewPrivate::deactivateEditActions() { for (QAction *action : qAsConst(m_editActions)) { action->setEnabled(false); } } void KTextEditor::ViewPrivate::activateEditActions() { for (QAction *action : qAsConst(m_editActions)) { action->setEnabled(true); } } bool KTextEditor::ViewPrivate::mouseTrackingEnabled() const { // FIXME support return true; } bool KTextEditor::ViewPrivate::setMouseTrackingEnabled(bool) { // FIXME support return true; } bool KTextEditor::ViewPrivate::isCompletionActive() const { return completionWidget()->isCompletionActive(); } KateCompletionWidget *KTextEditor::ViewPrivate::completionWidget() const { if (!m_completionWidget) { m_completionWidget = new KateCompletionWidget(const_cast(this)); } return m_completionWidget; } void KTextEditor::ViewPrivate::startCompletion(const KTextEditor::Range &word, KTextEditor::CodeCompletionModel *model) { completionWidget()->startCompletion(word, model); } void KTextEditor::ViewPrivate::abortCompletion() { completionWidget()->abortCompletion(); } void KTextEditor::ViewPrivate::forceCompletion() { completionWidget()->execute(); } void KTextEditor::ViewPrivate::registerCompletionModel(KTextEditor::CodeCompletionModel *model) { completionWidget()->registerCompletionModel(model); } void KTextEditor::ViewPrivate::unregisterCompletionModel(KTextEditor::CodeCompletionModel *model) { completionWidget()->unregisterCompletionModel(model); } bool KTextEditor::ViewPrivate::isCompletionModelRegistered(KTextEditor::CodeCompletionModel *model) const { return completionWidget()->isCompletionModelRegistered(model); } bool KTextEditor::ViewPrivate::isAutomaticInvocationEnabled() const { return !m_temporaryAutomaticInvocationDisabled && m_config->automaticCompletionInvocation(); } void KTextEditor::ViewPrivate::setAutomaticInvocationEnabled(bool enabled) { config()->setValue(KateViewConfig::AutomaticCompletionInvocation, enabled); } void KTextEditor::ViewPrivate::sendCompletionExecuted(const KTextEditor::Cursor &position, KTextEditor::CodeCompletionModel *model, const QModelIndex &index) { emit completionExecuted(this, position, model, index); } void KTextEditor::ViewPrivate::sendCompletionAborted() { emit completionAborted(this); } void KTextEditor::ViewPrivate::paste(const QString *textToPaste) { m_temporaryAutomaticInvocationDisabled = true; doc()->paste(this, textToPaste ? *textToPaste : QApplication::clipboard()->text(QClipboard::Clipboard)); m_temporaryAutomaticInvocationDisabled = false; } bool KTextEditor::ViewPrivate::setCursorPosition(KTextEditor::Cursor position) { return setCursorPositionInternal(position, 1, true); } KTextEditor::Cursor KTextEditor::ViewPrivate::cursorPosition() const { return m_viewInternal->cursorPosition(); } KTextEditor::Cursor KTextEditor::ViewPrivate::cursorPositionVirtual() const { return KTextEditor::Cursor(m_viewInternal->cursorPosition().line(), virtualCursorColumn()); } QPoint KTextEditor::ViewPrivate::cursorToCoordinate(const KTextEditor::Cursor &cursor) const { // map from ViewInternal to View coordinates const QPoint pt = m_viewInternal->cursorToCoordinate(cursor, true, false); return pt == QPoint(-1, -1) ? pt : m_viewInternal->mapToParent(pt); } KTextEditor::Cursor KTextEditor::ViewPrivate::coordinatesToCursor(const QPoint &coords) const { // map from View to ViewInternal coordinates return m_viewInternal->coordinatesToCursor(m_viewInternal->mapFromParent(coords), false); } QPoint KTextEditor::ViewPrivate::cursorPositionCoordinates() const { // map from ViewInternal to View coordinates const QPoint pt = m_viewInternal->cursorCoordinates(false); return pt == QPoint(-1, -1) ? pt : m_viewInternal->mapToParent(pt); } void KTextEditor::ViewPrivate::setScrollPositionInternal(KTextEditor::Cursor &cursor) { m_viewInternal->scrollPos(cursor, false, true, false); } void KTextEditor::ViewPrivate::setHorizontalScrollPositionInternal(int x) { m_viewInternal->scrollColumns(x); } KTextEditor::Cursor KTextEditor::ViewPrivate::maxScrollPositionInternal() const { return m_viewInternal->maxStartPos(true); } int KTextEditor::ViewPrivate::firstDisplayedLineInternal(LineType lineType) const { if (lineType == RealLine) { return m_textFolding.visibleLineToLine(m_viewInternal->startLine()); } else { return m_viewInternal->startLine(); } } int KTextEditor::ViewPrivate::lastDisplayedLineInternal(LineType lineType) const { if (lineType == RealLine) { return m_textFolding.visibleLineToLine(m_viewInternal->endLine()); } else { return m_viewInternal->endLine(); } } QRect KTextEditor::ViewPrivate::textAreaRectInternal() const { const auto sourceRect = m_viewInternal->rect(); const auto topLeft = m_viewInternal->mapTo(this, sourceRect.topLeft()); const auto bottomRight = m_viewInternal->mapTo(this, sourceRect.bottomRight()); return {topLeft, bottomRight}; } bool KTextEditor::ViewPrivate::setCursorPositionVisual(const KTextEditor::Cursor &position) { return setCursorPositionInternal(position, doc()->config()->tabWidth(), true); } QString KTextEditor::ViewPrivate::currentTextLine() { return doc()->line(cursorPosition().line()); } QTextLayout *KTextEditor::ViewPrivate::textLayout(int line) const { KateLineLayoutPtr thisLine = m_viewInternal->cache()->line(line); return thisLine->isValid() ? thisLine->layout() : nullptr; } QTextLayout *KTextEditor::ViewPrivate::textLayout(const KTextEditor::Cursor &pos) const { KateLineLayoutPtr thisLine = m_viewInternal->cache()->line(pos); return thisLine->isValid() ? thisLine->layout() : nullptr; } void KTextEditor::ViewPrivate::indent() { KTextEditor::Cursor c(cursorPosition().line(), 0); KTextEditor::Range r = selection() ? selectionRange() : KTextEditor::Range(c, c); doc()->indent(r, 1); } void KTextEditor::ViewPrivate::unIndent() { KTextEditor::Cursor c(cursorPosition().line(), 0); KTextEditor::Range r = selection() ? selectionRange() : KTextEditor::Range(c, c); doc()->indent(r, -1); } void KTextEditor::ViewPrivate::cleanIndent() { KTextEditor::Cursor c(cursorPosition().line(), 0); KTextEditor::Range r = selection() ? selectionRange() : KTextEditor::Range(c, c); doc()->indent(r, 0); } void KTextEditor::ViewPrivate::align() { // no selection: align current line; selection: use selection range const int line = cursorPosition().line(); KTextEditor::Range alignRange(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, 0)); if (selection()) { alignRange = selectionRange(); } doc()->align(this, alignRange); } void KTextEditor::ViewPrivate::comment() { m_selection.setInsertBehaviors(Kate::TextRange::ExpandLeft | Kate::TextRange::ExpandRight); doc()->comment(this, cursorPosition().line(), cursorPosition().column(), 1); m_selection.setInsertBehaviors(Kate::TextRange::ExpandRight); } void KTextEditor::ViewPrivate::uncomment() { doc()->comment(this, cursorPosition().line(), cursorPosition().column(), -1); } void KTextEditor::ViewPrivate::toggleComment() { m_selection.setInsertBehaviors(Kate::TextRange::ExpandLeft | Kate::TextRange::ExpandRight); doc()->comment(this, cursorPosition().line(), cursorPosition().column(), 0); m_selection.setInsertBehaviors(Kate::TextRange::ExpandRight); } void KTextEditor::ViewPrivate::uppercase() { doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Uppercase); } void KTextEditor::ViewPrivate::killLine() { if (m_selection.isEmpty()) { doc()->removeLine(cursorPosition().line()); } else { doc()->editStart(); // cache endline, else that moves and we might delete complete document if last line is selected! for (int line = m_selection.end().line(), endLine = m_selection.start().line(); line >= endLine; line--) { doc()->removeLine(line); } doc()->editEnd(); } } void KTextEditor::ViewPrivate::lowercase() { doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Lowercase); } void KTextEditor::ViewPrivate::capitalize() { doc()->editStart(); doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Lowercase); doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Capitalize); doc()->editEnd(); } void KTextEditor::ViewPrivate::keyReturn() { doc()->newLine(this); m_viewInternal->iconBorder()->updateForCursorLineChange(); m_viewInternal->updateView(); } void KTextEditor::ViewPrivate::smartNewline() { const KTextEditor::Cursor cursor = cursorPosition(); const int ln = cursor.line(); Kate::TextLine line = doc()->kateTextLine(ln); int col = qMin(cursor.column(), line->firstChar()); if (col != -1) { while (line->length() > col && !(line->at(col).isLetterOrNumber() || line->at(col) == QLatin1Char('_')) && col < cursor.column()) { ++col; } } else { col = line->length(); // stay indented } doc()->editStart(); doc()->editWrapLine(ln, cursor.column()); doc()->insertText(KTextEditor::Cursor(ln + 1, 0), line->string(0, col)); doc()->editEnd(); m_viewInternal->updateView(); } void KTextEditor::ViewPrivate::noIndentNewline() { doc()->newLine(this, KTextEditor::DocumentPrivate::NoIndent); m_viewInternal->iconBorder()->updateForCursorLineChange(); m_viewInternal->updateView(); } void KTextEditor::ViewPrivate::backspace() { doc()->backspace(this, cursorPosition()); } void KTextEditor::ViewPrivate::insertTab() { doc()->insertTab(this, cursorPosition()); } void KTextEditor::ViewPrivate::deleteWordLeft() { doc()->editStart(); m_viewInternal->wordPrev(true); KTextEditor::Range selection = selectionRange(); removeSelectedText(); doc()->editEnd(); m_viewInternal->tagRange(selection, true); m_viewInternal->updateDirty(); } void KTextEditor::ViewPrivate::keyDelete() { doc()->del(this, cursorPosition()); } void KTextEditor::ViewPrivate::deleteWordRight() { doc()->editStart(); m_viewInternal->wordNext(true); KTextEditor::Range selection = selectionRange(); removeSelectedText(); doc()->editEnd(); m_viewInternal->tagRange(selection, true); m_viewInternal->updateDirty(); } void KTextEditor::ViewPrivate::transpose() { doc()->transpose(cursorPosition()); } void KTextEditor::ViewPrivate::cursorLeft() { if (selection() && !config()->persistentSelection()) { if (currentTextLine().isRightToLeft()) { m_viewInternal->updateCursor(selectionRange().end()); setSelection(KTextEditor::Range::invalid()); } else { m_viewInternal->updateCursor(selectionRange().start()); setSelection(KTextEditor::Range::invalid()); } } else { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorNextChar(); } else { m_viewInternal->cursorPrevChar(); } } } void KTextEditor::ViewPrivate::shiftCursorLeft() { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorNextChar(true); } else { m_viewInternal->cursorPrevChar(true); } } void KTextEditor::ViewPrivate::cursorRight() { if (selection() && !config()->persistentSelection()) { if (currentTextLine().isRightToLeft()) { m_viewInternal->updateCursor(selectionRange().start()); setSelection(KTextEditor::Range::invalid()); } else { m_viewInternal->updateCursor(selectionRange().end()); setSelection(KTextEditor::Range::invalid()); } } else { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorPrevChar(); } else { m_viewInternal->cursorNextChar(); } } } void KTextEditor::ViewPrivate::shiftCursorRight() { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorPrevChar(true); } else { m_viewInternal->cursorNextChar(true); } } void KTextEditor::ViewPrivate::wordLeft() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordNext(); } else { m_viewInternal->wordPrev(); } } void KTextEditor::ViewPrivate::shiftWordLeft() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordNext(true); } else { m_viewInternal->wordPrev(true); } } void KTextEditor::ViewPrivate::wordRight() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordPrev(); } else { m_viewInternal->wordNext(); } } void KTextEditor::ViewPrivate::shiftWordRight() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordPrev(true); } else { m_viewInternal->wordNext(true); } } void KTextEditor::ViewPrivate::home() { m_viewInternal->home(); } void KTextEditor::ViewPrivate::shiftHome() { m_viewInternal->home(true); } void KTextEditor::ViewPrivate::end() { m_viewInternal->end(); } void KTextEditor::ViewPrivate::shiftEnd() { m_viewInternal->end(true); } void KTextEditor::ViewPrivate::up() { m_viewInternal->cursorUp(); } void KTextEditor::ViewPrivate::shiftUp() { m_viewInternal->cursorUp(true); } void KTextEditor::ViewPrivate::down() { m_viewInternal->cursorDown(); } void KTextEditor::ViewPrivate::shiftDown() { m_viewInternal->cursorDown(true); } void KTextEditor::ViewPrivate::scrollUp() { m_viewInternal->scrollUp(); } void KTextEditor::ViewPrivate::scrollDown() { m_viewInternal->scrollDown(); } void KTextEditor::ViewPrivate::topOfView() { m_viewInternal->topOfView(); } void KTextEditor::ViewPrivate::shiftTopOfView() { m_viewInternal->topOfView(true); } void KTextEditor::ViewPrivate::bottomOfView() { m_viewInternal->bottomOfView(); } void KTextEditor::ViewPrivate::shiftBottomOfView() { m_viewInternal->bottomOfView(true); } void KTextEditor::ViewPrivate::pageUp() { m_viewInternal->pageUp(); } void KTextEditor::ViewPrivate::shiftPageUp() { m_viewInternal->pageUp(true); } void KTextEditor::ViewPrivate::pageDown() { m_viewInternal->pageDown(); } void KTextEditor::ViewPrivate::shiftPageDown() { m_viewInternal->pageDown(true); } void KTextEditor::ViewPrivate::top() { m_viewInternal->top_home(); } void KTextEditor::ViewPrivate::shiftTop() { m_viewInternal->top_home(true); } void KTextEditor::ViewPrivate::bottom() { m_viewInternal->bottom_end(); } void KTextEditor::ViewPrivate::shiftBottom() { m_viewInternal->bottom_end(true); } void KTextEditor::ViewPrivate::toMatchingBracket() { m_viewInternal->cursorToMatchingBracket(); } void KTextEditor::ViewPrivate::shiftToMatchingBracket() { m_viewInternal->cursorToMatchingBracket(true); } void KTextEditor::ViewPrivate::toPrevModifiedLine() { const int startLine = cursorPosition().line() - 1; const int line = doc()->findTouchedLine(startLine, false); if (line >= 0) { KTextEditor::Cursor c(line, 0); m_viewInternal->updateSelection(c, false); m_viewInternal->updateCursor(c); } } void KTextEditor::ViewPrivate::toNextModifiedLine() { const int startLine = cursorPosition().line() + 1; const int line = doc()->findTouchedLine(startLine, true); if (line >= 0) { KTextEditor::Cursor c(line, 0); m_viewInternal->updateSelection(c, false); m_viewInternal->updateCursor(c); } } KTextEditor::Range KTextEditor::ViewPrivate::selectionRange() const { return m_selection; } KTextEditor::Document *KTextEditor::ViewPrivate::document() const { return m_doc; } void KTextEditor::ViewPrivate::setContextMenu(QMenu *menu) { if (m_contextMenu) { disconnect(m_contextMenu, SIGNAL(aboutToShow()), this, SLOT(aboutToShowContextMenu())); disconnect(m_contextMenu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideContextMenu())); } m_contextMenu = menu; m_userContextMenuSet = true; if (m_contextMenu) { connect(m_contextMenu, SIGNAL(aboutToShow()), this, SLOT(aboutToShowContextMenu())); connect(m_contextMenu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideContextMenu())); } } QMenu *KTextEditor::ViewPrivate::contextMenu() const { if (m_userContextMenuSet) { return m_contextMenu; } else { KXMLGUIClient *client = const_cast(this); while (client->parentClient()) { client = client->parentClient(); } // qCDebug(LOG_KTE) << "looking up all menu containers"; if (client->factory()) { const QList menuContainers = client->factory()->containers(QStringLiteral("menu")); for (QWidget *w : menuContainers) { if (w->objectName() == QLatin1String("ktexteditor_popup")) { // perhaps optimize this block QMenu *menu = (QMenu *)w; // menu is a reusable instance shared among all views. Therefore, // disconnect the current receiver(s) from the menu show/hide signals // before connecting `this` view. This ensures that only the current // view gets a signal when the menu is about to be shown or hidden, // and not also the view(s) that previously had the menu open. disconnect(menu, SIGNAL(aboutToShow()), nullptr, nullptr); disconnect(menu, SIGNAL(aboutToHide()), nullptr, nullptr); connect(menu, SIGNAL(aboutToShow()), this, SLOT(aboutToShowContextMenu())); connect(menu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideContextMenu())); return menu; } } } } return nullptr; } QMenu *KTextEditor::ViewPrivate::defaultContextMenu(QMenu *menu) const { if (!menu) { menu = new QMenu(const_cast(this)); } menu->addAction(m_editUndo); menu->addAction(m_editRedo); menu->addSeparator(); menu->addAction(m_cut); menu->addAction(m_copy); menu->addAction(m_paste); menu->addSeparator(); menu->addAction(m_selectAll); menu->addAction(m_deSelect); if (QAction *spellingSuggestions = actionCollection()->action(QStringLiteral("spelling_suggestions"))) { menu->addSeparator(); menu->addAction(spellingSuggestions); } if (QAction *bookmark = actionCollection()->action(QStringLiteral("bookmarks"))) { menu->addSeparator(); menu->addAction(bookmark); } return menu; } void KTextEditor::ViewPrivate::aboutToShowContextMenu() { QMenu *menu = qobject_cast(sender()); if (menu) { emit contextMenuAboutToShow(this, menu); } } void KTextEditor::ViewPrivate::aboutToHideContextMenu() { m_spellingMenu->setUseMouseForMisspelledRange(false); } // BEGIN ConfigInterface stff QStringList KTextEditor::ViewPrivate::configKeys() const { static const QStringList keys = {QStringLiteral("icon-bar"), QStringLiteral("line-numbers"), QStringLiteral("dynamic-word-wrap"), QStringLiteral("background-color"), QStringLiteral("selection-color"), QStringLiteral("search-highlight-color"), QStringLiteral("replace-highlight-color"), QStringLiteral("default-mark-type"), QStringLiteral("allow-mark-menu"), QStringLiteral("folding-bar"), QStringLiteral("folding-preview"), QStringLiteral("icon-border-color"), QStringLiteral("folding-marker-color"), QStringLiteral("line-number-color"), QStringLiteral("current-line-number-color"), QStringLiteral("modification-markers"), QStringLiteral("keyword-completion"), QStringLiteral("word-count"), QStringLiteral("scrollbar-minimap"), QStringLiteral("scrollbar-preview"), QStringLiteral("font")}; return keys; } QVariant KTextEditor::ViewPrivate::configValue(const QString &key) { if (key == QLatin1String("icon-bar")) { return config()->iconBar(); } else if (key == QLatin1String("line-numbers")) { return config()->lineNumbers(); } else if (key == QLatin1String("dynamic-word-wrap")) { return config()->dynWordWrap(); } else if (key == QLatin1String("background-color")) { return renderer()->config()->backgroundColor(); } else if (key == QLatin1String("selection-color")) { return renderer()->config()->selectionColor(); } else if (key == QLatin1String("search-highlight-color")) { return renderer()->config()->searchHighlightColor(); } else if (key == QLatin1String("replace-highlight-color")) { return renderer()->config()->replaceHighlightColor(); } else if (key == QLatin1String("default-mark-type")) { return config()->defaultMarkType(); } else if (key == QLatin1String("allow-mark-menu")) { return config()->allowMarkMenu(); } else if (key == QLatin1String("folding-bar")) { return config()->foldingBar(); } else if (key == QLatin1String("folding-preview")) { return config()->foldingPreview(); } else if (key == QLatin1String("icon-border-color")) { return renderer()->config()->iconBarColor(); } else if (key == QLatin1String("folding-marker-color")) { return renderer()->config()->foldingColor(); } else if (key == QLatin1String("line-number-color")) { return renderer()->config()->lineNumberColor(); } else if (key == QLatin1String("current-line-number-color")) { return renderer()->config()->currentLineNumberColor(); } else if (key == QLatin1String("modification-markers")) { return config()->lineModification(); } else if (key == QLatin1String("keyword-completion")) { return config()->keywordCompletion(); } else if (key == QLatin1String("word-count")) { return config()->showWordCount(); } else if (key == QLatin1String("scrollbar-minimap")) { return config()->scrollBarMiniMap(); } else if (key == QLatin1String("scrollbar-preview")) { return config()->scrollBarPreview(); } else if (key == QLatin1String("font")) { return renderer()->config()->baseFont(); } // return invalid variant return QVariant(); } void KTextEditor::ViewPrivate::setConfigValue(const QString &key, const QVariant &value) { // First, try the new config interface if (config()->setValue(key, value)) { return; } else if (renderer()->config()->setValue(key, value)) { return; } // No success? Go the old way if (value.canConvert(QVariant::Color)) { if (key == QLatin1String("background-color")) { renderer()->config()->setBackgroundColor(value.value()); } else if (key == QLatin1String("selection-color")) { renderer()->config()->setSelectionColor(value.value()); } else if (key == QLatin1String("search-highlight-color")) { renderer()->config()->setSearchHighlightColor(value.value()); } else if (key == QLatin1String("replace-highlight-color")) { renderer()->config()->setReplaceHighlightColor(value.value()); } else if (key == QLatin1String("icon-border-color")) { renderer()->config()->setIconBarColor(value.value()); } else if (key == QLatin1String("folding-marker-color")) { renderer()->config()->setFoldingColor(value.value()); } else if (key == QLatin1String("line-number-color")) { renderer()->config()->setLineNumberColor(value.value()); } else if (key == QLatin1String("current-line-number-color")) { renderer()->config()->setCurrentLineNumberColor(value.value()); } } else if (value.type() == QVariant::Bool) { // Note explicit type check above. If we used canConvert, then // values of type UInt will be trapped here. if (key == QLatin1String("dynamic-word-wrap")) { config()->setDynWordWrap(value.toBool()); } else if (key == QLatin1String("word-count")) { config()->setShowWordCount(value.toBool()); } } else if (value.canConvert(QVariant::Font)) { if (key == QLatin1String("font")) { renderer()->config()->setFont(value.value()); } } } // END ConfigInterface void KTextEditor::ViewPrivate::userInvokedCompletion() { completionWidget()->userInvokedCompletion(); } KateViewBar *KTextEditor::ViewPrivate::bottomViewBar() const { return m_bottomViewBar; } KateGotoBar *KTextEditor::ViewPrivate::gotoBar() { if (!m_gotoBar) { m_gotoBar = new KateGotoBar(this); bottomViewBar()->addBarWidget(m_gotoBar); } return m_gotoBar; } KateDictionaryBar *KTextEditor::ViewPrivate::dictionaryBar() { if (!m_dictionaryBar) { m_dictionaryBar = new KateDictionaryBar(this); bottomViewBar()->addBarWidget(m_dictionaryBar); } return m_dictionaryBar; } void KTextEditor::ViewPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model) { KTextEditor::AnnotationModel *oldmodel = m_annotationModel; m_annotationModel = model; m_viewInternal->m_leftBorder->annotationModelChanged(oldmodel, m_annotationModel); } KTextEditor::AnnotationModel *KTextEditor::ViewPrivate::annotationModel() const { return m_annotationModel; } void KTextEditor::ViewPrivate::setAnnotationBorderVisible(bool visible) { m_viewInternal->m_leftBorder->setAnnotationBorderOn(visible); } bool KTextEditor::ViewPrivate::isAnnotationBorderVisible() const { return m_viewInternal->m_leftBorder->annotationBorderOn(); } KTextEditor::AbstractAnnotationItemDelegate *KTextEditor::ViewPrivate::annotationItemDelegate() const { return m_viewInternal->m_leftBorder->annotationItemDelegate(); } void KTextEditor::ViewPrivate::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate) { m_viewInternal->m_leftBorder->setAnnotationItemDelegate(delegate); } bool KTextEditor::ViewPrivate::uniformAnnotationItemSizes() const { return m_viewInternal->m_leftBorder->uniformAnnotationItemSizes(); } void KTextEditor::ViewPrivate::setAnnotationUniformItemSizes(bool enable) { m_viewInternal->m_leftBorder->setAnnotationUniformItemSizes(enable); } KTextEditor::Range KTextEditor::ViewPrivate::visibleRange() { // ensure that the view is up-to-date, otherwise 'endPos()' might fail! if (!m_viewInternal->endPos().isValid()) { m_viewInternal->updateView(); } return KTextEditor::Range(m_viewInternal->toRealCursor(m_viewInternal->startPos()), m_viewInternal->toRealCursor(m_viewInternal->endPos())); } bool KTextEditor::ViewPrivate::event(QEvent *e) { switch (e->type()) { - case QEvent::StyleChange: - setupLayout(); - return true; - default: - return KTextEditor::View::event(e); + case QEvent::StyleChange: + setupLayout(); + return true; + default: + return KTextEditor::View::event(e); } } void KTextEditor::ViewPrivate::paintEvent(QPaintEvent *e) { // base class KTextEditor::View::paintEvent(e); const QRect contentsRect = m_topSpacer->geometry() | m_bottomSpacer->geometry() | m_leftSpacer->geometry() | m_rightSpacer->geometry(); if (contentsRect.isValid()) { QStyleOptionFrame opt; opt.initFrom(this); opt.frameShape = QFrame::StyledPanel; opt.state |= QStyle::State_Sunken; // clear mouseOver and focus state // update from relevant widgets opt.state &= ~(QStyle::State_HasFocus | QStyle::State_MouseOver); const QList widgets = QList() << m_viewInternal << m_viewInternal->m_leftBorder << m_viewInternal->m_lineScroll << m_viewInternal->m_columnScroll; for (const QWidget *w : widgets) { if (w->hasFocus()) opt.state |= QStyle::State_HasFocus; if (w->underMouse()) opt.state |= QStyle::State_MouseOver; } // update rect opt.rect = contentsRect; // render QPainter paint(this); paint.setClipRegion(e->region()); paint.setRenderHints(QPainter::Antialiasing); style()->drawControl(QStyle::CE_ShapedFrame, &opt, &paint, this); } } void KTextEditor::ViewPrivate::toggleOnTheFlySpellCheck(bool b) { doc()->onTheFlySpellCheckingEnabled(b); } void KTextEditor::ViewPrivate::reflectOnTheFlySpellCheckStatus(bool enabled) { m_spellingMenu->setVisible(enabled); m_toggleOnTheFlySpellCheck->setChecked(enabled); } KateSpellingMenu *KTextEditor::ViewPrivate::spellingMenu() { return m_spellingMenu; } void KTextEditor::ViewPrivate::notifyAboutRangeChange(int startLine, int endLine, bool rangeWithAttribute) { #ifdef VIEW_RANGE_DEBUG // output args qCDebug(LOG_KTE) << "trigger attribute changed from" << startLine << "to" << endLine << "rangeWithAttribute" << rangeWithAttribute; #endif // first call: if (!m_delayedUpdateTriggered) { m_delayedUpdateTriggered = true; m_lineToUpdateMin = -1; m_lineToUpdateMax = -1; // only set initial line range, if range with attribute! if (rangeWithAttribute) { m_lineToUpdateMin = startLine; m_lineToUpdateMax = endLine; } // emit queued signal and be done emit delayedUpdateOfView(); return; } // ignore lines if no attribute if (!rangeWithAttribute) { return; } // update line range if (startLine != -1 && (m_lineToUpdateMin == -1 || startLine < m_lineToUpdateMin)) { m_lineToUpdateMin = startLine; } if (endLine != -1 && endLine > m_lineToUpdateMax) { m_lineToUpdateMax = endLine; } } void KTextEditor::ViewPrivate::slotDelayedUpdateOfView() { if (!m_delayedUpdateTriggered) { return; } #ifdef VIEW_RANGE_DEBUG // output args qCDebug(LOG_KTE) << "delayed attribute changed from" << m_lineToUpdateMin << "to" << m_lineToUpdateMax; #endif // update ranges in updateRangesIn(KTextEditor::Attribute::ActivateMouseIn); updateRangesIn(KTextEditor::Attribute::ActivateCaretIn); // update view, if valid line range, else only feedback update wanted anyway if (m_lineToUpdateMin != -1 && m_lineToUpdateMax != -1) { tagLines(m_lineToUpdateMin, m_lineToUpdateMax, true); updateView(true); } // reset flags m_delayedUpdateTriggered = false; m_lineToUpdateMin = -1; m_lineToUpdateMax = -1; } void KTextEditor::ViewPrivate::updateRangesIn(KTextEditor::Attribute::ActivationType activationType) { // new ranges with cursor in, default none QSet newRangesIn; // on which range set we work? QSet &oldSet = (activationType == KTextEditor::Attribute::ActivateMouseIn) ? m_rangesMouseIn : m_rangesCaretIn; // which cursor position to honor? KTextEditor::Cursor currentCursor = (activationType == KTextEditor::Attribute::ActivateMouseIn) ? m_viewInternal->mousePosition() : m_viewInternal->cursorPosition(); // first: validate the remembered ranges QSet validRanges; for (Kate::TextRange *range : qAsConst(oldSet)) { if (doc()->buffer().rangePointerValid(range)) { validRanges.insert(range); } } // cursor valid? else no new ranges can be found if (currentCursor.isValid() && currentCursor.line() < doc()->buffer().lines()) { // now: get current ranges for the line of cursor with an attribute const QList rangesForCurrentCursor = doc()->buffer().rangesForLine(currentCursor.line(), this, false); // match which ranges really fit the given cursor for (Kate::TextRange *range : rangesForCurrentCursor) { // range has no dynamic attribute of right type and no feedback object if ((!range->attribute() || !range->attribute()->dynamicAttribute(activationType)) && !range->feedback()) { continue; } // range doesn't contain cursor, not interesting if ((range->start().insertBehavior() == KTextEditor::MovingCursor::StayOnInsert) ? (currentCursor < range->start().toCursor()) : (currentCursor <= range->start().toCursor())) { continue; } if ((range->end().insertBehavior() == KTextEditor::MovingCursor::StayOnInsert) ? (range->end().toCursor() <= currentCursor) : (range->end().toCursor() < currentCursor)) { continue; } // range contains cursor, was it already in old set? if (validRanges.contains(range)) { // insert in new, remove from old, be done with it newRangesIn.insert(range); validRanges.remove(range); continue; } // oh, new range, trigger update and insert into new set newRangesIn.insert(range); if (range->attribute() && range->attribute()->dynamicAttribute(activationType)) { notifyAboutRangeChange(range->start().line(), range->end().line(), true); } // feedback if (range->feedback()) { if (activationType == KTextEditor::Attribute::ActivateMouseIn) { range->feedback()->mouseEnteredRange(range, this); } else { range->feedback()->caretEnteredRange(range, this); emit caretChangedRange(this); } } #ifdef VIEW_RANGE_DEBUG // found new range for activation qCDebug(LOG_KTE) << "activated new range" << range << "by" << activationType; #endif } } // now: notify for left ranges! for (Kate::TextRange *range : qAsConst(validRanges)) { // range valid + right dynamic attribute, trigger update if (range->toRange().isValid() && range->attribute() && range->attribute()->dynamicAttribute(activationType)) { notifyAboutRangeChange(range->start().line(), range->end().line(), true); } // feedback if (range->feedback()) { if (activationType == KTextEditor::Attribute::ActivateMouseIn) { range->feedback()->mouseExitedRange(range, this); } else { range->feedback()->caretExitedRange(range, this); emit caretChangedRange(this); } } } // set new ranges oldSet = newRangesIn; } void KTextEditor::ViewPrivate::postMessage(KTextEditor::Message *message, QList> actions) { // just forward to KateMessageWidget :-) auto messageWidget = m_messageWidgets[message->position()]; if (!messageWidget) { // this branch is used for: TopInView, CenterInView, and BottomInView messageWidget = new KateMessageWidget(m_viewInternal, true); m_messageWidgets[message->position()] = messageWidget; m_notificationLayout->addWidget(messageWidget, message->position()); connect(this, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate *)), messageWidget, SLOT(startAutoHideTimer())); connect(this, SIGNAL(cursorPositionChanged(KTextEditor::View *, KTextEditor::Cursor)), messageWidget, SLOT(startAutoHideTimer())); } messageWidget->postMessage(message, std::move(actions)); } KateMessageWidget *KTextEditor::ViewPrivate::messageWidget() { return m_messageWidgets[KTextEditor::Message::TopInView]; } void KTextEditor::ViewPrivate::saveFoldingState() { m_savedFoldingState = m_textFolding.exportFoldingRanges(); } void KTextEditor::ViewPrivate::applyFoldingState() { m_textFolding.importFoldingRanges(m_savedFoldingState); m_savedFoldingState = QJsonDocument(); } void KTextEditor::ViewPrivate::exportHtmlToFile(const QString &file) { KateExporter(this).exportToFile(file); } void KTextEditor::ViewPrivate::exportHtmlToClipboard() { KateExporter(this).exportToClipboard(); } void KTextEditor::ViewPrivate::exportHtmlToFile() { const QString file = QFileDialog::getSaveFileName(this, i18n("Export File as HTML"), doc()->documentName()); if (!file.isEmpty()) { KateExporter(this).exportToFile(file); } } void KTextEditor::ViewPrivate::clearHighlights() { qDeleteAll(m_rangesForHighlights); m_rangesForHighlights.clear(); m_currentTextForHighlights.clear(); } void KTextEditor::ViewPrivate::selectionChangedForHighlights() { QString text; // if text of selection is still the same, abort if (selection() && selectionRange().onSingleLine()) { text = selectionText(); if (text == m_currentTextForHighlights) return; } // text changed: remove all highlights + create new ones // (do not call clearHighlights(), since this also resets the m_currentTextForHighlights qDeleteAll(m_rangesForHighlights); m_rangesForHighlights.clear(); // do not highlight strings with leading and trailing spaces if (!text.isEmpty() && (text.at(0).isSpace() || text.at(text.length() - 1).isSpace())) return; // trigger creation of ranges for current view range m_currentTextForHighlights = text; createHighlights(); } void KTextEditor::ViewPrivate::createHighlights() { // do nothing if no text to highlight if (m_currentTextForHighlights.isEmpty()) { return; } KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); attr->setBackground(Qt::yellow); // set correct highlight color from Kate's color schema QColor fgColor = defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); QColor bgColor = renderer()->config()->searchHighlightColor(); attr->setForeground(fgColor); attr->setBackground(bgColor); KTextEditor::Cursor start(visibleRange().start()); KTextEditor::Range searchRange; /** * only add word boundary if we can find the text then * fixes $lala hl */ QString regex = QRegExp::escape(m_currentTextForHighlights); if (QRegExp(QStringLiteral("\\b%1").arg(regex)).indexIn(QStringLiteral(" %1 ").arg(m_currentTextForHighlights)) != -1) regex = QStringLiteral("\\b%1").arg(regex); if (QRegExp(QStringLiteral("%1\\b").arg(regex)).indexIn(QStringLiteral(" %1 ").arg(m_currentTextForHighlights)) != -1) regex = QStringLiteral("%1\\b").arg(regex); QVector matches; do { searchRange.setRange(start, visibleRange().end()); matches = doc()->searchText(searchRange, regex, KTextEditor::Regex); if (matches.first().isValid()) { KTextEditor::MovingRange *mr = doc()->newMovingRange(matches.first()); mr->setAttribute(attr); mr->setView(this); mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection mr->setAttributeOnlyForViews(true); m_rangesForHighlights.append(mr); start = matches.first().end(); } } while (matches.first().isValid()); } KateAbstractInputMode *KTextEditor::ViewPrivate::currentInputMode() const { return m_viewInternal->m_currentInputMode; } void KTextEditor::ViewPrivate::toggleInputMode() { if (QAction *a = dynamic_cast(sender())) { setInputMode(static_cast(a->data().toInt())); } } void KTextEditor::ViewPrivate::cycleInputMode() { InputMode current = currentInputMode()->viewInputMode(); InputMode to = (current == KTextEditor::View::NormalInputMode) ? KTextEditor::View::ViInputMode : KTextEditor::View::NormalInputMode; setInputMode(to); } // BEGIN KTextEditor::PrintInterface stuff bool KTextEditor::ViewPrivate::print() { return KatePrinter::print(this); } void KTextEditor::ViewPrivate::printPreview() { KatePrinter::printPreview(this); } // END // BEGIN KTextEditor::InlineNoteInterface void KTextEditor::ViewPrivate::registerInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) { if (!m_inlineNoteProviders.contains(provider)) { m_inlineNoteProviders.append(provider); connect(provider, &KTextEditor::InlineNoteProvider::inlineNotesReset, this, &ViewPrivate::inlineNotesReset); connect(provider, &KTextEditor::InlineNoteProvider::inlineNotesChanged, this, &ViewPrivate::inlineNotesLineChanged); inlineNotesReset(); } } void KTextEditor::ViewPrivate::unregisterInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) { const int index = m_inlineNoteProviders.indexOf(provider); if (index >= 0) { m_inlineNoteProviders.removeAt(index); provider->disconnect(this); inlineNotesReset(); } } QVarLengthArray KTextEditor::ViewPrivate::inlineNotes(int line) const { QVarLengthArray allInlineNotes; for (KTextEditor::InlineNoteProvider *provider : m_inlineNoteProviders) { int index = 0; for (auto column : provider->inlineNotes(line)) { KateInlineNoteData note = {provider, this, {line, column}, index, m_viewInternal->m_activeInlineNote.m_underMouse, m_viewInternal->renderer()->currentFont(), m_viewInternal->renderer()->lineHeight()}; allInlineNotes.append(note); index++; } } return allInlineNotes; } QRect KTextEditor::ViewPrivate::inlineNoteRect(const KateInlineNoteData ¬e) const { return m_viewInternal->inlineNoteRect(note); } void KTextEditor::ViewPrivate::inlineNotesReset() { m_viewInternal->m_activeInlineNote = {}; tagLines(0, doc()->lastLine(), true); } void KTextEditor::ViewPrivate::inlineNotesLineChanged(int line) { if (line == m_viewInternal->m_activeInlineNote.m_position.line()) { m_viewInternal->m_activeInlineNote = {}; } tagLines(line, line, true); } // END KTextEditor::InlineNoteInterface KTextEditor::Attribute::Ptr KTextEditor::ViewPrivate::defaultStyleAttribute(KTextEditor::DefaultStyle defaultStyle) const { KateRendererConfig *renderConfig = const_cast(this)->renderer()->config(); KTextEditor::Attribute::Ptr style = doc()->highlight()->attributes(renderConfig->schema()).at(defaultStyle); if (!style->hasProperty(QTextFormat::BackgroundBrush)) { // make sure the returned style has the default background color set style = new KTextEditor::Attribute(*style); style->setBackground(QBrush(renderConfig->backgroundColor())); } return style; } QList KTextEditor::ViewPrivate::lineAttributes(int line) { QList attribs; if (line < 0 || line >= doc()->lines()) return attribs; Kate::TextLine kateLine = doc()->kateTextLine(line); if (!kateLine) { return attribs; } const QVector &intAttrs = kateLine->attributesList(); for (int i = 0; i < intAttrs.size(); ++i) { if (intAttrs[i].length > 0 && intAttrs[i].attributeValue > 0) { attribs << KTextEditor::AttributeBlock(intAttrs.at(i).offset, intAttrs.at(i).length, renderer()->attribute(intAttrs.at(i).attributeValue)); } } return attribs; } diff --git a/src/view/kateviewinternal.cpp b/src/view/kateviewinternal.cpp index 57935160..2939b830 100644 --- a/src/view/kateviewinternal.cpp +++ b/src/view/kateviewinternal.cpp @@ -1,3889 +1,3889 @@ /* This file is part of the KDE libraries Copyright (C) 2002 John Firebaugh Copyright (C) 2002 Joseph Wenninger Copyright (C) 2002,2003 Christoph Cullmann Copyright (C) 2002-2007 Hamish Rodda Copyright (C) 2003 Anakim Border Copyright (C) 2007 Mirko Stocker Copyright (C) 2007 Matthew Woehlke Copyright (C) 2008 Erlend Hamberg Based on: KWriteView : Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateviewinternal.h" #include "inlinenotedata.h" #include "kateabstractinputmode.h" #include "kateabstractinputmodefactory.h" #include "katebuffer.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "kateglobal.h" #include "katehighlight.h" #include "katelayoutcache.h" #include "katemessagewidget.h" #include "katepartdebug.h" #include "katerenderer.h" #include "katetextanimation.h" #include "kateview.h" #include "kateviewaccessible.h" #include "kateviewhelpers.h" #include "spellcheck/spellingmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const bool debugPainting = false; class ZoomEventFilter { public: ZoomEventFilter() = default; bool detectZoomingEvent(QWheelEvent *e, Qt::KeyboardModifiers modifier = Qt::ControlModifier) { Qt::KeyboardModifiers modState = e->modifiers(); if (modState == modifier) { if (m_lastWheelEvent.isValid()) { const qint64 deltaT = m_lastWheelEvent.elapsed(); // Pressing the specified modifier key within 200ms of the previous "unmodified" // wheelevent is not allowed to toggle on text zooming if (m_lastWheelEventUnmodified && deltaT < 200) { m_ignoreZoom = true; } else if (deltaT > 1000) { // the protection is kept active for 1s after the last wheel event // TODO: this value should be tuned, preferably by someone using // Ctrl+Wheel zooming frequently. m_ignoreZoom = false; } } else { // we can't say anything and have to assume there's nothing // accidental to the modifier being pressed. m_ignoreZoom = false; } m_lastWheelEventUnmodified = false; if (m_ignoreZoom) { // unset the modifier so the view scrollbars can handle the scroll // event and produce normal, not accelerated scrolling modState &= ~modifier; e->setModifiers(modState); } } else { // state is reset after any wheel event without the zoom modifier m_lastWheelEventUnmodified = true; m_ignoreZoom = false; } m_lastWheelEvent.start(); // inform the caller whether this event is allowed to trigger text zooming. return !m_ignoreZoom && modState == modifier; } protected: QElapsedTimer m_lastWheelEvent; bool m_ignoreZoom = false; bool m_lastWheelEventUnmodified = false; }; KateViewInternal::KateViewInternal(KTextEditor::ViewPrivate *view) : QWidget(view) , editSessionNumber(0) , editIsRunning(false) , m_view(view) , m_cursor(doc()->buffer(), KTextEditor::Cursor(0, 0), Kate::TextCursor::MoveOnInsert) , m_mouse() , m_possibleTripleClick(false) , m_completionItemExpanded(false) , m_bm(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand)) , m_bmStart(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand)) , m_bmEnd(doc()->newMovingRange(KTextEditor::Range::invalid(), KTextEditor::MovingRange::DoNotExpand)) , m_bmLastFlashPos(doc()->newMovingCursor(KTextEditor::Cursor::invalid())) , m_dummy(nullptr) // stay on cursor will avoid that the view scroll around on press return at beginning , m_startPos(doc()->buffer(), KTextEditor::Cursor(0, 0), Kate::TextCursor::StayOnInsert) , m_visibleLineCount(0) , m_madeVisible(false) , m_shiftKeyPressed(false) , m_autoCenterLines(0) , m_minLinesVisible(0) , m_selChangedByUser(false) , m_selectAnchor(-1, -1) , m_selectionMode(Default) , m_layoutCache(new KateLayoutCache(renderer(), this)) , m_preserveX(false) , m_preservedX(0) , m_cachedMaxStartPos(-1, -1) , m_dragScrollTimer(this) , m_scrollTimer(this) , m_cursorTimer(this) , m_textHintTimer(this) , m_textHintDelay(500) , m_textHintPos(-1, -1) , m_imPreeditRange(nullptr) { const QList factories = KTextEditor::EditorPrivate::self()->inputModeFactories(); for (KateAbstractInputModeFactory *factory : factories) { KateAbstractInputMode *m = factory->createInputMode(this); m_inputModes.insert(m->viewInputMode(), m); } m_currentInputMode = m_inputModes[KTextEditor::View::NormalInputMode]; // TODO: twisted, but needed setMinimumSize(0, 0); setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_InputMethodEnabled); // invalidate m_selectionCached.start(), or keyb selection is screwed initially m_selectionCached = KTextEditor::Range::invalid(); // bracket markers are only for this view and should not be printed m_bm->setView(m_view); m_bmStart->setView(m_view); m_bmEnd->setView(m_view); m_bm->setAttributeOnlyForViews(true); m_bmStart->setAttributeOnlyForViews(true); m_bmEnd->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface m_bm->setZDepth(-1000.0); m_bmStart->setZDepth(-1000.0); m_bmEnd->setZDepth(-1000.0); // update mark attributes updateBracketMarkAttributes(); // // scrollbar for lines // m_lineScroll = new KateScrollBar(Qt::Vertical, this); m_lineScroll->show(); m_lineScroll->setTracking(true); m_lineScroll->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); // Hijack the line scroller's controls, so we can scroll nicely for word-wrap connect(m_lineScroll, SIGNAL(actionTriggered(int)), SLOT(scrollAction(int))); connect(m_lineScroll, SIGNAL(sliderMoved(int)), SLOT(scrollLines(int))); connect(m_lineScroll, SIGNAL(sliderMMBMoved(int)), SLOT(scrollLines(int))); connect(m_lineScroll, SIGNAL(valueChanged(int)), SLOT(scrollLines(int))); // // scrollbar for columns // m_columnScroll = new QScrollBar(Qt::Horizontal, m_view); if (m_view->dynWordWrap()) { m_columnScroll->hide(); } else { m_columnScroll->show(); } m_columnScroll->setTracking(true); m_startX = 0; connect(m_columnScroll, SIGNAL(valueChanged(int)), SLOT(scrollColumns(int))); // bottom corner box m_dummy = new QWidget(m_view); m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height()); m_dummy->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); if (m_view->dynWordWrap()) { m_dummy->hide(); } else { m_dummy->show(); } cache()->setWrap(m_view->dynWordWrap()); // // iconborder ;) // m_leftBorder = new KateIconBorder(this, m_view); m_leftBorder->show(); // update view if folding ranges change connect(&m_view->textFolding(), SIGNAL(foldingRangesChanged()), SLOT(slotRegionVisibilityChanged())); m_displayCursor.setPosition(0, 0); setAcceptDrops(true); m_zoomEventFilter = new ZoomEventFilter(); // event filter installEventFilter(this); // set initial cursor m_mouseCursor = Qt::IBeamCursor; setCursor(m_mouseCursor); // call mouseMoveEvent also if no mouse button is pressed setMouseTracking(true); m_dragInfo.state = diNone; // timers connect(&m_dragScrollTimer, SIGNAL(timeout()), this, SLOT(doDragScroll())); connect(&m_scrollTimer, SIGNAL(timeout()), this, SLOT(scrollTimeout())); connect(&m_cursorTimer, SIGNAL(timeout()), this, SLOT(cursorTimeout())); connect(&m_textHintTimer, SIGNAL(timeout()), this, SLOT(textHintTimeout())); // selection changed to set anchor connect(m_view, SIGNAL(selectionChanged(KTextEditor::View *)), this, SLOT(viewSelectionChanged())); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(accessibleInterfaceFactory); #endif connect(doc(), &KTextEditor::DocumentPrivate::textInserted, this, &KateViewInternal::documentTextInserted); connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &KateViewInternal::documentTextRemoved); // update is called in KTextEditor::ViewPrivate, after construction and layout is over // but before any other kateviewinternal call } KateViewInternal::~KateViewInternal() { // delete text animation object here, otherwise it updates the view in its destructor delete m_textAnimation; #ifndef QT_NO_ACCESSIBILITY QAccessible::removeFactory(accessibleInterfaceFactory); #endif // kill preedit ranges delete m_imPreeditRange; qDeleteAll(m_imPreeditRangeChildren); qDeleteAll(m_inputModes); // delete bracket markers delete m_bm; delete m_bmStart; delete m_bmEnd; delete m_zoomEventFilter; } void KateViewInternal::prepareForDynWrapChange() { // Which is the current view line? m_wrapChangeViewLine = cache()->displayViewLine(m_displayCursor, true); } void KateViewInternal::dynWrapChanged() { m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height()); if (view()->dynWordWrap()) { m_columnScroll->hide(); m_dummy->hide(); } else { // column scrollbar + bottom corner box m_columnScroll->show(); m_dummy->show(); } cache()->setWrap(view()->dynWordWrap()); updateView(); if (view()->dynWordWrap()) { scrollColumns(0); } // Determine where the cursor should be to get the cursor on the same view line if (m_wrapChangeViewLine != -1) { KTextEditor::Cursor newStart = viewLineOffset(m_displayCursor, -m_wrapChangeViewLine); makeVisible(newStart, newStart.column(), true); } else { update(); } } KTextEditor::Cursor KateViewInternal::endPos() const { // Hrm, no lines laid out at all?? if (!cache()->viewCacheLineCount()) { return KTextEditor::Cursor(); } for (int i = qMin(linesDisplayed() - 1, cache()->viewCacheLineCount() - 1); i >= 0; i--) { const KateTextLayout &thisLine = cache()->viewLine(i); if (thisLine.line() == -1) { continue; } if (thisLine.virtualLine() >= view()->textFolding().visibleLines()) { // Cache is too out of date return KTextEditor::Cursor(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); } return KTextEditor::Cursor(thisLine.virtualLine(), thisLine.wrap() ? thisLine.endCol() - 1 : thisLine.endCol()); } // can happen, if view is still invisible return KTextEditor::Cursor(); } int KateViewInternal::endLine() const { return endPos().line(); } KateTextLayout KateViewInternal::yToKateTextLayout(int y) const { if (y < 0 || y > size().height()) { return KateTextLayout::invalid(); } int range = y / renderer()->lineHeight(); // lineRanges is always bigger than 0, after the initial updateView call if (range >= 0 && range < cache()->viewCacheLineCount()) { return cache()->viewLine(range); } return KateTextLayout::invalid(); } int KateViewInternal::lineToY(int viewLine) const { return (viewLine - startLine()) * renderer()->lineHeight(); } void KateViewInternal::slotIncFontSizes(qreal step) { renderer()->increaseFontSizes(step); } void KateViewInternal::slotDecFontSizes(qreal step) { renderer()->decreaseFontSizes(step); } void KateViewInternal::slotResetFontSizes() { renderer()->resetFontSizes(); } /** * Line is the real line number to scroll to. */ void KateViewInternal::scrollLines(int line) { KTextEditor::Cursor newPos(line, 0); scrollPos(newPos); } // This can scroll less than one true line void KateViewInternal::scrollViewLines(int offset) { KTextEditor::Cursor c = viewLineOffset(startPos(), offset); scrollPos(c); bool blocked = m_lineScroll->blockSignals(true); m_lineScroll->setValue(startLine()); m_lineScroll->blockSignals(blocked); } void KateViewInternal::scrollAction(int action) { switch (action) { - case QAbstractSlider::SliderSingleStepAdd: - scrollNextLine(); - break; + case QAbstractSlider::SliderSingleStepAdd: + scrollNextLine(); + break; - case QAbstractSlider::SliderSingleStepSub: - scrollPrevLine(); - break; + case QAbstractSlider::SliderSingleStepSub: + scrollPrevLine(); + break; - case QAbstractSlider::SliderPageStepAdd: - scrollNextPage(); - break; + case QAbstractSlider::SliderPageStepAdd: + scrollNextPage(); + break; - case QAbstractSlider::SliderPageStepSub: - scrollPrevPage(); - break; + case QAbstractSlider::SliderPageStepSub: + scrollPrevPage(); + break; - case QAbstractSlider::SliderToMinimum: - top_home(); - break; + case QAbstractSlider::SliderToMinimum: + top_home(); + break; - case QAbstractSlider::SliderToMaximum: - bottom_end(); - break; + case QAbstractSlider::SliderToMaximum: + bottom_end(); + break; } } void KateViewInternal::scrollNextPage() { scrollViewLines(qMax(linesDisplayed() - 1, 0)); } void KateViewInternal::scrollPrevPage() { scrollViewLines(-qMax(linesDisplayed() - 1, 0)); } void KateViewInternal::scrollPrevLine() { scrollViewLines(-1); } void KateViewInternal::scrollNextLine() { scrollViewLines(1); } KTextEditor::Cursor KateViewInternal::maxStartPos(bool changed) { cache()->setAcceptDirtyLayouts(true); if (m_cachedMaxStartPos.line() == -1 || changed) { KTextEditor::Cursor end(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); if (view()->config()->scrollPastEnd()) { m_cachedMaxStartPos = viewLineOffset(end, -m_minLinesVisible); } else { m_cachedMaxStartPos = viewLineOffset(end, -(linesDisplayed() - 1)); } } cache()->setAcceptDirtyLayouts(false); return m_cachedMaxStartPos; } // c is a virtual cursor void KateViewInternal::scrollPos(KTextEditor::Cursor &c, bool force, bool calledExternally, bool emitSignals) { if (!force && ((!view()->dynWordWrap() && c.line() == startLine()) || c == startPos())) { return; } if (c.line() < 0) { c.setLine(0); } KTextEditor::Cursor limit = maxStartPos(); if (c > limit) { c = limit; // Re-check we're not just scrolling to the same place if (!force && ((!view()->dynWordWrap() && c.line() == startLine()) || c == startPos())) { return; } } int viewLinesScrolled = 0; // only calculate if this is really used and useful, could be wrong here, please recheck // for larger scrolls this makes 2-4 seconds difference on my xeon with dyn. word wrap on // try to get it really working ;) bool viewLinesScrolledUsable = !force && (c.line() >= startLine() - linesDisplayed() - 1) && (c.line() <= endLine() + linesDisplayed() + 1); if (viewLinesScrolledUsable) { viewLinesScrolled = cache()->displayViewLine(c); } m_startPos.setPosition(c); // set false here but reversed if we return to makeVisible m_madeVisible = false; if (viewLinesScrolledUsable) { int lines = linesDisplayed(); if (view()->textFolding().visibleLines() < lines) { KTextEditor::Cursor end(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); lines = qMin(linesDisplayed(), cache()->displayViewLine(end) + 1); } Q_ASSERT(lines >= 0); if (!calledExternally && qAbs(viewLinesScrolled) < lines && // NOTE: on some machines we must update if the floating widget is visible // otherwise strange painting bugs may occur during scrolling... !((view()->m_messageWidgets[KTextEditor::Message::TopInView] && view()->m_messageWidgets[KTextEditor::Message::TopInView]->isVisible()) || (view()->m_messageWidgets[KTextEditor::Message::CenterInView] && view()->m_messageWidgets[KTextEditor::Message::CenterInView]->isVisible()) || (view()->m_messageWidgets[KTextEditor::Message::BottomInView] && view()->m_messageWidgets[KTextEditor::Message::BottomInView]->isVisible()))) { updateView(false, viewLinesScrolled); int scrollHeight = -(viewLinesScrolled * (int)renderer()->lineHeight()); // scroll excluding child widgets (floating notifications) scroll(0, scrollHeight, rect()); m_leftBorder->scroll(0, scrollHeight); if (emitSignals) { emit view()->verticalScrollPositionChanged(m_view, c); emit view()->displayRangeChanged(m_view); } return; } } updateView(); update(); m_leftBorder->update(); if (emitSignals) { emit view()->verticalScrollPositionChanged(m_view, c); emit view()->displayRangeChanged(m_view); } } void KateViewInternal::scrollColumns(int x) { if (x < 0) { x = 0; } if (x > m_columnScroll->maximum()) { x = m_columnScroll->maximum(); } if (x == startX()) { return; } int dx = startX() - x; m_startX = x; if (qAbs(dx) < width()) { // scroll excluding child widgets (floating notifications) scroll(dx, 0, rect()); } else { update(); } emit view()->horizontalScrollPositionChanged(m_view); emit view()->displayRangeChanged(m_view); bool blocked = m_columnScroll->blockSignals(true); m_columnScroll->setValue(startX()); m_columnScroll->blockSignals(blocked); } // If changed is true, the lines that have been set dirty have been updated. void KateViewInternal::updateView(bool changed, int viewLinesScrolled) { if (!isVisible() && !viewLinesScrolled && !changed) { return; // When this view is not visible, don't do anything } view()->doc()->delayAutoReload(); // Don't reload while user scrolls around bool blocked = m_lineScroll->blockSignals(true); int wrapWidth = width(); if (view()->config()->dynWrapAtStaticMarker() && view()->config()->dynWordWrap()) { // We need to transform char count to a pixel width, stolen from PrintPainter::updateCache() QString s; s.fill(QLatin1Char('5'), view()->doc()->config()->wordWrapAt()); wrapWidth = qMin(width(), static_cast(renderer()->currentFontMetrics().boundingRect(s).width())); } if (wrapWidth != cache()->viewWidth()) { cache()->setViewWidth(wrapWidth); changed = true; } /* It was observed that height() could be negative here -- when the main Kate view has 0 as size (during creation), and there frame around KateViewInternal. In which case we'd set the view cache to 0 (or less!) lines, and start allocating huge chunks of data, later. */ int newSize = (qMax(0, height()) / renderer()->lineHeight()) + 1; cache()->updateViewCache(startPos(), newSize, viewLinesScrolled); m_visibleLineCount = newSize; KTextEditor::Cursor maxStart = maxStartPos(changed); int maxLineScrollRange = maxStart.line(); if (view()->dynWordWrap() && maxStart.column() != 0) { maxLineScrollRange++; } m_lineScroll->setRange(0, maxLineScrollRange); m_lineScroll->setValue(startLine()); m_lineScroll->setSingleStep(1); m_lineScroll->setPageStep(qMax(0, height()) / renderer()->lineHeight()); m_lineScroll->blockSignals(blocked); KateViewConfig::ScrollbarMode show_scrollbars = static_cast(view()->config()->showScrollbars()); bool visible = ((show_scrollbars == KateViewConfig::AlwaysOn) || ((show_scrollbars == KateViewConfig::ShowWhenNeeded) && (maxLineScrollRange != 0))); bool visible_dummy = visible; m_lineScroll->setVisible(visible); if (!view()->dynWordWrap()) { int max = maxLen(startLine()) - width(); if (max < 0) { max = 0; } // if we lose the ability to scroll horizontally, move view to the far-left if (max == 0) { scrollColumns(0); } blocked = m_columnScroll->blockSignals(true); // disable scrollbar m_columnScroll->setDisabled(max == 0); visible = ((show_scrollbars == KateViewConfig::AlwaysOn) || ((show_scrollbars == KateViewConfig::ShowWhenNeeded) && (max != 0))); visible_dummy &= visible; m_columnScroll->setVisible(visible); m_columnScroll->setRange(0, max + (renderer()->spaceWidth() / 2)); // Add some space for the caret at EOL m_columnScroll->setValue(startX()); // Approximate linescroll m_columnScroll->setSingleStep(renderer()->currentFontMetrics().horizontalAdvance(QLatin1Char('a'))); m_columnScroll->setPageStep(width()); m_columnScroll->blockSignals(blocked); } else { visible_dummy = false; } m_dummy->setVisible(visible_dummy); if (changed) { updateDirty(); } } /** * this function ensures a certain location is visible on the screen. * if endCol is -1, ignore making the columns visible. */ void KateViewInternal::makeVisible(const KTextEditor::Cursor &c, int endCol, bool force, bool center, bool calledExternally) { // qCDebug(LOG_KTE) << "MakeVisible start " << startPos() << " end " << endPos() << " -> request: " << c;// , new start [" << scroll.line << "," << scroll.col << "] lines " << (linesDisplayed() - 1) << " height " << height(); // if the line is in a folded region, unfold all the way up // if ( doc()->foldingTree()->findNodeForLine( c.line )->visible ) // qCDebug(LOG_KTE)<<"line ("<displayViewLine(c, true) < 0 && cache()->displayViewLine(c, false) > 0); if (force) { KTextEditor::Cursor scroll = c; scrollPos(scroll, force, calledExternally); } else if (center && (c < startPos() || c > endPos())) { KTextEditor::Cursor scroll = viewLineOffset(c, -int(lnDisp) / 2); scrollPos(scroll, false, calledExternally); } else if ((cache()->displayViewLine(c, true) >= (lnDisp - m_minLinesVisible)) || (curBelowScreen)) { KTextEditor::Cursor scroll = viewLineOffset(c, -(lnDisp - m_minLinesVisible - 1)); scrollPos(scroll, false, calledExternally); } else if (c < viewLineOffset(startPos(), m_minLinesVisible)) { KTextEditor::Cursor scroll = viewLineOffset(c, -m_minLinesVisible); scrollPos(scroll, false, calledExternally); } else { // Check to see that we're not showing blank lines KTextEditor::Cursor max = maxStartPos(); if (startPos() > max) { scrollPos(max, max.column(), calledExternally); } } if (!view()->dynWordWrap() && (endCol != -1 || view()->wrapCursor())) { KTextEditor::Cursor rc = toRealCursor(c); int sX = renderer()->cursorToX(cache()->textLayout(rc), rc, !view()->wrapCursor()); int sXborder = sX - 8; if (sXborder < 0) { sXborder = 0; } if (sX < startX()) { scrollColumns(sXborder); } else if (sX > startX() + width()) { scrollColumns(sX - width() + 8); } } m_madeVisible = !force; #ifndef QT_NO_ACCESSIBILITY // FIXME -- is this needed? // QAccessible::updateAccessibility(this, KateCursorAccessible::ChildId, QAccessible::Focus); #endif } void KateViewInternal::slotRegionVisibilityChanged() { qCDebug(LOG_KTE); /** * ensure the layout cache is ok for the updateCursor calls below * without the updateView() the view will jump to the bottom on hiding blocks after * change cfb0af25bdfac0d8f86b42db0b34a6bc9f9a361e */ cache()->clear(); updateView(); m_cachedMaxStartPos.setLine(-1); KTextEditor::Cursor max = maxStartPos(); if (startPos() > max) { scrollPos(max, false, false, false /* don't emit signals! */); } // if text was folded: make sure the cursor is on a visible line qint64 foldedRangeId = -1; if (!view()->textFolding().isLineVisible(m_cursor.line(), &foldedRangeId)) { KTextEditor::Range foldingRange = view()->textFolding().foldingRange(foldedRangeId); Q_ASSERT(foldingRange.start().isValid()); // set cursor to start of folding region updateCursor(foldingRange.start(), true); } else { // force an update of the cursor, since otherwise the m_displayCursor // line may be below the total amount of visible lines. updateCursor(m_cursor, true); } updateView(); update(); m_leftBorder->update(); // emit signals here, scrollPos has this disabled, to ensure we do this after all stuff is updated! emit view()->verticalScrollPositionChanged(m_view, max); emit view()->displayRangeChanged(m_view); } void KateViewInternal::slotRegionBeginEndAddedRemoved(unsigned int) { qCDebug(LOG_KTE); // FIXME: performance problem m_leftBorder->update(); } void KateViewInternal::showEvent(QShowEvent *e) { updateView(); QWidget::showEvent(e); } int KateViewInternal::linesDisplayed() const { int h = height(); // catch zero heights, even if should not happen int fh = qMax(1, renderer()->lineHeight()); // default to 1, there is always one line around.... // too many places calc with linesDisplayed() - 1 return qMax(1, (h - (h % fh)) / fh); } QPoint KateViewInternal::cursorToCoordinate(const KTextEditor::Cursor &cursor, bool realCursor, bool includeBorder) const { if (cursor.line() >= doc()->lines()) { return QPoint(-1, -1); } int viewLine = cache()->displayViewLine(realCursor ? toVirtualCursor(cursor) : cursor, true); if (viewLine < 0 || viewLine >= cache()->viewCacheLineCount()) { return QPoint(-1, -1); } const int y = (int)viewLine * renderer()->lineHeight(); KateTextLayout layout = cache()->viewLine(viewLine); if (cursor.column() > doc()->lineLength(cursor.line())) { return QPoint(-1, -1); } int x = 0; // only set x value if we have a valid layout (bug #171027) if (layout.isValid()) { x = (int)layout.lineLayout().cursorToX(cursor.column()); } // else // qCDebug(LOG_KTE) << "Invalid Layout"; if (includeBorder) { x += m_leftBorder->width(); } x -= startX(); return QPoint(x, y); } QPoint KateViewInternal::cursorCoordinates(bool includeBorder) const { return cursorToCoordinate(m_displayCursor, false, includeBorder); } KTextEditor::Cursor KateViewInternal::findMatchingBracket() { KTextEditor::Cursor c; if (!m_bm->toRange().isValid()) { return KTextEditor::Cursor::invalid(); } Q_ASSERT(m_bmEnd->toRange().isValid()); Q_ASSERT(m_bmStart->toRange().isValid()); if (m_bmStart->toRange().contains(m_cursor) || m_bmStart->end() == m_cursor.toCursor()) { c = m_bmEnd->end(); // We need to adjust the cursor position in case of override mode, BUG-402594 if (doc()->config()->ovr()) { c.setColumn(c.column() - 1); } } else if (m_bmEnd->toRange().contains(m_cursor) || m_bmEnd->end() == m_cursor.toCursor()) { c = m_bmStart->start(); } else { // should never happen: a range exists, but the cursor position is // neither at the start nor at the end... return KTextEditor::Cursor::invalid(); } return c; } class CalculatingCursor { public: // These constructors constrain their arguments to valid positions // before only the third one did, but that leads to crashes // see bug 227449 CalculatingCursor(KateViewInternal *vi) : m_vi(vi) { makeValid(); } CalculatingCursor(KateViewInternal *vi, const KTextEditor::Cursor &c) : m_cursor(c) , m_vi(vi) { makeValid(); } CalculatingCursor(KateViewInternal *vi, int line, int col) : m_cursor(line, col) , m_vi(vi) { makeValid(); } virtual ~CalculatingCursor() { } int line() const { return m_cursor.line(); } int column() const { return m_cursor.column(); } operator KTextEditor::Cursor() const { return m_cursor; } virtual CalculatingCursor &operator+=(int n) = 0; virtual CalculatingCursor &operator-=(int n) = 0; CalculatingCursor &operator++() { return operator+=(1); } CalculatingCursor &operator--() { return operator-=(1); } void makeValid() { m_cursor.setLine(qBound(0, line(), int(doc()->lines() - 1))); if (view()->wrapCursor()) { m_cursor.setColumn(qBound(0, column(), doc()->lineLength(line()))); } else { m_cursor.setColumn(qMax(0, column())); } Q_ASSERT(valid()); } void toEdge(KateViewInternal::Bias bias) { if (bias == KateViewInternal::left) { m_cursor.setColumn(0); } else if (bias == KateViewInternal::right) { m_cursor.setColumn(doc()->lineLength(line())); } } bool atEdge() const { return atEdge(KateViewInternal::left) || atEdge(KateViewInternal::right); } bool atEdge(KateViewInternal::Bias bias) const { switch (bias) { - case KateViewInternal::left: - return column() == 0; - case KateViewInternal::none: - return atEdge(); - case KateViewInternal::right: - return column() >= doc()->lineLength(line()); - default: - Q_ASSERT(false); - return false; + case KateViewInternal::left: + return column() == 0; + case KateViewInternal::none: + return atEdge(); + case KateViewInternal::right: + return column() >= doc()->lineLength(line()); + default: + Q_ASSERT(false); + return false; } } protected: bool valid() const { return line() >= 0 && line() < doc()->lines() && column() >= 0 && (!view()->wrapCursor() || column() <= doc()->lineLength(line())); } KTextEditor::ViewPrivate *view() { return m_vi->m_view; } const KTextEditor::ViewPrivate *view() const { return m_vi->m_view; } KTextEditor::DocumentPrivate *doc() { return view()->doc(); } const KTextEditor::DocumentPrivate *doc() const { return view()->doc(); } KTextEditor::Cursor m_cursor; KateViewInternal *m_vi; }; class BoundedCursor : public CalculatingCursor { public: BoundedCursor(KateViewInternal *vi) : CalculatingCursor(vi) { } BoundedCursor(KateViewInternal *vi, const KTextEditor::Cursor &c) : CalculatingCursor(vi, c) { } BoundedCursor(KateViewInternal *vi, int line, int col) : CalculatingCursor(vi, line, col) { } CalculatingCursor &operator+=(int n) override { KateLineLayoutPtr thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve valid layout for line " << line(); return *this; } const bool wrapCursor = view()->wrapCursor(); int maxColumn = -1; if (n >= 0) { for (int i = 0; i < n; i++) { if (column() >= thisLine->length()) { if (wrapCursor) { break; } else if (view()->dynWordWrap()) { // Don't go past the edge of the screen in dynamic wrapping mode if (maxColumn == -1) { maxColumn = thisLine->length() + ((m_vi->width() - thisLine->widthOfLastLine()) / m_vi->renderer()->spaceWidth()) - 1; } if (column() >= maxColumn) { m_cursor.setColumn(maxColumn); break; } m_cursor.setColumn(column() + 1); } else { m_cursor.setColumn(column() + 1); } } else { m_cursor.setColumn(thisLine->layout()->nextCursorPosition(column())); } } } else { for (int i = 0; i > n; i--) { if (column() >= thisLine->length()) { m_cursor.setColumn(column() - 1); } else if (column() == 0) { break; } else { m_cursor.setColumn(thisLine->layout()->previousCursorPosition(column())); } } } Q_ASSERT(valid()); return *this; } CalculatingCursor &operator-=(int n) override { return operator+=(-n); } }; class WrappingCursor : public CalculatingCursor { public: WrappingCursor(KateViewInternal *vi) : CalculatingCursor(vi) { } WrappingCursor(KateViewInternal *vi, const KTextEditor::Cursor &c) : CalculatingCursor(vi, c) { } WrappingCursor(KateViewInternal *vi, int line, int col) : CalculatingCursor(vi, line, col) { } CalculatingCursor &operator+=(int n) override { KateLineLayoutPtr thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line(); return *this; } if (n >= 0) { for (int i = 0; i < n; i++) { if (column() >= thisLine->length()) { // Have come to the end of a line if (line() >= doc()->lines() - 1) // Have come to the end of the document { break; } // Advance to the beginning of the next line m_cursor.setColumn(0); m_cursor.setLine(line() + 1); // Retrieve the next text range thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line(); return *this; } continue; } m_cursor.setColumn(thisLine->layout()->nextCursorPosition(column())); } } else { for (int i = 0; i > n; i--) { if (column() == 0) { // Have come to the start of the document if (line() == 0) { break; } // Start going back to the end of the last line m_cursor.setLine(line() - 1); // Retrieve the next text range thisLine = m_vi->cache()->line(line()); if (!thisLine->isValid()) { qCWarning(LOG_KTE) << "Did not retrieve a valid layout for line " << line(); return *this; } // Finish going back to the end of the last line m_cursor.setColumn(thisLine->length()); continue; } if (column() > thisLine->length()) { m_cursor.setColumn(column() - 1); } else { m_cursor.setColumn(thisLine->layout()->previousCursorPosition(column())); } } } Q_ASSERT(valid()); return *this; } CalculatingCursor &operator-=(int n) override { return operator+=(-n); } }; void KateViewInternal::moveChar(KateViewInternal::Bias bias, bool sel) { KTextEditor::Cursor c; if (view()->wrapCursor()) { c = WrappingCursor(this, m_cursor) += bias; } else { c = BoundedCursor(this, m_cursor) += bias; } updateSelection(c, sel); updateCursor(c); } void KateViewInternal::cursorPrevChar(bool sel) { if (!view()->wrapCursor() && m_cursor.column() == 0) { return; } moveChar(KateViewInternal::left, sel); } void KateViewInternal::cursorNextChar(bool sel) { moveChar(KateViewInternal::right, sel); } void KateViewInternal::wordPrev(bool sel) { WrappingCursor c(this, m_cursor); // First we skip backwards all space. // Then we look up into which category the current position falls: // 1. a "word" character // 2. a "non-word" character (except space) // 3. the beginning of the line // and skip all preceding characters that fall into this class. // The code assumes that space is never part of the word character class. KateHighlighting *h = doc()->highlight(); if (!c.atEdge(left)) { while (!c.atEdge(left) && doc()->line(c.line())[c.column() - 1].isSpace()) { --c; } } if (c.atEdge(left)) { --c; } else if (h->isInWord(doc()->line(c.line())[c.column() - 1])) { while (!c.atEdge(left) && h->isInWord(doc()->line(c.line())[c.column() - 1])) { --c; } } else { while (!c.atEdge(left) && !h->isInWord(doc()->line(c.line())[c.column() - 1]) // in order to stay symmetric to wordLeft() // we must not skip space preceding a non-word sequence && !doc()->line(c.line())[c.column() - 1].isSpace()) { --c; } } updateSelection(c, sel); updateCursor(c); } void KateViewInternal::wordNext(bool sel) { WrappingCursor c(this, m_cursor); // We look up into which category the current position falls: // 1. a "word" character // 2. a "non-word" character (except space) // 3. the end of the line // and skip all following characters that fall into this class. // If the skipped characters are followed by space, we skip that too. // The code assumes that space is never part of the word character class. KateHighlighting *h = doc()->highlight(); if (c.atEdge(right)) { ++c; } else if (h->isInWord(doc()->line(c.line())[c.column()])) { while (!c.atEdge(right) && h->isInWord(doc()->line(c.line())[c.column()])) { ++c; } } else { while (!c.atEdge(right) && !h->isInWord(doc()->line(c.line())[c.column()]) // we must not skip space, because if that space is followed // by more non-word characters, we would skip them, too && !doc()->line(c.line())[c.column()].isSpace()) { ++c; } } while (!c.atEdge(right) && doc()->line(c.line())[c.column()].isSpace()) { ++c; } updateSelection(c, sel); updateCursor(c); } void KateViewInternal::moveEdge(KateViewInternal::Bias bias, bool sel) { BoundedCursor c(this, m_cursor); c.toEdge(bias); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::home(bool sel) { if (view()->dynWordWrap() && currentLayout().startCol()) { // Allow us to go to the real start if we're already at the start of the view line if (m_cursor.column() != currentLayout().startCol()) { KTextEditor::Cursor c = currentLayout().start(); updateSelection(c, sel); updateCursor(c); return; } } if (!doc()->config()->smartHome()) { moveEdge(left, sel); return; } Kate::TextLine l = doc()->kateTextLine(m_cursor.line()); if (!l) { return; } KTextEditor::Cursor c = m_cursor; int lc = l->firstChar(); if (lc < 0 || c.column() == lc) { c.setColumn(0); } else { c.setColumn(lc); } updateSelection(c, sel); updateCursor(c, true); } void KateViewInternal::end(bool sel) { KateTextLayout layout = currentLayout(); if (view()->dynWordWrap() && layout.wrap()) { // Allow us to go to the real end if we're already at the end of the view line if (m_cursor.column() < layout.endCol() - 1) { KTextEditor::Cursor c(m_cursor.line(), layout.endCol() - 1); updateSelection(c, sel); updateCursor(c); return; } } if (!doc()->config()->smartHome()) { moveEdge(right, sel); return; } Kate::TextLine l = doc()->kateTextLine(m_cursor.line()); if (!l) { return; } // "Smart End", as requested in bugs #78258 and #106970 if (m_cursor.column() == doc()->lineLength(m_cursor.line())) { KTextEditor::Cursor c = m_cursor; c.setColumn(l->lastChar() + 1); updateSelection(c, sel); updateCursor(c, true); } else { moveEdge(right, sel); } } KateTextLayout KateViewInternal::currentLayout() const { return cache()->textLayout(m_cursor); } KateTextLayout KateViewInternal::previousLayout() const { int currentViewLine = cache()->viewLine(m_cursor); if (currentViewLine) { return cache()->textLayout(m_cursor.line(), currentViewLine - 1); } else { return cache()->textLayout(view()->textFolding().visibleLineToLine(m_displayCursor.line() - 1), -1); } } KateTextLayout KateViewInternal::nextLayout() const { int currentViewLine = cache()->viewLine(m_cursor) + 1; if (currentViewLine >= cache()->line(m_cursor.line())->viewLineCount()) { currentViewLine = 0; return cache()->textLayout(view()->textFolding().visibleLineToLine(m_displayCursor.line() + 1), currentViewLine); } else { return cache()->textLayout(m_cursor.line(), currentViewLine); } } /* * This returns the cursor which is offset by (offset) view lines. * This is the main function which is called by code not specifically dealing with word-wrap. * The opposite conversion (cursor to offset) can be done with cache()->displayViewLine(). * * The cursors involved are virtual cursors (ie. equivalent to m_displayCursor) */ KTextEditor::Cursor KateViewInternal::viewLineOffset(const KTextEditor::Cursor &virtualCursor, int offset, bool keepX) { if (!view()->dynWordWrap()) { KTextEditor::Cursor ret(qMin((int)view()->textFolding().visibleLines() - 1, virtualCursor.line() + offset), 0); if (ret.line() < 0) { ret.setLine(0); } if (keepX) { int realLine = view()->textFolding().visibleLineToLine(ret.line()); KateTextLayout t = cache()->textLayout(realLine, 0); Q_ASSERT(t.isValid()); ret.setColumn(renderer()->xToCursor(t, m_preservedX, !view()->wrapCursor()).column()); } return ret; } KTextEditor::Cursor realCursor = virtualCursor; realCursor.setLine(view()->textFolding().visibleLineToLine(view()->textFolding().lineToVisibleLine(virtualCursor.line()))); int cursorViewLine = cache()->viewLine(realCursor); int currentOffset = 0; int virtualLine = 0; bool forwards = (offset > 0) ? true : false; if (forwards) { currentOffset = cache()->lastViewLine(realCursor.line()) - cursorViewLine; if (offset <= currentOffset) { // the answer is on the same line KateTextLayout thisLine = cache()->textLayout(realCursor.line(), cursorViewLine + offset); Q_ASSERT(thisLine.virtualLine() == (int)view()->textFolding().lineToVisibleLine(virtualCursor.line())); return KTextEditor::Cursor(virtualCursor.line(), thisLine.startCol()); } virtualLine = virtualCursor.line() + 1; } else { offset = -offset; currentOffset = cursorViewLine; if (offset <= currentOffset) { // the answer is on the same line KateTextLayout thisLine = cache()->textLayout(realCursor.line(), cursorViewLine - offset); Q_ASSERT(thisLine.virtualLine() == (int)view()->textFolding().lineToVisibleLine(virtualCursor.line())); return KTextEditor::Cursor(virtualCursor.line(), thisLine.startCol()); } virtualLine = virtualCursor.line() - 1; } currentOffset++; while (virtualLine >= 0 && virtualLine < (int)view()->textFolding().visibleLines()) { int realLine = view()->textFolding().visibleLineToLine(virtualLine); KateLineLayoutPtr thisLine = cache()->line(realLine, virtualLine); if (!thisLine) { break; } for (int i = 0; i < thisLine->viewLineCount(); ++i) { if (offset == currentOffset) { KateTextLayout thisViewLine = thisLine->viewLine(i); if (!forwards) { // We actually want it the other way around int requiredViewLine = cache()->lastViewLine(realLine) - thisViewLine.viewLine(); if (requiredViewLine != thisViewLine.viewLine()) { thisViewLine = thisLine->viewLine(requiredViewLine); } } KTextEditor::Cursor ret(virtualLine, thisViewLine.startCol()); // keep column position if (keepX) { KTextEditor::Cursor realCursor = toRealCursor(virtualCursor); KateTextLayout t = cache()->textLayout(realCursor); // renderer()->cursorToX(t, realCursor, !view()->wrapCursor()); realCursor = renderer()->xToCursor(thisViewLine, m_preservedX, !view()->wrapCursor()); ret.setColumn(realCursor.column()); } return ret; } currentOffset++; } if (forwards) { virtualLine++; } else { virtualLine--; } } // Looks like we were asked for something a bit exotic. // Return the max/min valid position. if (forwards) { return KTextEditor::Cursor(view()->textFolding().visibleLines() - 1, doc()->lineLength(view()->textFolding().visibleLineToLine(view()->textFolding().visibleLines() - 1))); } else { return KTextEditor::Cursor(0, 0); } } int KateViewInternal::lineMaxCursorX(const KateTextLayout &range) { if (!view()->wrapCursor() && !range.wrap()) { return INT_MAX; } int maxX = range.endX(); if (maxX && range.wrap()) { QChar lastCharInLine = doc()->kateTextLine(range.line())->at(range.endCol() - 1); maxX -= renderer()->currentFontMetrics().horizontalAdvance(lastCharInLine); } return maxX; } int KateViewInternal::lineMaxCol(const KateTextLayout &range) { int maxCol = range.endCol(); if (maxCol && range.wrap()) { maxCol--; } return maxCol; } void KateViewInternal::cursorUp(bool sel) { if (!sel && view()->completionWidget()->isCompletionActive()) { view()->completionWidget()->cursorUp(); return; } // assert that the display cursor is in visible lines Q_ASSERT(m_displayCursor.line() < view()->textFolding().visibleLines()); /** * move cursor to start of line, if we are at first line! */ if (m_displayCursor.line() == 0 && (!view()->dynWordWrap() || cache()->viewLine(m_cursor) == 0)) { home(sel); return; } m_preserveX = true; KateTextLayout thisLine = currentLayout(); // This is not the first line because that is already simplified out above KateTextLayout pRange = previousLayout(); // Ensure we're in the right spot Q_ASSERT(m_cursor.line() == thisLine.line()); Q_ASSERT(m_cursor.column() >= thisLine.startCol()); Q_ASSERT(!thisLine.wrap() || m_cursor.column() < thisLine.endCol()); KTextEditor::Cursor c = renderer()->xToCursor(pRange, m_preservedX, !view()->wrapCursor()); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::cursorDown(bool sel) { if (!sel && view()->completionWidget()->isCompletionActive()) { view()->completionWidget()->cursorDown(); return; } /** * move cursor to end of line, if we are at last line! */ if ((m_displayCursor.line() >= view()->textFolding().visibleLines() - 1) && (!view()->dynWordWrap() || cache()->viewLine(m_cursor) == cache()->lastViewLine(m_cursor.line()))) { end(sel); return; } m_preserveX = true; KateTextLayout thisLine = currentLayout(); // This is not the last line because that is already simplified out above KateTextLayout nRange = nextLayout(); // Ensure we're in the right spot Q_ASSERT((m_cursor.line() == thisLine.line()) && (m_cursor.column() >= thisLine.startCol()) && (!thisLine.wrap() || m_cursor.column() < thisLine.endCol())); KTextEditor::Cursor c = renderer()->xToCursor(nRange, m_preservedX, !view()->wrapCursor()); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::cursorToMatchingBracket(bool sel) { KTextEditor::Cursor c = findMatchingBracket(); if (c.isValid()) { updateSelection(c, sel); updateCursor(c); } } void KateViewInternal::topOfView(bool sel) { KTextEditor::Cursor c = viewLineOffset(startPos(), m_minLinesVisible); updateSelection(toRealCursor(c), sel); updateCursor(toRealCursor(c)); } void KateViewInternal::bottomOfView(bool sel) { KTextEditor::Cursor c = viewLineOffset(endPos(), -m_minLinesVisible); updateSelection(toRealCursor(c), sel); updateCursor(toRealCursor(c)); } // lines is the offset to scroll by void KateViewInternal::scrollLines(int lines, bool sel) { KTextEditor::Cursor c = viewLineOffset(m_displayCursor, lines, true); // Fix the virtual cursor -> real cursor c.setLine(view()->textFolding().visibleLineToLine(c.line())); updateSelection(c, sel); updateCursor(c); } // This is a bit misleading... it's asking for the view to be scrolled, not the cursor void KateViewInternal::scrollUp() { KTextEditor::Cursor newPos = viewLineOffset(startPos(), -1); scrollPos(newPos); } void KateViewInternal::scrollDown() { KTextEditor::Cursor newPos = viewLineOffset(startPos(), 1); scrollPos(newPos); } void KateViewInternal::setAutoCenterLines(int viewLines, bool updateView) { m_autoCenterLines = viewLines; m_minLinesVisible = qMin(int((linesDisplayed() - 1) / 2), m_autoCenterLines); if (updateView) { KateViewInternal::updateView(); } } void KateViewInternal::pageUp(bool sel, bool half) { if (view()->isCompletionActive()) { view()->completionWidget()->pageUp(); return; } // remember the view line and x pos int viewLine = cache()->displayViewLine(m_displayCursor); bool atTop = startPos().atStartOfDocument(); // Adjust for an auto-centering cursor int lineadj = m_minLinesVisible; int linesToScroll; if (!half) { linesToScroll = -qMax((linesDisplayed() - 1) - lineadj, 0); } else { linesToScroll = -qMax((linesDisplayed() / 2 - 1) - lineadj, 0); } m_preserveX = true; if (!doc()->pageUpDownMovesCursor() && !atTop) { KTextEditor::Cursor newStartPos = viewLineOffset(startPos(), linesToScroll - 1); scrollPos(newStartPos); // put the cursor back approximately where it was KTextEditor::Cursor newPos = toRealCursor(viewLineOffset(newStartPos, viewLine, true)); KateTextLayout newLine = cache()->textLayout(newPos); newPos = renderer()->xToCursor(newLine, m_preservedX, !view()->wrapCursor()); m_preserveX = true; updateSelection(newPos, sel); updateCursor(newPos); } else { scrollLines(linesToScroll, sel); } } void KateViewInternal::pageDown(bool sel, bool half) { if (view()->isCompletionActive()) { view()->completionWidget()->pageDown(); return; } // remember the view line int viewLine = cache()->displayViewLine(m_displayCursor); bool atEnd = startPos() >= m_cachedMaxStartPos; // Adjust for an auto-centering cursor int lineadj = m_minLinesVisible; int linesToScroll; if (!half) { linesToScroll = qMax((linesDisplayed() - 1) - lineadj, 0); } else { linesToScroll = qMax((linesDisplayed() / 2 - 1) - lineadj, 0); } m_preserveX = true; if (!doc()->pageUpDownMovesCursor() && !atEnd) { KTextEditor::Cursor newStartPos = viewLineOffset(startPos(), linesToScroll + 1); scrollPos(newStartPos); // put the cursor back approximately where it was KTextEditor::Cursor newPos = toRealCursor(viewLineOffset(newStartPos, viewLine, true)); KateTextLayout newLine = cache()->textLayout(newPos); newPos = renderer()->xToCursor(newLine, m_preservedX, !view()->wrapCursor()); m_preserveX = true; updateSelection(newPos, sel); updateCursor(newPos); } else { scrollLines(linesToScroll, sel); } } int KateViewInternal::maxLen(int startLine) { Q_ASSERT(!view()->dynWordWrap()); int displayLines = (view()->height() / renderer()->lineHeight()) + 1; int maxLen = 0; for (int z = 0; z < displayLines; z++) { int virtualLine = startLine + z; if (virtualLine < 0 || virtualLine >= (int)view()->textFolding().visibleLines()) { break; } maxLen = qMax(maxLen, cache()->line(view()->textFolding().visibleLineToLine(virtualLine))->width()); } return maxLen; } bool KateViewInternal::columnScrollingPossible() { return !view()->dynWordWrap() && m_columnScroll->isEnabled() && (m_columnScroll->maximum() > 0); } bool KateViewInternal::lineScrollingPossible() { return m_lineScroll->minimum() != m_lineScroll->maximum(); } void KateViewInternal::top(bool sel) { KTextEditor::Cursor newCursor(0, 0); newCursor = renderer()->xToCursor(cache()->textLayout(newCursor), m_preservedX, !view()->wrapCursor()); updateSelection(newCursor, sel); updateCursor(newCursor); } void KateViewInternal::bottom(bool sel) { KTextEditor::Cursor newCursor(doc()->lastLine(), 0); newCursor = renderer()->xToCursor(cache()->textLayout(newCursor), m_preservedX, !view()->wrapCursor()); updateSelection(newCursor, sel); updateCursor(newCursor); } void KateViewInternal::top_home(bool sel) { if (view()->isCompletionActive()) { view()->completionWidget()->top(); return; } KTextEditor::Cursor c(0, 0); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::bottom_end(bool sel) { if (view()->isCompletionActive()) { view()->completionWidget()->bottom(); return; } KTextEditor::Cursor c(doc()->lastLine(), doc()->lineLength(doc()->lastLine())); updateSelection(c, sel); updateCursor(c); } void KateViewInternal::updateSelection(const KTextEditor::Cursor &_newCursor, bool keepSel) { KTextEditor::Cursor newCursor = _newCursor; if (keepSel) { if (!view()->selection() || (m_selectAnchor.line() == -1) // don't kill the selection if we have a persistent selection and // the cursor is inside or at the boundaries of the selected area || (view()->config()->persistentSelection() && !(view()->selectionRange().contains(m_cursor) || view()->selectionRange().boundaryAtCursor(m_cursor)))) { m_selectAnchor = m_cursor; setSelection(KTextEditor::Range(m_cursor, newCursor)); } else { bool doSelect = true; switch (m_selectionMode) { - case Word: { - // Restore selStartCached if needed. It gets nuked by - // viewSelectionChanged if we drag the selection into non-existence, - // which can legitimately happen if a shift+DC selection is unable to - // set a "proper" (i.e. non-empty) cached selection, e.g. because the - // start was on something that isn't a word. Word select mode relies - // on the cached selection being set properly, even if it is empty - // (i.e. selStartCached == selEndCached). - if (!m_selectionCached.isValid()) { - m_selectionCached.setStart(m_selectionCached.end()); - } + case Word: { + // Restore selStartCached if needed. It gets nuked by + // viewSelectionChanged if we drag the selection into non-existence, + // which can legitimately happen if a shift+DC selection is unable to + // set a "proper" (i.e. non-empty) cached selection, e.g. because the + // start was on something that isn't a word. Word select mode relies + // on the cached selection being set properly, even if it is empty + // (i.e. selStartCached == selEndCached). + if (!m_selectionCached.isValid()) { + m_selectionCached.setStart(m_selectionCached.end()); + } - int c; - if (newCursor > m_selectionCached.start()) { - m_selectAnchor = m_selectionCached.start(); + int c; + if (newCursor > m_selectionCached.start()) { + m_selectAnchor = m_selectionCached.start(); - Kate::TextLine l = doc()->kateTextLine(newCursor.line()); + Kate::TextLine l = doc()->kateTextLine(newCursor.line()); - c = newCursor.column(); - if (c > 0 && doc()->highlight()->isInWord(l->at(c - 1))) { - for (; c < l->length(); c++) - if (!doc()->highlight()->isInWord(l->at(c))) { - break; - } - } + c = newCursor.column(); + if (c > 0 && doc()->highlight()->isInWord(l->at(c - 1))) { + for (; c < l->length(); c++) + if (!doc()->highlight()->isInWord(l->at(c))) { + break; + } + } - newCursor.setColumn(c); - } else if (newCursor < m_selectionCached.start()) { - m_selectAnchor = m_selectionCached.end(); + newCursor.setColumn(c); + } else if (newCursor < m_selectionCached.start()) { + m_selectAnchor = m_selectionCached.end(); - Kate::TextLine l = doc()->kateTextLine(newCursor.line()); + Kate::TextLine l = doc()->kateTextLine(newCursor.line()); - c = newCursor.column(); - if (c > 0 && c < doc()->lineLength(newCursor.line()) && doc()->highlight()->isInWord(l->at(c)) && doc()->highlight()->isInWord(l->at(c - 1))) { - for (c -= 2; c >= 0; c--) - if (!doc()->highlight()->isInWord(l->at(c))) { - break; - } - newCursor.setColumn(c + 1); - } - } else { - doSelect = false; + c = newCursor.column(); + if (c > 0 && c < doc()->lineLength(newCursor.line()) && doc()->highlight()->isInWord(l->at(c)) && doc()->highlight()->isInWord(l->at(c - 1))) { + for (c -= 2; c >= 0; c--) + if (!doc()->highlight()->isInWord(l->at(c))) { + break; + } + newCursor.setColumn(c + 1); } + } else { + doSelect = false; + } - } break; - case Line: - if (!m_selectionCached.isValid()) { - m_selectionCached = KTextEditor::Range(endLine(), 0, endLine(), 0); + } break; + case Line: + if (!m_selectionCached.isValid()) { + m_selectionCached = KTextEditor::Range(endLine(), 0, endLine(), 0); + } + if (newCursor.line() > m_selectionCached.start().line()) { + if (newCursor.line() + 1 >= doc()->lines()) { + newCursor.setColumn(doc()->line(newCursor.line()).length()); + } else { + newCursor.setPosition(newCursor.line() + 1, 0); } - if (newCursor.line() > m_selectionCached.start().line()) { - if (newCursor.line() + 1 >= doc()->lines()) { - newCursor.setColumn(doc()->line(newCursor.line()).length()); + // Grow to include the entire line + m_selectAnchor = m_selectionCached.start(); + m_selectAnchor.setColumn(0); + } else if (newCursor.line() < m_selectionCached.start().line()) { + newCursor.setColumn(0); + // Grow to include entire line + m_selectAnchor = m_selectionCached.end(); + if (m_selectAnchor.column() > 0) { + if (m_selectAnchor.line() + 1 >= doc()->lines()) { + m_selectAnchor.setColumn(doc()->line(newCursor.line()).length()); } else { - newCursor.setPosition(newCursor.line() + 1, 0); + m_selectAnchor.setPosition(m_selectAnchor.line() + 1, 0); } - // Grow to include the entire line - m_selectAnchor = m_selectionCached.start(); - m_selectAnchor.setColumn(0); - } else if (newCursor.line() < m_selectionCached.start().line()) { - newCursor.setColumn(0); - // Grow to include entire line - m_selectAnchor = m_selectionCached.end(); - if (m_selectAnchor.column() > 0) { - if (m_selectAnchor.line() + 1 >= doc()->lines()) { - m_selectAnchor.setColumn(doc()->line(newCursor.line()).length()); - } else { - m_selectAnchor.setPosition(m_selectAnchor.line() + 1, 0); - } - } - } else { // same line, ignore - doSelect = false; } + } else { // same line, ignore + doSelect = false; + } + break; + case Mouse: { + if (!m_selectionCached.isValid()) { break; - case Mouse: { - if (!m_selectionCached.isValid()) { - break; - } + } - if (newCursor > m_selectionCached.end()) { - m_selectAnchor = m_selectionCached.start(); - } else if (newCursor < m_selectionCached.start()) { - m_selectAnchor = m_selectionCached.end(); - } else { - doSelect = false; - } - } break; - default: /* nothing special to do */; + if (newCursor > m_selectionCached.end()) { + m_selectAnchor = m_selectionCached.start(); + } else if (newCursor < m_selectionCached.start()) { + m_selectAnchor = m_selectionCached.end(); + } else { + doSelect = false; + } + } break; + default: /* nothing special to do */; } if (doSelect) { setSelection(KTextEditor::Range(m_selectAnchor, newCursor)); } else if (m_selectionCached.isValid()) { // we have a cached selection, so we restore that setSelection(m_selectionCached); } } m_selChangedByUser = true; } else if (!view()->config()->persistentSelection()) { view()->clearSelection(); m_selectionCached = KTextEditor::Range::invalid(); m_selectAnchor = KTextEditor::Cursor::invalid(); } #ifndef QT_NO_ACCESSIBILITY // FIXME KF5 // QAccessibleTextSelectionEvent ev(this, /* selection start, selection end*/); // QAccessible::updateAccessibility(&ev); #endif } void KateViewInternal::setSelection(const KTextEditor::Range &range) { disconnect(m_view, SIGNAL(selectionChanged(KTextEditor::View *)), this, SLOT(viewSelectionChanged())); view()->setSelection(range); connect(m_view, SIGNAL(selectionChanged(KTextEditor::View *)), this, SLOT(viewSelectionChanged())); } void KateViewInternal::moveCursorToSelectionEdge() { if (!view()->selection()) { return; } int tmp = m_minLinesVisible; m_minLinesVisible = 0; if (view()->selectionRange().start() < m_selectAnchor) { updateCursor(view()->selectionRange().start()); } else { updateCursor(view()->selectionRange().end()); } m_minLinesVisible = tmp; } void KateViewInternal::updateCursor(const KTextEditor::Cursor &newCursor, bool force, bool center, bool calledExternally) { if (!force && (m_cursor.toCursor() == newCursor)) { m_displayCursor = toVirtualCursor(newCursor); if (!m_madeVisible && m_view == doc()->activeView()) { // unfold if required view()->textFolding().ensureLineIsVisible(newCursor.line()); makeVisible(m_displayCursor, m_displayCursor.column(), false, center, calledExternally); } return; } if (m_cursor.line() != newCursor.line()) { m_leftBorder->updateForCursorLineChange(); } // unfold if required view()->textFolding().ensureLineIsVisible(newCursor.line()); KTextEditor::Cursor oldDisplayCursor = m_displayCursor; m_displayCursor = toVirtualCursor(newCursor); m_cursor.setPosition(newCursor); if (m_view == doc()->activeView()) { makeVisible(m_displayCursor, m_displayCursor.column(), false, center, calledExternally); } updateBracketMarks(); // It's efficient enough to just tag them both without checking to see if they're on the same view line /* kdDebug()<<"oldDisplayCursor:"< 0) { m_cursorTimer.start(QApplication::cursorFlashTime() / 2); } renderer()->setDrawCaret(true); } // Remember the maximum X position if requested if (m_preserveX) { m_preserveX = false; } else { m_preservedX = renderer()->cursorToX(cache()->textLayout(m_cursor), m_cursor, !view()->wrapCursor()); } // qCDebug(LOG_KTE) << "m_preservedX: " << m_preservedX << " (was "<< oldmaxx << "), m_cursorX: " << m_cursorX; // qCDebug(LOG_KTE) << "Cursor now located at real " << cursor.line << "," << cursor.col << ", virtual " << m_displayCursor.line << ", " << m_displayCursor.col << "; Top is " << startLine() << ", " << startPos().col; cursorMoved(); updateDirty(); // paintText(0, 0, width(), height(), true); emit view()->cursorPositionChanged(m_view, m_cursor); } void KateViewInternal::updateBracketMarkAttributes() { KTextEditor::Attribute::Ptr bracketFill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); bracketFill->setBackground(view()->m_renderer->config()->highlightedBracketColor()); bracketFill->setBackgroundFillWhitespace(false); if (QFontInfo(renderer()->currentFont()).fixedPitch()) { // make font bold only for fixed fonts, otherwise text jumps around bracketFill->setFontBold(); } m_bmStart->setAttribute(bracketFill); m_bmEnd->setAttribute(bracketFill); if (view()->m_renderer->config()->showWholeBracketExpression()) { KTextEditor::Attribute::Ptr expressionFill = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); expressionFill->setBackground(view()->m_renderer->config()->highlightedBracketColor()); expressionFill->setBackgroundFillWhitespace(false); m_bm->setAttribute(expressionFill); } else { m_bm->setAttribute(KTextEditor::Attribute::Ptr(new KTextEditor::Attribute())); } } void KateViewInternal::updateBracketMarks() { // add some limit to this, this is really endless on big files without limit const int maxLines = 5000; const KTextEditor::Range newRange = doc()->findMatchingBracket(m_cursor, maxLines); // new range valid, then set ranges to it if (newRange.isValid()) { if (m_bm->toRange() == newRange) { return; } // modify full range m_bm->setRange(newRange); // modify start and end ranges m_bmStart->setRange(KTextEditor::Range(m_bm->start(), KTextEditor::Cursor(m_bm->start().line(), m_bm->start().column() + 1))); m_bmEnd->setRange(KTextEditor::Range(m_bm->end(), KTextEditor::Cursor(m_bm->end().line(), m_bm->end().column() + 1))); // flash matching bracket if (!renderer()->config()->animateBracketMatching()) { return; } const KTextEditor::Cursor flashPos = (m_cursor == m_bmStart->start() || m_cursor == m_bmStart->end()) ? m_bmEnd->start() : m_bm->start(); if (flashPos != m_bmLastFlashPos->toCursor()) { m_bmLastFlashPos->setPosition(flashPos); KTextEditor::Attribute::Ptr attribute = doc()->attributeAt(flashPos); attribute->setBackground(view()->m_renderer->config()->highlightedBracketColor()); attribute->setFontBold(m_bmStart->attribute()->fontBold()); flashChar(flashPos, attribute); } return; } // new range was invalid m_bm->setRange(KTextEditor::Range::invalid()); m_bmStart->setRange(KTextEditor::Range::invalid()); m_bmEnd->setRange(KTextEditor::Range::invalid()); m_bmLastFlashPos->setPosition(KTextEditor::Cursor::invalid()); } bool KateViewInternal::tagLine(const KTextEditor::Cursor &virtualCursor) { // FIXME may be a more efficient way for this if ((int)view()->textFolding().visibleLineToLine(virtualCursor.line()) > doc()->lastLine()) { return false; } // End FIXME int viewLine = cache()->displayViewLine(virtualCursor, true); if (viewLine >= 0 && viewLine < cache()->viewCacheLineCount()) { cache()->viewLine(viewLine).setDirty(); // tag one line more because of overlapping things like _, bug 335079 if (viewLine + 1 < cache()->viewCacheLineCount()) { cache()->viewLine(viewLine + 1).setDirty(); } m_leftBorder->update(0, lineToY(viewLine), m_leftBorder->width(), renderer()->lineHeight()); return true; } return false; } bool KateViewInternal::tagLines(int start, int end, bool realLines) { return tagLines(KTextEditor::Cursor(start, 0), KTextEditor::Cursor(end, -1), realLines); } bool KateViewInternal::tagLines(KTextEditor::Cursor start, KTextEditor::Cursor end, bool realCursors) { if (realCursors) { cache()->relayoutLines(start.line(), end.line()); // qCDebug(LOG_KTE)<<"realLines is true"; start = toVirtualCursor(start); end = toVirtualCursor(end); } else { cache()->relayoutLines(toRealCursor(start).line(), toRealCursor(end).line()); } if (end.line() < startLine()) { // qCDebug(LOG_KTE)<<"end endLine(), but cache may not be valid when checking, so use a // less optimal but still adequate approximation (potential overestimation but minimal performance difference) if (start.line() > startLine() + cache()->viewCacheLineCount()) { // qCDebug(LOG_KTE)<<"start> endLine"<updateViewCache(startPos()); // qCDebug(LOG_KTE) << "tagLines( [" << start << "], [" << end << "] )"; bool ret = false; for (int z = 0; z < cache()->viewCacheLineCount(); z++) { KateTextLayout &line = cache()->viewLine(z); if ((line.virtualLine() > start.line() || (line.virtualLine() == start.line() && line.endCol() >= start.column() && start.column() != -1)) && (line.virtualLine() < end.line() || (line.virtualLine() == end.line() && (line.startCol() <= end.column() || end.column() == -1)))) { ret = true; break; // qCDebug(LOG_KTE) << "Tagged line " << line.line(); } } if (!view()->dynWordWrap()) { int y = lineToY(start.line()); // FIXME is this enough for when multiple lines are deleted int h = (end.line() - start.line() + 2) * renderer()->lineHeight(); if (end.line() >= view()->textFolding().visibleLines() - 1) { h = height(); } m_leftBorder->update(0, y, m_leftBorder->width(), h); } else { // FIXME Do we get enough good info in editRemoveText to optimize this more? // bool justTagged = false; for (int z = 0; z < cache()->viewCacheLineCount(); z++) { KateTextLayout &line = cache()->viewLine(z); if (!line.isValid() || ((line.virtualLine() > start.line() || (line.virtualLine() == start.line() && line.endCol() >= start.column() && start.column() != -1)) && (line.virtualLine() < end.line() || (line.virtualLine() == end.line() && (line.startCol() <= end.column() || end.column() == -1))))) { // justTagged = true; m_leftBorder->update(0, z * renderer()->lineHeight(), m_leftBorder->width(), m_leftBorder->height()); break; } /*else if (justTagged) { justTagged = false; leftBorder->update (0, z * doc()->viewFont.fontHeight, leftBorder->width(), doc()->viewFont.fontHeight); break; }*/ } } return ret; } bool KateViewInternal::tagRange(const KTextEditor::Range &range, bool realCursors) { return tagLines(range.start(), range.end(), realCursors); } void KateViewInternal::tagAll() { // clear the cache... cache()->clear(); m_leftBorder->updateFont(); m_leftBorder->update(); } void KateViewInternal::paintCursor() { if (tagLine(m_displayCursor)) { updateDirty(); // paintText (0,0,width(), height(), true); } } // Point in content coordinates void KateViewInternal::placeCursor(const QPoint &p, bool keepSelection, bool updateSelection) { KateTextLayout thisLine = yToKateTextLayout(p.y()); KTextEditor::Cursor c; if (!thisLine.isValid()) { // probably user clicked below the last line -> use the last line thisLine = cache()->textLayout(doc()->lines() - 1, -1); } c = renderer()->xToCursor(thisLine, startX() + p.x(), !view()->wrapCursor()); if (c.line() < 0 || c.line() >= doc()->lines()) { return; } if (updateSelection) { KateViewInternal::updateSelection(c, keepSelection); } int tmp = m_minLinesVisible; m_minLinesVisible = 0; updateCursor(c); m_minLinesVisible = tmp; if (updateSelection && keepSelection) { moveCursorToSelectionEdge(); } } // Point in content coordinates bool KateViewInternal::isTargetSelected(const QPoint &p) { const KateTextLayout &thisLine = yToKateTextLayout(p.y()); if (!thisLine.isValid()) { return false; } return view()->cursorSelected(renderer()->xToCursor(thisLine, startX() + p.x(), !view()->wrapCursor())); } // BEGIN EVENT HANDLING STUFF bool KateViewInternal::eventFilter(QObject *obj, QEvent *e) { switch (e->type()) { - case QEvent::ChildAdded: - case QEvent::ChildRemoved: { - QChildEvent *c = static_cast(e); - if (c->added()) { - c->child()->installEventFilter(this); - - } else if (c->removed()) { - c->child()->removeEventFilter(this); - } - } break; - - case QEvent::ShortcutOverride: { - QKeyEvent *k = static_cast(e); - - if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { - if (view()->isCompletionActive()) { - view()->abortCompletion(); - k->accept(); - // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "aborting completion"; - return true; - } else if (!view()->bottomViewBar()->hiddenOrPermanent()) { - view()->bottomViewBar()->hideCurrentBarWidget(); - k->accept(); - // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "closing view bar"; - return true; - } else if (!view()->config()->persistentSelection() && view()->selection()) { - m_currentInputMode->clearSelection(); - k->accept(); - // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "clearing selection"; - return true; - } - } + case QEvent::ChildAdded: + case QEvent::ChildRemoved: { + QChildEvent *c = static_cast(e); + if (c->added()) { + c->child()->installEventFilter(this); + + } else if (c->removed()) { + c->child()->removeEventFilter(this); + } + } break; + + case QEvent::ShortcutOverride: { + QKeyEvent *k = static_cast(e); - if (m_currentInputMode->stealKey(k)) { + if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { + if (view()->isCompletionActive()) { + view()->abortCompletion(); k->accept(); + // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "aborting completion"; + return true; + } else if (!view()->bottomViewBar()->hiddenOrPermanent()) { + view()->bottomViewBar()->hideCurrentBarWidget(); + k->accept(); + // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "closing view bar"; + return true; + } else if (!view()->config()->persistentSelection() && view()->selection()) { + m_currentInputMode->clearSelection(); + k->accept(); + // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "clearing selection"; return true; } + } - } break; + if (m_currentInputMode->stealKey(k)) { + k->accept(); + return true; + } - case QEvent::KeyPress: { - QKeyEvent *k = static_cast(e); + } break; - // Override all other single key shortcuts which do not use a modifier other than Shift - if (obj == this && (!k->modifiers() || k->modifiers() == Qt::ShiftModifier)) { - keyPressEvent(k); - if (k->isAccepted()) { - // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "using keystroke"; - return true; - } + case QEvent::KeyPress: { + QKeyEvent *k = static_cast(e); + + // Override all other single key shortcuts which do not use a modifier other than Shift + if (obj == this && (!k->modifiers() || k->modifiers() == Qt::ShiftModifier)) { + keyPressEvent(k); + if (k->isAccepted()) { + // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "using keystroke"; + return true; } + } - // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "ignoring"; - } break; + // qCDebug(LOG_KTE) << obj << "shortcut override" << k->key() << "ignoring"; + } break; - case QEvent::DragMove: { - QPoint currentPoint = ((QDragMoveEvent *)e)->pos(); + case QEvent::DragMove: { + QPoint currentPoint = ((QDragMoveEvent *)e)->pos(); - QRect doNotScrollRegion(s_scrollMargin, s_scrollMargin, width() - s_scrollMargin * 2, height() - s_scrollMargin * 2); + QRect doNotScrollRegion(s_scrollMargin, s_scrollMargin, width() - s_scrollMargin * 2, height() - s_scrollMargin * 2); - if (!doNotScrollRegion.contains(currentPoint)) { - startDragScroll(); - // Keep sending move events - ((QDragMoveEvent *)e)->accept(QRect(0, 0, 0, 0)); - } + if (!doNotScrollRegion.contains(currentPoint)) { + startDragScroll(); + // Keep sending move events + ((QDragMoveEvent *)e)->accept(QRect(0, 0, 0, 0)); + } - dragMoveEvent((QDragMoveEvent *)e); - } break; + dragMoveEvent((QDragMoveEvent *)e); + } break; - case QEvent::DragLeave: - // happens only when pressing ESC while dragging - stopDragScroll(); - break; + case QEvent::DragLeave: + // happens only when pressing ESC while dragging + stopDragScroll(); + break; - default: - break; + default: + break; } return QWidget::eventFilter(obj, e); } void KateViewInternal::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Left && e->modifiers() == Qt::AltModifier) { view()->emitNavigateLeft(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Right && e->modifiers() == Qt::AltModifier) { view()->emitNavigateRight(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Up && e->modifiers() == Qt::AltModifier) { view()->emitNavigateUp(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Down && e->modifiers() == Qt::AltModifier) { view()->emitNavigateDown(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Return && e->modifiers() == Qt::AltModifier) { view()->emitNavigateAccept(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Backspace && e->modifiers() == Qt::AltModifier) { view()->emitNavigateBack(); e->setAccepted(true); return; } if (e->key() == Qt::Key_Alt && view()->completionWidget()->isCompletionActive()) { m_completionItemExpanded = view()->completionWidget()->toggleExpanded(true); view()->completionWidget()->resetHadNavigation(); m_altDownTime.start(); } // Note: AND'ing with is a quick hack to fix Key_Enter const int key = e->key() | (e->modifiers() & Qt::ShiftModifier); if (m_currentInputMode->keyPress(e)) { return; } if (!doc()->isReadWrite()) { e->ignore(); return; } if ((key == Qt::Key_Return) || (key == Qt::Key_Enter) || (key == Qt::SHIFT + Qt::Key_Return) || (key == Qt::SHIFT + Qt::Key_Enter)) { view()->keyReturn(); e->accept(); return; } if (key == Qt::Key_Backspace || key == Qt::SHIFT + Qt::Key_Backspace) { // view()->backspace(); e->accept(); return; } if (key == Qt::Key_Tab || key == Qt::SHIFT + Qt::Key_Backtab || key == Qt::Key_Backtab) { if (view()->completionWidget()->isCompletionActive()) { e->accept(); view()->completionWidget()->tab(key != Qt::Key_Tab); return; } if (key == Qt::Key_Tab) { uint tabHandling = doc()->config()->tabHandling(); // convert tabSmart into tabInsertsTab or tabIndents: if (tabHandling == KateDocumentConfig::tabSmart) { // multiple lines selected if (view()->selection() && !view()->selectionRange().onSingleLine()) { tabHandling = KateDocumentConfig::tabIndents; } // otherwise: take look at cursor position else { // if the cursor is at or before the first non-space character // or on an empty line, // Tab indents, otherwise it inserts a tab character. Kate::TextLine line = doc()->kateTextLine(m_cursor.line()); int first = line->firstChar(); if (first < 0 || m_cursor.column() <= first) { tabHandling = KateDocumentConfig::tabIndents; } else { tabHandling = KateDocumentConfig::tabInsertsTab; } } } // either we just insert a tab or we convert that into an indent action if (tabHandling == KateDocumentConfig::tabInsertsTab) { doc()->typeChars(m_view, QStringLiteral("\t")); } else { doc()->indent(view()->selection() ? view()->selectionRange() : KTextEditor::Range(m_cursor.line(), 0, m_cursor.line(), 0), 1); } e->accept(); return; } else if (doc()->config()->tabHandling() != KateDocumentConfig::tabInsertsTab) { // key == Qt::SHIFT+Qt::Key_Backtab || key == Qt::Key_Backtab doc()->indent(view()->selection() ? view()->selectionRange() : KTextEditor::Range(m_cursor.line(), 0, m_cursor.line(), 0), -1); e->accept(); return; } } if (isAcceptableInput(e)) { doc()->typeChars(m_view, e->text()); e->accept(); return; } e->ignore(); } void KateViewInternal::keyReleaseEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Alt && view()->completionWidget()->isCompletionActive() && ((m_completionItemExpanded && (view()->completionWidget()->hadNavigation() || m_altDownTime.elapsed() > 300)) || (!m_completionItemExpanded && !view()->completionWidget()->hadNavigation()))) { view()->completionWidget()->toggleExpanded(false, true); } if ((e->modifiers() & Qt::SHIFT) == Qt::SHIFT) { m_shiftKeyPressed = true; } else { if (m_shiftKeyPressed) { m_shiftKeyPressed = false; if (m_selChangedByUser) { if (view()->selection()) { QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } m_selChangedByUser = false; } } } e->ignore(); return; } bool KateViewInternal::isAcceptableInput(const QKeyEvent *e) const { // reimplemented from QInputControl::isAcceptableInput() const QString text = e->text(); if (text.isEmpty()) { return false; } const QChar c = text.at(0); // Formatting characters such as ZWNJ, ZWJ, RLM, etc. This needs to go before the // next test, since CTRL+SHIFT is sometimes used to input it on Windows. // see bug 389796 (typing formatting characters such as ZWNJ) // and bug 396764 (typing soft-hyphens) if (c.category() == QChar::Other_Format) { return true; } // QTBUG-35734: ignore Ctrl/Ctrl+Shift; accept only AltGr (Alt+Ctrl) on German keyboards if ((e->modifiers() == Qt::ControlModifier) || (e->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier))) { return false; } // printable or private use is good, see e.g. bug 366424 (typing "private use" unicode characters) return c.isPrint() || (c.category() == QChar::Other_PrivateUse); } void KateViewInternal::contextMenuEvent(QContextMenuEvent *e) { // try to show popup menu QPoint p = e->pos(); if (e->reason() == QContextMenuEvent::Keyboard) { makeVisible(m_displayCursor, 0); p = cursorCoordinates(false); p.rx() -= startX(); } else if (!view()->selection() || view()->config()->persistentSelection()) { placeCursor(e->pos()); } // popup is a qguardedptr now if (view()->contextMenu()) { view()->spellingMenu()->setUseMouseForMisspelledRange((e->reason() == QContextMenuEvent::Mouse)); view()->contextMenu()->popup(mapToGlobal(p)); e->accept(); } } void KateViewInternal::mousePressEvent(QMouseEvent *e) { // was an inline note clicked? const auto noteData = inlineNoteAt(e->globalPos()); const KTextEditor::InlineNote note(noteData); if (note.position().isValid()) { note.provider()->inlineNoteActivated(noteData, e->button(), e->globalPos()); return; } // no -- continue with normal handling switch (e->button()) { - case Qt::LeftButton: + case Qt::LeftButton: - m_selChangedByUser = false; - - if (m_possibleTripleClick) { - m_possibleTripleClick = false; + m_selChangedByUser = false; - m_selectionMode = Line; + if (m_possibleTripleClick) { + m_possibleTripleClick = false; - if (e->modifiers() & Qt::ShiftModifier) { - updateSelection(m_cursor, true); - } else { - view()->selectLine(m_cursor); - if (view()->selection()) { - m_selectAnchor = view()->selectionRange().start(); - } - } + m_selectionMode = Line; + if (e->modifiers() & Qt::ShiftModifier) { + updateSelection(m_cursor, true); + } else { + view()->selectLine(m_cursor); if (view()->selection()) { - QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); + m_selectAnchor = view()->selectionRange().start(); } + } - // Keep the line at the select anchor selected during further - // mouse selection - if (m_selectAnchor.line() > view()->selectionRange().start().line()) { - // Preserve the last selected line - if (m_selectAnchor == view()->selectionRange().end() && m_selectAnchor.column() == 0) { - m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line() - 1, 0)); - } else { - m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), 0)); - } - m_selectionCached.setEnd(view()->selectionRange().end()); + if (view()->selection()) { + QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); + } + + // Keep the line at the select anchor selected during further + // mouse selection + if (m_selectAnchor.line() > view()->selectionRange().start().line()) { + // Preserve the last selected line + if (m_selectAnchor == view()->selectionRange().end() && m_selectAnchor.column() == 0) { + m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line() - 1, 0)); } else { - // Preserve the first selected line - m_selectionCached.setStart(view()->selectionRange().start()); - if (view()->selectionRange().end().line() > view()->selectionRange().start().line()) { - m_selectionCached.setEnd(KTextEditor::Cursor(view()->selectionRange().start().line() + 1, 0)); - } else { - m_selectionCached.setEnd(view()->selectionRange().end()); - } + m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), 0)); + } + m_selectionCached.setEnd(view()->selectionRange().end()); + } else { + // Preserve the first selected line + m_selectionCached.setStart(view()->selectionRange().start()); + if (view()->selectionRange().end().line() > view()->selectionRange().start().line()) { + m_selectionCached.setEnd(KTextEditor::Cursor(view()->selectionRange().start().line() + 1, 0)); + } else { + m_selectionCached.setEnd(view()->selectionRange().end()); } + } + + moveCursorToSelectionEdge(); - moveCursorToSelectionEdge(); + m_scrollX = 0; + m_scrollY = 0; + m_scrollTimer.start(50); - m_scrollX = 0; - m_scrollY = 0; - m_scrollTimer.start(50); + e->accept(); + return; + } else if (m_selectionMode == Default) { + m_selectionMode = Mouse; + } - e->accept(); - return; - } else if (m_selectionMode == Default) { - m_selectionMode = Mouse; + // request the software keyboard, if any + if (e->button() == Qt::LeftButton && qApp->autoSipEnabled()) { + QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel(style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); + if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { + QEvent event(QEvent::RequestSoftwareInputPanel); + QApplication::sendEvent(this, &event); } + } - // request the software keyboard, if any - if (e->button() == Qt::LeftButton && qApp->autoSipEnabled()) { - QStyle::RequestSoftwareInputPanel behavior = QStyle::RequestSoftwareInputPanel(style()->styleHint(QStyle::SH_RequestSoftwareInputPanel)); - if (hasFocus() || behavior == QStyle::RSIP_OnMouseClick) { - QEvent event(QEvent::RequestSoftwareInputPanel); - QApplication::sendEvent(this, &event); - } + if (e->modifiers() & Qt::ShiftModifier) { + if (!m_selectAnchor.isValid()) { + m_selectAnchor = m_cursor; } + } else { + m_selectionCached = KTextEditor::Range::invalid(); + } + + if (view()->config()->textDragAndDrop() && !(e->modifiers() & Qt::ShiftModifier) && isTargetSelected(e->pos())) { + m_dragInfo.state = diPending; + m_dragInfo.start = e->pos(); + } else { + m_dragInfo.state = diNone; if (e->modifiers() & Qt::ShiftModifier) { - if (!m_selectAnchor.isValid()) { - m_selectAnchor = m_cursor; + placeCursor(e->pos(), true, false); + if (m_selectionCached.start().isValid()) { + if (m_cursor.toCursor() < m_selectionCached.start()) { + m_selectAnchor = m_selectionCached.end(); + } else { + m_selectAnchor = m_selectionCached.start(); + } } + setSelection(KTextEditor::Range(m_selectAnchor, m_cursor)); } else { - m_selectionCached = KTextEditor::Range::invalid(); + placeCursor(e->pos()); } - if (view()->config()->textDragAndDrop() && !(e->modifiers() & Qt::ShiftModifier) && isTargetSelected(e->pos())) { - m_dragInfo.state = diPending; - m_dragInfo.start = e->pos(); - } else { - m_dragInfo.state = diNone; - - if (e->modifiers() & Qt::ShiftModifier) { - placeCursor(e->pos(), true, false); - if (m_selectionCached.start().isValid()) { - if (m_cursor.toCursor() < m_selectionCached.start()) { - m_selectAnchor = m_selectionCached.end(); - } else { - m_selectAnchor = m_selectionCached.start(); - } - } - setSelection(KTextEditor::Range(m_selectAnchor, m_cursor)); - } else { - placeCursor(e->pos()); - } + m_scrollX = 0; + m_scrollY = 0; - m_scrollX = 0; - m_scrollY = 0; + m_scrollTimer.start(50); + } - m_scrollTimer.start(50); - } + e->accept(); + break; + case Qt::RightButton: + if (e->pos().x() == 0) { + // Special handling for folding by right click + placeCursor(e->pos()); e->accept(); - break; - - case Qt::RightButton: - if (e->pos().x() == 0) { - // Special handling for folding by right click - placeCursor(e->pos()); - e->accept(); - } - break; + } + break; - default: - e->ignore(); - break; + default: + e->ignore(); + break; } } void KateViewInternal::mouseDoubleClickEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { m_selectionMode = Word; if (e->modifiers() & Qt::ShiftModifier) { // Now select the word under the select anchor int cs, ce; Kate::TextLine l = doc()->kateTextLine(m_selectAnchor.line()); ce = m_selectAnchor.column(); if (ce > 0 && doc()->highlight()->isInWord(l->at(ce))) { for (; ce < l->length(); ce++) if (!doc()->highlight()->isInWord(l->at(ce))) { break; } } cs = m_selectAnchor.column() - 1; if (cs < doc()->lineLength(m_selectAnchor.line()) && doc()->highlight()->isInWord(l->at(cs))) { for (cs--; cs >= 0; cs--) if (!doc()->highlight()->isInWord(l->at(cs))) { break; } } // ...and keep it selected if (cs + 1 < ce) { m_selectionCached.setStart(KTextEditor::Cursor(m_selectAnchor.line(), cs + 1)); m_selectionCached.setEnd(KTextEditor::Cursor(m_selectAnchor.line(), ce)); } else { m_selectionCached.setStart(m_selectAnchor); m_selectionCached.setEnd(m_selectAnchor); } // Now word select to the mouse cursor placeCursor(e->pos(), true); } else { // first clear the selection, otherwise we run into bug #106402 // ...and set the cursor position, for the same reason (otherwise there // are *other* idiosyncrasies we can't fix without reintroducing said // bug) // Parameters: don't redraw, and don't emit selectionChanged signal yet view()->clearSelection(false, false); placeCursor(e->pos()); view()->selectWord(m_cursor); cursorToMatchingBracket(true); if (view()->selection()) { m_selectAnchor = view()->selectionRange().start(); m_selectionCached = view()->selectionRange(); } else { m_selectAnchor = m_cursor; m_selectionCached = KTextEditor::Range(m_cursor, m_cursor); } } // Move cursor to end (or beginning) of selected word #ifndef Q_OS_MACOS if (view()->selection()) { QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } #endif moveCursorToSelectionEdge(); m_possibleTripleClick = true; QTimer::singleShot(QApplication::doubleClickInterval(), this, SLOT(tripleClickTimeout())); m_scrollX = 0; m_scrollY = 0; m_scrollTimer.start(50); e->accept(); } else { e->ignore(); } } void KateViewInternal::tripleClickTimeout() { m_possibleTripleClick = false; } void KateViewInternal::beginSelectLine(const QPoint &pos) { placeCursor(pos); m_possibleTripleClick = true; // set so subsequent mousePressEvent will select line } void KateViewInternal::mouseReleaseEvent(QMouseEvent *e) { switch (e->button()) { - case Qt::LeftButton: - m_selectionMode = Default; - // m_selectionCached.start().setLine( -1 ); - - if (m_selChangedByUser) { - if (view()->selection()) { - QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); - } - moveCursorToSelectionEdge(); + case Qt::LeftButton: + m_selectionMode = Default; + // m_selectionCached.start().setLine( -1 ); - m_selChangedByUser = false; + if (m_selChangedByUser) { + if (view()->selection()) { + QApplication::clipboard()->setText(view()->selectionText(), QClipboard::Selection); } + moveCursorToSelectionEdge(); - if (m_dragInfo.state == diPending) { - placeCursor(e->pos(), e->modifiers() & Qt::ShiftModifier); - } else if (m_dragInfo.state == diNone) { - m_scrollTimer.stop(); - } + m_selChangedByUser = false; + } - m_dragInfo.state = diNone; + if (m_dragInfo.state == diPending) { + placeCursor(e->pos(), e->modifiers() & Qt::ShiftModifier); + } else if (m_dragInfo.state == diNone) { + m_scrollTimer.stop(); + } - e->accept(); - break; + m_dragInfo.state = diNone; - case Qt::MidButton: - if (!view()->config()->mousePasteAtCursorPosition()) { - placeCursor(e->pos()); - } + e->accept(); + break; - if (doc()->isReadWrite()) { - QString clipboard = QApplication::clipboard()->text(QClipboard::Selection); - view()->paste(&clipboard); - } + case Qt::MidButton: + if (!view()->config()->mousePasteAtCursorPosition()) { + placeCursor(e->pos()); + } - e->accept(); - break; + if (doc()->isReadWrite()) { + QString clipboard = QApplication::clipboard()->text(QClipboard::Selection); + view()->paste(&clipboard); + } - default: - e->ignore(); - break; + e->accept(); + break; + + default: + e->ignore(); + break; } } void KateViewInternal::leaveEvent(QEvent *) { m_textHintTimer.stop(); // fix bug 194452, scrolling keeps going if you scroll via mouse drag and press and other mouse // button outside the view area if (m_dragInfo.state == diNone) { m_scrollTimer.stop(); } } KTextEditor::Cursor KateViewInternal::coordinatesToCursor(const QPoint &_coord, bool includeBorder) const { QPoint coord(_coord); KTextEditor::Cursor ret = KTextEditor::Cursor::invalid(); if (includeBorder) { coord.rx() -= m_leftBorder->width(); } coord.rx() += startX(); const KateTextLayout &thisLine = yToKateTextLayout(coord.y()); if (thisLine.isValid()) { ret = renderer()->xToCursor(thisLine, coord.x(), !view()->wrapCursor()); } if (ret.column() > view()->document()->lineLength(ret.line())) { // The cursor is beyond the end of the line; in that case the renderer // gives the index of the character behind the last one. return KTextEditor::Cursor::invalid(); } return ret; } void KateViewInternal::mouseMoveEvent(QMouseEvent *e) { KTextEditor::Cursor newPosition = coordinatesToCursor(e->pos(), false); if (newPosition != m_mouse) { m_mouse = newPosition; mouseMoved(); } if (e->buttons() == Qt::NoButton) { const auto noteData = inlineNoteAt(e->globalPos()); const KTextEditor::InlineNote note(noteData); const KTextEditor::InlineNote activeNote(m_activeInlineNote); if (note.position().isValid()) { if (!activeNote.position().isValid()) { // no active note -- focus in note.provider()->inlineNoteFocusInEvent(note, e->globalPos()); m_activeInlineNote = noteData; } else { note.provider()->inlineNoteMouseMoveEvent(note, e->globalPos()); } // the note might change its appearance in result to the event tagLines(note.position(), note.position(), true); } else if (activeNote.position().isValid()) { activeNote.provider()->inlineNoteFocusOutEvent(activeNote); tagLines(activeNote.position(), activeNote.position(), true); m_activeInlineNote = {}; } } if (e->buttons() & Qt::LeftButton) { if (m_dragInfo.state == diPending) { // we had a mouse down, but haven't confirmed a drag yet // if the mouse has moved sufficiently, we will confirm QPoint p(e->pos() - m_dragInfo.start); // we've left the drag square, we can start a real drag operation now if (p.manhattanLength() > QApplication::startDragDistance()) { doDrag(); } return; } else if (m_dragInfo.state == diDragging) { // Don't do anything after a canceled drag until the user lets go of // the mouse button! return; } m_mouseX = e->x(); m_mouseY = e->y(); m_scrollX = 0; m_scrollY = 0; int d = renderer()->lineHeight(); if (m_mouseX < 0) { m_scrollX = -d; } if (m_mouseX > width()) { m_scrollX = d; } if (m_mouseY < 0) { m_mouseY = 0; m_scrollY = -d; } if (m_mouseY > height()) { m_mouseY = height(); m_scrollY = d; } if (!m_scrollY) { placeCursor(QPoint(m_mouseX, m_mouseY), true); } } else { if (view()->config()->textDragAndDrop() && isTargetSelected(e->pos())) { // mouse is over selected text. indicate that the text is draggable by setting // the arrow cursor as other Qt text editing widgets do if (m_mouseCursor != Qt::ArrowCursor) { m_mouseCursor = Qt::ArrowCursor; setCursor(m_mouseCursor); } } else { // normal text cursor if (m_mouseCursor != Qt::IBeamCursor) { m_mouseCursor = Qt::IBeamCursor; setCursor(m_mouseCursor); } } // We need to check whether the mouse position is actually within the widget, // because other widgets like the icon border forward their events to this, // and we will create invalid text hint requests if we don't check if (textHintsEnabled() && geometry().contains(parentWidget()->mapFromGlobal(e->globalPos()))) { if (QToolTip::isVisible()) { QToolTip::hideText(); } m_textHintTimer.start(m_textHintDelay); m_textHintPos = e->pos(); } } } void KateViewInternal::updateDirty() { const int h = renderer()->lineHeight(); int currentRectStart = -1; int currentRectEnd = -1; QRegion updateRegion; { for (int i = 0; i < cache()->viewCacheLineCount(); ++i) { if (cache()->viewLine(i).isDirty()) { if (currentRectStart == -1) { currentRectStart = h * i; currentRectEnd = h; } else { currentRectEnd += h; } } else if (currentRectStart != -1) { updateRegion += QRect(0, currentRectStart, width(), currentRectEnd); currentRectStart = -1; currentRectEnd = -1; } } } if (currentRectStart != -1) { updateRegion += QRect(0, currentRectStart, width(), currentRectEnd); } if (!updateRegion.isEmpty()) { if (debugPainting) { qCDebug(LOG_KTE) << "Update dirty region " << updateRegion; } update(updateRegion); } } void KateViewInternal::hideEvent(QHideEvent *e) { Q_UNUSED(e); if (view()->isCompletionActive()) { view()->completionWidget()->abortCompletion(); } } void KateViewInternal::paintEvent(QPaintEvent *e) { if (debugPainting) { qCDebug(LOG_KTE) << "GOT PAINT EVENT: Region" << e->region(); } const QRect &unionRect = e->rect(); int xStart = startX() + unionRect.x(); int xEnd = xStart + unionRect.width(); uint h = renderer()->lineHeight(); uint startz = (unionRect.y() / h); uint endz = startz + 1 + (unionRect.height() / h); uint lineRangesSize = cache()->viewCacheLineCount(); const KTextEditor::Cursor pos = m_cursor; QPainter paint(this); // THIS IS ULTRA EVIL AND ADDS STRANGE RENDERING ARTIFACTS WITH SCALING!!!! // SEE BUG https://bugreports.qt.io/browse/QTBUG-66036 // paint.setRenderHints(QPainter::TextAntialiasing); paint.save(); renderer()->setCaretStyle(m_currentInputMode->caretStyle()); renderer()->setShowTabs(doc()->config()->showTabs()); renderer()->setShowSpaces(doc()->config()->showSpaces()); renderer()->updateMarkerSize(); /** * paint line by line * this includes parts that span areas without real lines * translate to first line to paint */ paint.translate(unionRect.x(), startz * h); for (uint z = startz; z <= endz; z++) { /** * paint regions without lines mapped to */ if ((z >= lineRangesSize) || (cache()->viewLine(z).line() == -1)) { if (!(z >= lineRangesSize)) { cache()->viewLine(z).setDirty(false); } paint.fillRect(0, 0, unionRect.width(), h, renderer()->config()->backgroundColor()); } /** * paint text lines */ else { /** * If viewLine() returns non-zero, then a document line was split * in several visual lines, and we're trying to paint visual line * that is not the first. In that case, this line was already * painted previously, since KateRenderer::paintTextLine paints * all visual lines. * * Except if we're at the start of the region that needs to * be painted -- when no previous calls to paintTextLine were made. */ KateTextLayout &thisLine = cache()->viewLine(z); if (!thisLine.viewLine() || z == startz) { /** * paint our line * set clipping region to only paint the relevant parts */ paint.save(); paint.translate(QPoint(0, h * -thisLine.viewLine())); // compute rect for line, fill the stuff const QRectF lineRect(0, 0, unionRect.width(), h * thisLine.kateLineLayout()->viewLineCount()); paint.fillRect(lineRect, renderer()->config()->backgroundColor()); // THIS IS ULTRA EVIL AND ADDS STRANGE RENDERING ARTIFACTS WITH SCALING!!!! // SEE BUG https://bugreports.qt.io/browse/QTBUG-66036 // => using a QRectF solves the cut of 1 pixel, the same call with QRect does create artifacts! paint.setClipRect(lineRect); renderer()->paintTextLine(paint, thisLine.kateLineLayout(), xStart, xEnd, &pos); paint.restore(); /** * line painted, reset and state + mark line as non-dirty */ thisLine.setDirty(false); } } /** * translate to next line */ paint.translate(0, h); } paint.restore(); if (m_textAnimation) { m_textAnimation->draw(paint); } } void KateViewInternal::resizeEvent(QResizeEvent *e) { bool expandedHorizontally = width() > e->oldSize().width(); bool expandedVertically = height() > e->oldSize().height(); bool heightChanged = height() != e->oldSize().height(); m_dummy->setFixedSize(m_lineScroll->width(), m_columnScroll->sizeHint().height()); m_madeVisible = false; if (heightChanged) { setAutoCenterLines(m_autoCenterLines, false); m_cachedMaxStartPos.setPosition(-1, -1); } if (view()->dynWordWrap()) { bool dirtied = false; for (int i = 0; i < cache()->viewCacheLineCount(); i++) { // find the first dirty line // the word wrap updateView algorithm is forced to check all lines after a dirty one KateTextLayout viewLine = cache()->viewLine(i); if (viewLine.wrap() || viewLine.isRightToLeft() || viewLine.width() > width()) { dirtied = true; viewLine.setDirty(); break; } } if (dirtied || heightChanged) { updateView(true); m_leftBorder->update(); } } else { updateView(); if (expandedHorizontally && startX() > 0) { scrollColumns(startX() - (width() - e->oldSize().width())); } } if (width() < e->oldSize().width() && !view()->wrapCursor()) { // May have to restrain cursor to new smaller width... if (m_cursor.column() > doc()->lineLength(m_cursor.line())) { KateTextLayout thisLine = currentLayout(); KTextEditor::Cursor newCursor(m_cursor.line(), thisLine.endCol() + ((width() - thisLine.xOffset() - (thisLine.width() - startX())) / renderer()->spaceWidth()) - 1); if (newCursor.column() < m_cursor.column()) { updateCursor(newCursor); } } } if (expandedVertically) { KTextEditor::Cursor max = maxStartPos(); if (startPos() > max) { scrollPos(max); return; // already fired displayRangeChanged } } emit view()->displayRangeChanged(m_view); } void KateViewInternal::scrollTimeout() { if (m_scrollX || m_scrollY) { const int scrollTo = startPos().line() + (m_scrollY / (int)renderer()->lineHeight()); placeCursor(QPoint(m_mouseX, m_mouseY), true); scrollLines(scrollTo); } } void KateViewInternal::cursorTimeout() { if (!debugPainting && m_currentInputMode->blinkCaret()) { renderer()->setDrawCaret(!renderer()->drawCaret()); paintCursor(); } } void KateViewInternal::textHintTimeout() { m_textHintTimer.stop(); KTextEditor::Cursor c = coordinatesToCursor(m_textHintPos, false); if (!c.isValid()) { return; } QStringList textHints; for (KTextEditor::TextHintProvider *const p : qAsConst(m_textHintProviders)) { const QString hint = p->textHint(m_view, c); if (!hint.isEmpty()) { textHints.append(hint); } } if (!textHints.isEmpty()) { qCDebug(LOG_KTE) << "Hint text: " << textHints; QString hint; for (const QString &str : qAsConst(textHints)) { hint += QStringLiteral("

%1

").arg(str); } QPoint pos(startX() + m_textHintPos.x(), m_textHintPos.y()); QToolTip::showText(mapToGlobal(pos), hint); } } void KateViewInternal::focusInEvent(QFocusEvent *) { if (QApplication::cursorFlashTime() > 0) { m_cursorTimer.start(QApplication::cursorFlashTime() / 2); } paintCursor(); doc()->setActiveView(m_view); // this will handle focus stuff in kateview view()->slotGotFocus(); } void KateViewInternal::focusOutEvent(QFocusEvent *) { // if (view()->isCompletionActive()) // view()->abortCompletion(); m_cursorTimer.stop(); view()->renderer()->setDrawCaret(true); paintCursor(); m_textHintTimer.stop(); view()->slotLostFocus(); } void KateViewInternal::doDrag() { m_dragInfo.state = diDragging; m_dragInfo.dragObject = new QDrag(this); QMimeData *mimeData = new QMimeData(); mimeData->setText(view()->selectionText()); m_dragInfo.dragObject->setMimeData(mimeData); m_dragInfo.dragObject->exec(Qt::MoveAction); } void KateViewInternal::dragEnterEvent(QDragEnterEvent *event) { if (event->source() == this) { event->setDropAction(Qt::MoveAction); } event->setAccepted((event->mimeData()->hasText() && doc()->isReadWrite()) || event->mimeData()->hasUrls()); } void KateViewInternal::fixDropEvent(QDropEvent *event) { if (event->source() != this) { event->setDropAction(Qt::CopyAction); } else { Qt::DropAction action = Qt::MoveAction; #ifdef Q_WS_MAC if (event->keyboardModifiers() & Qt::AltModifier) { action = Qt::CopyAction; } #else if (event->keyboardModifiers() & Qt::ControlModifier) { action = Qt::CopyAction; } #endif event->setDropAction(action); } } void KateViewInternal::dragMoveEvent(QDragMoveEvent *event) { // track the cursor to the current drop location placeCursor(event->pos(), true, false); // important: accept action to switch between copy and move mode // without this, the text will always be copied. fixDropEvent(event); } void KateViewInternal::dropEvent(QDropEvent *event) { /** * if we have urls, pass this event off to the hosting application */ if (event->mimeData()->hasUrls()) { emit dropEventPass(event); return; } if (event->mimeData()->hasText() && doc()->isReadWrite()) { const QString text = event->mimeData()->text(); const bool blockMode = view()->blockSelection(); fixDropEvent(event); // Remember where to paste/move... KTextEditor::Cursor targetCursor(m_cursor); // Use powerful MovingCursor to track our changes we may do std::unique_ptr targetCursor2(doc()->newMovingCursor(m_cursor)); // As always need the BlockMode some special treatment const KTextEditor::Range selRange(view()->selectionRange()); const KTextEditor::Cursor blockAdjust(selRange.numberOfLines(), selRange.columnWidth()); // Restore the cursor position before editStart(), so that it is correctly stored for the undo action if (event->dropAction() != Qt::CopyAction) { editSetCursor(selRange.end()); } else { view()->clearSelection(); } // use one transaction doc()->editStart(); if (event->dropAction() != Qt::CopyAction) { view()->removeSelectedText(); if (targetCursor2->toCursor() != targetCursor) { // Hm, multi line selection moved down, we need to adjust our dumb cursor targetCursor = targetCursor2->toCursor(); } doc()->insertText(targetCursor2->toCursor(), text, blockMode); } else { doc()->insertText(targetCursor, text, blockMode); } if (blockMode) { setSelection(KTextEditor::Range(targetCursor, targetCursor + blockAdjust)); editSetCursor(targetCursor + blockAdjust); } else { setSelection(KTextEditor::Range(targetCursor, targetCursor2->toCursor())); editSetCursor(targetCursor2->toCursor()); // Just to satisfy autotest } doc()->editEnd(); event->acceptProposedAction(); updateView(); } // finally finish drag and drop mode m_dragInfo.state = diNone; // important, because the eventFilter`s DragLeave does not occur stopDragScroll(); } // END EVENT HANDLING STUFF void KateViewInternal::clear() { m_startPos.setPosition(0, 0); m_displayCursor = KTextEditor::Cursor(0, 0); m_cursor.setPosition(0, 0); cache()->clear(); updateView(true); m_lineScroll->updatePixmap(); } void KateViewInternal::wheelEvent(QWheelEvent *e) { // check if this event should change the font size (Ctrl pressed, angle reported and not accidentally so) // Note: if detectZoomingEvent() doesn't unset the ControlModifier we'll get accelerated scrolling. if (m_zoomEventFilter->detectZoomingEvent(e)) { if (e->angleDelta().y() > 0) { slotIncFontSizes(qreal(e->angleDelta().y()) / QWheelEvent::DefaultDeltasPerStep); } else if (e->angleDelta().y() < 0) { slotDecFontSizes(qreal(-e->angleDelta().y()) / QWheelEvent::DefaultDeltasPerStep); } // accept always and be done for zooming e->accept(); return; } // handle vertical scrolling via the scrollbar if (e->angleDelta().y() != 0) { // compute distance auto sign = m_lineScroll->invertedControls() ? -1 : 1; auto offset = sign * qreal(e->angleDelta().y()) / 120.0; if (e->modifiers() & Qt::ShiftModifier) { const auto pageStep = m_lineScroll->pageStep(); offset = qBound(-pageStep, int(offset * pageStep), pageStep); } else { offset *= QApplication::wheelScrollLines(); } // handle accumulation m_accumulatedScroll += offset - int(offset); const auto extraAccumulated = int(m_accumulatedScroll); m_accumulatedScroll -= extraAccumulated; // do scroll scrollViewLines(int(offset) + extraAccumulated); e->accept(); } // handle horizontal scrolling via the scrollbar if (e->angleDelta().x() != 0) { // if we have dyn word wrap, we should ignore the scroll events if (view()->dynWordWrap()) { e->accept(); return; } QWheelEvent copy = *e; QApplication::sendEvent(m_columnScroll, ©); if (copy.isAccepted()) { e->accept(); } } } void KateViewInternal::startDragScroll() { if (!m_dragScrollTimer.isActive()) { m_dragScrollTimer.start(s_scrollTime); } } void KateViewInternal::stopDragScroll() { m_dragScrollTimer.stop(); updateView(); } void KateViewInternal::doDragScroll() { QPoint p = this->mapFromGlobal(QCursor::pos()); int dx = 0, dy = 0; if (p.y() < s_scrollMargin) { dy = p.y() - s_scrollMargin; } else if (p.y() > height() - s_scrollMargin) { dy = s_scrollMargin - (height() - p.y()); } if (p.x() < s_scrollMargin) { dx = p.x() - s_scrollMargin; } else if (p.x() > width() - s_scrollMargin) { dx = s_scrollMargin - (width() - p.x()); } dy /= 4; if (dy) { scrollLines(startLine() + dy); } if (columnScrollingPossible() && dx) { scrollColumns(qMin(startX() + dx, m_columnScroll->maximum())); } if (!dy && !dx) { stopDragScroll(); } } void KateViewInternal::registerTextHintProvider(KTextEditor::TextHintProvider *provider) { if (!m_textHintProviders.contains(provider)) { m_textHintProviders.append(provider); } // we have a client, so start timeout m_textHintTimer.start(m_textHintDelay); } void KateViewInternal::unregisterTextHintProvider(KTextEditor::TextHintProvider *provider) { const int index = m_textHintProviders.indexOf(provider); if (index >= 0) { m_textHintProviders.removeAt(index); } if (m_textHintProviders.isEmpty()) { m_textHintTimer.stop(); } } void KateViewInternal::setTextHintDelay(int delay) { if (delay <= 0) { m_textHintDelay = 200; // ms } else { m_textHintDelay = delay; // ms } } int KateViewInternal::textHintDelay() const { return m_textHintDelay; } bool KateViewInternal::textHintsEnabled() { return !m_textHintProviders.isEmpty(); } // BEGIN EDIT STUFF void KateViewInternal::editStart() { editSessionNumber++; if (editSessionNumber > 1) { return; } editIsRunning = true; editOldCursor = m_cursor; editOldSelection = view()->selectionRange(); } void KateViewInternal::editEnd(int editTagLineStart, int editTagLineEnd, bool tagFrom) { if (editSessionNumber == 0) { return; } editSessionNumber--; if (editSessionNumber > 0) { return; } // fix start position, might have moved from column 0 // try to clever calculate the right start column for the tricky dyn word wrap case int col = 0; if (view()->dynWordWrap()) { if (KateLineLayoutPtr layout = cache()->line(startLine())) { int index = layout->viewLineForColumn(startPos().column()); if (index >= 0 && index < layout->viewLineCount()) { col = layout->viewLine(index).startCol(); } } } m_startPos.setPosition(startLine(), col); if (tagFrom && (editTagLineStart <= int(view()->textFolding().visibleLineToLine(startLine())))) { tagAll(); } else { tagLines(editTagLineStart, tagFrom ? qMax(doc()->lastLine() + 1, editTagLineEnd) : editTagLineEnd, true); } if (editOldCursor == m_cursor.toCursor()) { updateBracketMarks(); } updateView(true); if (editOldCursor != m_cursor.toCursor() || m_view == doc()->activeView()) { // Only scroll the view to the cursor if the insertion happens at the cursor. // This might not be the case for e.g. collaborative editing, when a remote user // inserts text at a position not at the caret. if (m_cursor.line() >= editTagLineStart && m_cursor.line() <= editTagLineEnd) { m_madeVisible = false; updateCursor(m_cursor, true); } } /** * selection changed? * fixes bug 316226 */ if (editOldSelection != view()->selectionRange() || (editOldSelection.isValid() && !editOldSelection.isEmpty() && !(editTagLineStart > editOldSelection.end().line() && editTagLineEnd < editOldSelection.start().line()))) { emit view()->selectionChanged(m_view); } editIsRunning = false; } void KateViewInternal::editSetCursor(const KTextEditor::Cursor &_cursor) { if (m_cursor.toCursor() != _cursor) { m_cursor.setPosition(_cursor); } } // END void KateViewInternal::viewSelectionChanged() { if (!view()->selection()) { m_selectAnchor = KTextEditor::Cursor::invalid(); } else { m_selectAnchor = view()->selectionRange().start(); } // Do NOT nuke the entire range! The reason is that a shift+DC selection // might (correctly) set the range to be empty (i.e. start() == end()), and // subsequent dragging might shrink the selection into non-existence. When // this happens, we use the cached end to restore the cached start so that // updateSelection is not confused. See also comments in updateSelection. m_selectionCached.setStart(KTextEditor::Cursor::invalid()); } KateLayoutCache *KateViewInternal::cache() const { return m_layoutCache; } KTextEditor::Cursor KateViewInternal::toRealCursor(const KTextEditor::Cursor &virtualCursor) const { return KTextEditor::Cursor(view()->textFolding().visibleLineToLine(virtualCursor.line()), virtualCursor.column()); } KTextEditor::Cursor KateViewInternal::toVirtualCursor(const KTextEditor::Cursor &realCursor) const { /** * only convert valid lines, folding doesn't like invalid input! * don't validate whole cursor, column might be -1 */ if (realCursor.line() < 0) { return KTextEditor::Cursor::invalid(); } return KTextEditor::Cursor(view()->textFolding().lineToVisibleLine(realCursor.line()), realCursor.column()); } KateRenderer *KateViewInternal::renderer() const { return view()->renderer(); } void KateViewInternal::mouseMoved() { view()->notifyMousePositionChanged(m_mouse); view()->updateRangesIn(KTextEditor::Attribute::ActivateMouseIn); } void KateViewInternal::cursorMoved() { view()->updateRangesIn(KTextEditor::Attribute::ActivateCaretIn); #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive()) { QAccessibleTextCursorEvent ev(this, static_cast(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, m_cursor)); QAccessible::updateAccessibility(&ev); } #endif } bool KateViewInternal::rangeAffectsView(const KTextEditor::Range &range, bool realCursors) const { int startLine = KateViewInternal::startLine(); int endLine = startLine + (int)m_visibleLineCount; if (realCursors) { startLine = (int)view()->textFolding().visibleLineToLine(startLine); endLine = (int)view()->textFolding().visibleLineToLine(endLine); } return (range.end().line() >= startLine) || (range.start().line() <= endLine); } // BEGIN IM INPUT STUFF QVariant KateViewInternal::inputMethodQuery(Qt::InputMethodQuery query) const { switch (query) { - case Qt::ImCursorRectangle: { - // Cursor placement code is changed for Asian input method that - // shows candidate window. This behavior is same as Qt/E 2.3.7 - // which supports Asian input methods. Asian input methods need - // start point of IM selection text to place candidate window as - // adjacent to the selection text. - // - // in Qt5, cursor rectangle is used as QRectF internally, and it - // will be checked by QRectF::isValid(), which will mark rectangle - // with width == 0 or height == 0 as invalid. - auto lineHeight = renderer()->lineHeight(); - return QRect(cursorToCoordinate(m_cursor, true, false), QSize(1, lineHeight ? lineHeight : 1)); - } - - case Qt::ImFont: - return renderer()->currentFont(); - - case Qt::ImCursorPosition: - return m_cursor.column(); + case Qt::ImCursorRectangle: { + // Cursor placement code is changed for Asian input method that + // shows candidate window. This behavior is same as Qt/E 2.3.7 + // which supports Asian input methods. Asian input methods need + // start point of IM selection text to place candidate window as + // adjacent to the selection text. + // + // in Qt5, cursor rectangle is used as QRectF internally, and it + // will be checked by QRectF::isValid(), which will mark rectangle + // with width == 0 or height == 0 as invalid. + auto lineHeight = renderer()->lineHeight(); + return QRect(cursorToCoordinate(m_cursor, true, false), QSize(1, lineHeight ? lineHeight : 1)); + } + + case Qt::ImFont: + return renderer()->currentFont(); + + case Qt::ImCursorPosition: + return m_cursor.column(); - case Qt::ImAnchorPosition: - // If selectAnchor is at the same line, return the real anchor position - // Otherwise return the same position of cursor - if (view()->selection() && m_selectAnchor.line() == m_cursor.line()) { - return m_selectAnchor.column(); - } else { - return m_cursor.column(); - } + case Qt::ImAnchorPosition: + // If selectAnchor is at the same line, return the real anchor position + // Otherwise return the same position of cursor + if (view()->selection() && m_selectAnchor.line() == m_cursor.line()) { + return m_selectAnchor.column(); + } else { + return m_cursor.column(); + } - case Qt::ImSurroundingText: - if (Kate::TextLine l = doc()->kateTextLine(m_cursor.line())) { - return l->string(); - } else { - return QString(); - } + case Qt::ImSurroundingText: + if (Kate::TextLine l = doc()->kateTextLine(m_cursor.line())) { + return l->string(); + } else { + return QString(); + } - case Qt::ImCurrentSelection: - if (view()->selection()) { - return view()->selectionText(); - } else { - return QString(); - } - default: - /* values: ImMaximumTextLength */ - break; + case Qt::ImCurrentSelection: + if (view()->selection()) { + return view()->selectionText(); + } else { + return QString(); + } + default: + /* values: ImMaximumTextLength */ + break; } return QWidget::inputMethodQuery(query); } void KateViewInternal::inputMethodEvent(QInputMethodEvent *e) { if (doc()->readOnly()) { e->ignore(); return; } // qCDebug(LOG_KTE) << "Event: cursor" << m_cursor << "commit" << e->commitString() << "preedit" << e->preeditString() << "replacement start" << e->replacementStart() << "length" << e->replacementLength(); if (!m_imPreeditRange) { m_imPreeditRange = doc()->newMovingRange(KTextEditor::Range(m_cursor, m_cursor), KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); } if (!m_imPreeditRange->toRange().isEmpty()) { doc()->inputMethodStart(); doc()->removeText(*m_imPreeditRange); doc()->inputMethodEnd(); } if (!e->commitString().isEmpty() || e->replacementLength()) { view()->removeSelectedText(); KTextEditor::Range preeditRange = *m_imPreeditRange; KTextEditor::Cursor start(m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + e->replacementStart()); KTextEditor::Cursor removeEnd = start + KTextEditor::Cursor(0, e->replacementLength()); doc()->editStart(); if (start != removeEnd) { doc()->removeText(KTextEditor::Range(start, removeEnd)); } // if the input method event is text that should be inserted, call KTextEditor::DocumentPrivate::typeChars() // with the text. that method will handle the input and take care of overwrite mode, etc. doc()->typeChars(m_view, e->commitString()); doc()->editEnd(); // Revert to the same range as above m_imPreeditRange->setRange(preeditRange); } if (!e->preeditString().isEmpty()) { doc()->inputMethodStart(); doc()->insertText(m_imPreeditRange->start(), e->preeditString()); doc()->inputMethodEnd(); // The preedit range gets automatically repositioned } // Finished this input method context? if (m_imPreeditRange && e->preeditString().isEmpty()) { // delete the range and reset the pointer delete m_imPreeditRange; m_imPreeditRange = nullptr; qDeleteAll(m_imPreeditRangeChildren); m_imPreeditRangeChildren.clear(); if (QApplication::cursorFlashTime() > 0) { renderer()->setDrawCaret(false); } renderer()->setCaretOverrideColor(QColor()); e->accept(); return; } KTextEditor::Cursor newCursor = m_cursor; bool hideCursor = false; QColor caretColor; if (m_imPreeditRange) { qDeleteAll(m_imPreeditRangeChildren); m_imPreeditRangeChildren.clear(); int decorationColumn = 0; const auto &attributes = e->attributes(); for (auto &a : attributes) { if (a.type == QInputMethodEvent::Cursor) { newCursor = m_imPreeditRange->start() + KTextEditor::Cursor(0, a.start); hideCursor = !a.length; QColor c = qvariant_cast(a.value); if (c.isValid()) { caretColor = c; } } else if (a.type == QInputMethodEvent::TextFormat) { QTextCharFormat f = qvariant_cast(a.value).toCharFormat(); if (f.isValid() && decorationColumn <= a.start) { KTextEditor::Range fr(m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + a.start, m_imPreeditRange->start().line(), m_imPreeditRange->start().column() + a.start + a.length); KTextEditor::MovingRange *formatRange = doc()->newMovingRange(fr); KTextEditor::Attribute::Ptr attribute(new KTextEditor::Attribute()); attribute->merge(f); formatRange->setAttribute(attribute); decorationColumn = a.start + a.length; m_imPreeditRangeChildren.push_back(formatRange); } } } } renderer()->setDrawCaret(hideCursor); renderer()->setCaretOverrideColor(caretColor); if (newCursor != m_cursor.toCursor()) { updateCursor(newCursor); } e->accept(); } // END IM INPUT STUFF void KateViewInternal::flashChar(const KTextEditor::Cursor &pos, KTextEditor::Attribute::Ptr attribute) { Q_ASSERT(pos.isValid()); Q_ASSERT(attribute.constData()); // if line is folded away, do nothing if (!view()->textFolding().isLineVisible(pos.line())) { return; } KTextEditor::Range range(pos, KTextEditor::Cursor(pos.line(), pos.column() + 1)); if (m_textAnimation) { m_textAnimation->deleteLater(); } m_textAnimation = new KateTextAnimation(range, std::move(attribute), this); } void KateViewInternal::documentTextInserted(KTextEditor::Document *document, const KTextEditor::Range &range) { #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive()) { QAccessibleTextInsertEvent ev(this, static_cast(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, range.start()), document->text(range)); QAccessible::updateAccessibility(&ev); } #endif } void KateViewInternal::documentTextRemoved(KTextEditor::Document * /*document*/, const KTextEditor::Range &range, const QString &oldText) { #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive()) { QAccessibleTextRemoveEvent ev(this, static_cast(QAccessible::queryAccessibleInterface(this))->positionFromCursor(this, range.start()), oldText); QAccessible::updateAccessibility(&ev); } #endif } QRect KateViewInternal::inlineNoteRect(const KateInlineNoteData ¬eData) const { KTextEditor::InlineNote note(noteData); // compute note width and position const auto noteWidth = note.width(); auto noteCursor = note.position(); // The cursor might be outside of the text. In that case, clamp it to the text and // later on add the missing x offset. const auto lineLength = view()->document()->lineLength(noteCursor.line()); int extraOffset = -noteWidth; if (noteCursor.column() == lineLength) { extraOffset = 0; } else if (noteCursor.column() > lineLength) { extraOffset = (noteCursor.column() - lineLength) * renderer()->spaceWidth(); noteCursor.setColumn(lineLength); } auto noteStartPos = mapToGlobal(cursorToCoordinate(noteCursor, true, false)); // compute the note's rect auto globalNoteRect = QRect(noteStartPos + QPoint {extraOffset, 0}, QSize(noteWidth, renderer()->lineHeight())); return globalNoteRect; } KateInlineNoteData KateViewInternal::inlineNoteAt(const QPoint &globalPos) const { // compute the associated cursor to get the right line const int line = coordinatesToCursor(mapFromGlobal(globalPos)).line(); const auto inlineNotes = view()->inlineNotes(line); // loop over all notes and check if the point is inside it for (const auto ¬e : inlineNotes) { auto globalNoteRect = inlineNoteRect(note); if (globalNoteRect.contains(globalPos)) { return note; } } // none found -- return an invalid note return {}; } diff --git a/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp b/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp index 9beae351..5a92d16e 100644 --- a/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp +++ b/src/vimode/emulatedcommandbar/emulatedcommandbar.cpp @@ -1,436 +1,436 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2013-2016 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include "../commandrangeexpressionparser.h" #include "../globalstate.h" #include "commandmode.h" #include "interactivesedreplacemode.h" #include "katedocument.h" #include "kateglobal.h" #include "kateview.h" #include "matchhighlighter.h" #include "searchmode.h" #include #include #include #include #include "../history.h" #include "../registers.h" #include "../searcher.h" #include #include #include #include #include using namespace KateVi; namespace { /** * @return \a originalRegex but escaped in such a way that a Qt regex search for * the resulting string will match the string \a originalRegex. */ QString escapedForSearchingAsLiteral(const QString &originalQtRegex) { QString escapedForSearchingAsLiteral = originalQtRegex; escapedForSearchingAsLiteral.replace(QLatin1Char('\\'), QLatin1String("\\\\")); escapedForSearchingAsLiteral.replace(QLatin1Char('$'), QLatin1String("\\$")); escapedForSearchingAsLiteral.replace(QLatin1Char('^'), QLatin1String("\\^")); escapedForSearchingAsLiteral.replace(QLatin1Char('.'), QLatin1String("\\.")); escapedForSearchingAsLiteral.replace(QLatin1Char('*'), QLatin1String("\\*")); escapedForSearchingAsLiteral.replace(QLatin1Char('/'), QLatin1String("\\/")); escapedForSearchingAsLiteral.replace(QLatin1Char('['), QLatin1String("\\[")); escapedForSearchingAsLiteral.replace(QLatin1Char(']'), QLatin1String("\\]")); escapedForSearchingAsLiteral.replace(QLatin1Char('\n'), QLatin1String("\\n")); return escapedForSearchingAsLiteral; } } EmulatedCommandBar::EmulatedCommandBar(KateViInputMode *viInputMode, InputModeManager *viInputModeManager, QWidget *parent) : KateViewBarWidget(false, parent) , m_viInputMode(viInputMode) , m_viInputModeManager(viInputModeManager) , m_view(viInputModeManager->view()) { QHBoxLayout *layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); centralWidget()->setLayout(layout); createAndAddBarTypeIndicator(layout); createAndAddEditWidget(layout); createAndAddExitStatusMessageDisplay(layout); createAndInitExitStatusMessageDisplayTimer(); createAndAddWaitingForRegisterIndicator(layout); m_matchHighligher.reset(new MatchHighlighter(m_view)); m_completer.reset(new Completer(this, m_view, m_edit)); m_interactiveSedReplaceMode.reset(new InteractiveSedReplaceMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view)); layout->addWidget(m_interactiveSedReplaceMode->label()); m_searchMode.reset(new SearchMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view, m_edit)); m_commandMode.reset(new CommandMode(this, m_matchHighligher.data(), m_viInputModeManager, m_view, m_edit, m_interactiveSedReplaceMode.data(), m_completer.data())); m_edit->installEventFilter(this); connect(m_edit, SIGNAL(textChanged(QString)), this, SLOT(editTextChanged(QString))); } EmulatedCommandBar::~EmulatedCommandBar() { } void EmulatedCommandBar::init(EmulatedCommandBar::Mode mode, const QString &initialText) { m_mode = mode; m_isActive = true; m_wasAborted = true; showBarTypeIndicator(mode); if (mode == KateVi::EmulatedCommandBar::SearchBackward || mode == SearchForward) { switchToMode(m_searchMode.data()); m_searchMode->init(mode == SearchBackward ? SearchMode::SearchDirection::Backward : SearchMode::SearchDirection::Forward); } else { switchToMode(m_commandMode.data()); } m_edit->setFocus(); m_edit->setText(initialText); m_edit->show(); m_exitStatusMessageDisplay->hide(); m_exitStatusMessageDisplayHideTimer->stop(); // A change in focus will have occurred: make sure we process it now, instead of having it // occur later and stop() m_commandResponseMessageDisplayHide. // This is generally only a problem when feeding a sequence of keys without human intervention, // as when we execute a mapping, macro, or test case. QApplication::processEvents(); } bool EmulatedCommandBar::isActive() { return m_isActive; } void EmulatedCommandBar::setCommandResponseMessageTimeout(long int commandResponseMessageTimeOutMS) { m_exitStatusMessageHideTimeOutMS = commandResponseMessageTimeOutMS; } void EmulatedCommandBar::closed() { m_matchHighligher->updateMatchHighlight(KTextEditor::Range::invalid()); m_completer->deactivateCompletion(); m_isActive = false; if (m_currentMode) { m_currentMode->deactivate(m_wasAborted); m_currentMode = nullptr; } } void EmulatedCommandBar::switchToMode(ActiveMode *newMode) { if (m_currentMode) m_currentMode->deactivate(false); m_currentMode = newMode; m_completer->setCurrentMode(newMode); } bool EmulatedCommandBar::barHandledKeypress(const QKeyEvent *keyEvent) { if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_H) || keyEvent->key() == Qt::Key_Backspace) { if (m_edit->text().isEmpty()) { emit hideMe(); } m_edit->backspace(); return true; } if (keyEvent->modifiers() != Qt::ControlModifier) return false; if (keyEvent->key() == Qt::Key_B) { m_edit->setCursorPosition(0); return true; } else if (keyEvent->key() == Qt::Key_E) { m_edit->setCursorPosition(m_edit->text().length()); return true; } else if (keyEvent->key() == Qt::Key_W) { deleteSpacesToLeftOfCursor(); if (!deleteNonWordCharsToLeftOfCursor()) { deleteWordCharsToLeftOfCursor(); } return true; } else if (keyEvent->key() == Qt::Key_R || keyEvent->key() == Qt::Key_G) { m_waitingForRegister = true; m_waitingForRegisterIndicator->setVisible(true); if (keyEvent->key() == Qt::Key_G) { m_insertedTextShouldBeEscapedForSearchingAsLiteral = true; } return true; } return false; } void EmulatedCommandBar::insertRegisterContents(const QKeyEvent *keyEvent) { if (keyEvent->key() != Qt::Key_Shift && keyEvent->key() != Qt::Key_Control) { const QChar key = KeyParser::self()->KeyEventToQChar(*keyEvent).toLower(); const int oldCursorPosition = m_edit->cursorPosition(); QString textToInsert; if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_W) { textToInsert = m_view->doc()->wordAt(m_view->cursorPosition()); } else { textToInsert = m_viInputModeManager->globalState()->registers()->getContent(key); } if (m_insertedTextShouldBeEscapedForSearchingAsLiteral) { textToInsert = escapedForSearchingAsLiteral(textToInsert); m_insertedTextShouldBeEscapedForSearchingAsLiteral = false; } m_edit->setText(m_edit->text().insert(m_edit->cursorPosition(), textToInsert)); m_edit->setCursorPosition(oldCursorPosition + textToInsert.length()); m_waitingForRegister = false; m_waitingForRegisterIndicator->setVisible(false); } } bool EmulatedCommandBar::eventFilter(QObject *object, QEvent *event) { // The "object" will be either m_edit or m_completer's popup. if (m_suspendEditEventFiltering) { return false; } Q_UNUSED(object); if (event->type() == QEvent::KeyPress) { // Re-route this keypress through Vim's central keypress handling area, so that we can use the keypress in e.g. // mappings and macros. return m_viInputMode->keyPress(static_cast(event)); } return false; } void EmulatedCommandBar::deleteSpacesToLeftOfCursor() { while (m_edit->cursorPosition() != 0 && m_edit->text().at(m_edit->cursorPosition() - 1) == QLatin1Char(' ')) { m_edit->backspace(); } } void EmulatedCommandBar::deleteWordCharsToLeftOfCursor() { while (m_edit->cursorPosition() != 0) { const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1); if (!charToTheLeftOfCursor.isLetterOrNumber() && charToTheLeftOfCursor != QLatin1Char('_')) { break; } m_edit->backspace(); } } bool EmulatedCommandBar::deleteNonWordCharsToLeftOfCursor() { bool deletionsMade = false; while (m_edit->cursorPosition() != 0) { const QChar charToTheLeftOfCursor = m_edit->text().at(m_edit->cursorPosition() - 1); if (charToTheLeftOfCursor.isLetterOrNumber() || charToTheLeftOfCursor == QLatin1Char('_') || charToTheLeftOfCursor == QLatin1Char(' ')) { break; } m_edit->backspace(); deletionsMade = true; } return deletionsMade; } bool EmulatedCommandBar::handleKeyPress(const QKeyEvent *keyEvent) { if (m_waitingForRegister) { insertRegisterContents(keyEvent); return true; } const bool completerHandled = m_completer->completerHandledKeypress(keyEvent); if (completerHandled) return true; if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) { emit hideMe(); return true; } // Is this a built-in Emulated Command Bar keypress e.g. insert from register, ctrl-h, etc? const bool barHandled = barHandledKeypress(keyEvent); if (barHandled) return true; // Can the current mode handle it? const bool currentModeHandled = m_currentMode->handleKeyPress(keyEvent); if (currentModeHandled) return true; // Couldn't handle this key event. // Send the keypress back to the QLineEdit. Ideally, instead of doing this, we would simply return "false" // and let Qt re-dispatch the event itself; however, there is a corner case in that if the selection // changes (as a result of e.g. incremental searches during Visual Mode), and the keypress that causes it // is not dispatched from within KateViInputModeHandler::handleKeypress(...) // (so KateViInputModeManager::isHandlingKeypress() returns false), we lose information about whether we are // in Visual Mode, Visual Line Mode, etc. See VisualViMode::updateSelection( ). if (m_edit->isVisible()) { if (m_suspendEditEventFiltering) return false; m_suspendEditEventFiltering = true; QKeyEvent keyEventCopy(keyEvent->type(), keyEvent->key(), keyEvent->modifiers(), keyEvent->text(), keyEvent->isAutoRepeat(), keyEvent->count()); qApp->notify(m_edit, &keyEventCopy); m_suspendEditEventFiltering = false; } return true; } bool EmulatedCommandBar::isSendingSyntheticSearchCompletedKeypress() { return m_searchMode->isSendingSyntheticSearchCompletedKeypress(); } void EmulatedCommandBar::startInteractiveSearchAndReplace(QSharedPointer interactiveSedReplace) { Q_ASSERT_X(interactiveSedReplace->currentMatch().isValid(), "startInteractiveSearchAndReplace", "KateCommands shouldn't initiate an interactive sed replace with no initial match"); switchToMode(m_interactiveSedReplaceMode.data()); m_interactiveSedReplaceMode->activate(interactiveSedReplace); } void EmulatedCommandBar::showBarTypeIndicator(EmulatedCommandBar::Mode mode) { QChar barTypeIndicator = QChar::Null; switch (mode) { - case SearchForward: - barTypeIndicator = QLatin1Char('/'); - break; - case SearchBackward: - barTypeIndicator = QLatin1Char('?'); - break; - case Command: - barTypeIndicator = QLatin1Char(':'); - break; - default: - Q_ASSERT(false && "Unknown mode!"); + case SearchForward: + barTypeIndicator = QLatin1Char('/'); + break; + case SearchBackward: + barTypeIndicator = QLatin1Char('?'); + break; + case Command: + barTypeIndicator = QLatin1Char(':'); + break; + default: + Q_ASSERT(false && "Unknown mode!"); } m_barTypeIndicator->setText(barTypeIndicator); m_barTypeIndicator->show(); } QString EmulatedCommandBar::executeCommand(const QString &commandToExecute) { return m_commandMode->executeCommand(commandToExecute); } void EmulatedCommandBar::closeWithStatusMessage(const QString &exitStatusMessage) { // Display the message for a while. Become inactive, so we don't steal keys in the meantime. m_isActive = false; m_exitStatusMessageDisplay->show(); m_exitStatusMessageDisplay->setText(exitStatusMessage); hideAllWidgetsExcept(m_exitStatusMessageDisplay); m_exitStatusMessageDisplayHideTimer->start(m_exitStatusMessageHideTimeOutMS); } void EmulatedCommandBar::editTextChanged(const QString &newText) { Q_ASSERT(!m_interactiveSedReplaceMode->isActive()); m_currentMode->editTextChanged(newText); m_completer->editTextChanged(newText); } void EmulatedCommandBar::startHideExitStatusMessageTimer() { if (m_exitStatusMessageDisplay->isVisible() && !m_exitStatusMessageDisplayHideTimer->isActive()) { m_exitStatusMessageDisplayHideTimer->start(m_exitStatusMessageHideTimeOutMS); } } void EmulatedCommandBar::setViInputModeManager(InputModeManager *viInputModeManager) { m_viInputModeManager = viInputModeManager; m_searchMode->setViInputModeManager(viInputModeManager); m_commandMode->setViInputModeManager(viInputModeManager); m_interactiveSedReplaceMode->setViInputModeManager(viInputModeManager); } void EmulatedCommandBar::hideAllWidgetsExcept(QWidget *widgetToKeepVisible) { const QList widgets = centralWidget()->findChildren(); for (QWidget *widget : widgets) { if (widget != widgetToKeepVisible) widget->hide(); } } void EmulatedCommandBar::createAndAddBarTypeIndicator(QLayout *layout) { m_barTypeIndicator = new QLabel(this); m_barTypeIndicator->setObjectName(QStringLiteral("bartypeindicator")); layout->addWidget(m_barTypeIndicator); } void EmulatedCommandBar::createAndAddEditWidget(QLayout *layout) { m_edit = new QLineEdit(this); m_edit->setObjectName(QStringLiteral("commandtext")); layout->addWidget(m_edit); } void EmulatedCommandBar::createAndAddExitStatusMessageDisplay(QLayout *layout) { m_exitStatusMessageDisplay = new QLabel(this); m_exitStatusMessageDisplay->setObjectName(QStringLiteral("commandresponsemessage")); m_exitStatusMessageDisplay->setAlignment(Qt::AlignLeft); layout->addWidget(m_exitStatusMessageDisplay); } void EmulatedCommandBar::createAndInitExitStatusMessageDisplayTimer() { m_exitStatusMessageDisplayHideTimer = new QTimer(this); m_exitStatusMessageDisplayHideTimer->setSingleShot(true); connect(m_exitStatusMessageDisplayHideTimer, SIGNAL(timeout()), this, SIGNAL(hideMe())); // Make sure the timer is stopped when the user switches views. If not, focus will be given to the // wrong view when KateViewBar::hideCurrentBarWidget() is called as a result of m_commandResponseMessageDisplayHide // timing out. connect(m_view, SIGNAL(focusOut(KTextEditor::View *)), m_exitStatusMessageDisplayHideTimer, SLOT(stop())); // We can restart the timer once the view has focus again, though. connect(m_view, SIGNAL(focusIn(KTextEditor::View *)), this, SLOT(startHideExitStatusMessageTimer())); } void EmulatedCommandBar::createAndAddWaitingForRegisterIndicator(QLayout *layout) { m_waitingForRegisterIndicator = new QLabel(this); m_waitingForRegisterIndicator->setObjectName(QStringLiteral("waitingforregisterindicator")); m_waitingForRegisterIndicator->setVisible(false); m_waitingForRegisterIndicator->setText(QStringLiteral("\"")); layout->addWidget(m_waitingForRegisterIndicator); } diff --git a/src/vimode/emulatedcommandbar/searchmode.cpp b/src/vimode/emulatedcommandbar/searchmode.cpp index b6e73639..e9046995 100644 --- a/src/vimode/emulatedcommandbar/searchmode.cpp +++ b/src/vimode/emulatedcommandbar/searchmode.cpp @@ -1,384 +1,384 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2013-2016 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "searchmode.h" #include "../globalstate.h" #include "../history.h" #include "katedocument.h" #include "kateview.h" #include #include #include #include #include using namespace KateVi; namespace { bool isCharEscaped(const QString &string, int charPos) { if (charPos == 0) { return false; } int numContiguousBackslashesToLeft = 0; charPos--; while (charPos >= 0 && string[charPos] == QLatin1Char('\\')) { numContiguousBackslashesToLeft++; charPos--; } return ((numContiguousBackslashesToLeft % 2) == 1); } QString toggledEscaped(const QString &originalString, QChar escapeChar) { int searchFrom = 0; QString toggledEscapedString = originalString; do { const int indexOfEscapeChar = toggledEscapedString.indexOf(escapeChar, searchFrom); if (indexOfEscapeChar == -1) { break; } if (!isCharEscaped(toggledEscapedString, indexOfEscapeChar)) { // Escape. toggledEscapedString.replace(indexOfEscapeChar, 1, QLatin1String("\\") + escapeChar); searchFrom = indexOfEscapeChar + 2; } else { // Unescape. toggledEscapedString.remove(indexOfEscapeChar - 1, 1); searchFrom = indexOfEscapeChar; } } while (true); return toggledEscapedString; } int findPosOfSearchConfigMarker(const QString &searchText, const bool isSearchBackwards) { const QChar searchConfigMarkerChar = (isSearchBackwards ? QLatin1Char('?') : QLatin1Char('/')); for (int pos = 0; pos < searchText.length(); pos++) { if (searchText.at(pos) == searchConfigMarkerChar) { if (!isCharEscaped(searchText, pos)) { return pos; } } } return -1; } bool isRepeatLastSearch(const QString &searchText, const bool isSearchBackwards) { const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards); if (posOfSearchConfigMarker != -1) { if (searchText.leftRef(posOfSearchConfigMarker).isEmpty()) { return true; } } return false; } bool shouldPlaceCursorAtEndOfMatch(const QString &searchText, const bool isSearchBackwards) { const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards); if (posOfSearchConfigMarker != -1) { if (searchText.length() > posOfSearchConfigMarker + 1 && searchText.at(posOfSearchConfigMarker + 1) == QLatin1Char('e')) { return true; } } return false; } QString withSearchConfigRemoved(const QString &originalSearchText, const bool isSearchBackwards) { const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(originalSearchText, isSearchBackwards); if (posOfSearchConfigMarker == -1) { return originalSearchText; } else { return originalSearchText.left(posOfSearchConfigMarker); } } } QString KateVi::vimRegexToQtRegexPattern(const QString &vimRegexPattern) { QString qtRegexPattern = vimRegexPattern; qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('(')); qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char(')')); qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('+')); qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('|')); qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('?')); { // All curly brackets, except the closing curly bracket of a matching pair where the opening bracket is escaped, // must have their escaping toggled. bool lookingForMatchingCloseBracket = false; QList matchingClosedCurlyBracketPositions; for (int i = 0; i < qtRegexPattern.length(); i++) { if (qtRegexPattern[i] == QLatin1Char('{') && isCharEscaped(qtRegexPattern, i)) { lookingForMatchingCloseBracket = true; } if (qtRegexPattern[i] == QLatin1Char('}') && lookingForMatchingCloseBracket && qtRegexPattern[i - 1] != QLatin1Char('\\')) { matchingClosedCurlyBracketPositions.append(i); } } if (matchingClosedCurlyBracketPositions.isEmpty()) { // Escape all {'s and }'s - there are no matching pairs. qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('{')); qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('}')); } else { // Ensure that every chunk of qtRegexPattern that does *not* contain a curly closing bracket // that is matched have their { and } escaping toggled. QString qtRegexPatternNonMatchingCurliesToggled; int previousNonMatchingClosedCurlyPos = 0; // i.e. the position of the last character which is either // not a curly closing bracket, or is a curly closing bracket // that is not matched. for (int matchingClosedCurlyPos : qAsConst(matchingClosedCurlyBracketPositions)) { QString chunkExcludingMatchingCurlyClosed = qtRegexPattern.mid(previousNonMatchingClosedCurlyPos, matchingClosedCurlyPos - previousNonMatchingClosedCurlyPos); chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('{')); chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('}')); qtRegexPatternNonMatchingCurliesToggled += chunkExcludingMatchingCurlyClosed + qtRegexPattern[matchingClosedCurlyPos]; previousNonMatchingClosedCurlyPos = matchingClosedCurlyPos + 1; } QString chunkAfterLastMatchingClosedCurly = qtRegexPattern.mid(matchingClosedCurlyBracketPositions.last() + 1); chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('{')); chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('}')); qtRegexPatternNonMatchingCurliesToggled += chunkAfterLastMatchingClosedCurly; qtRegexPattern = qtRegexPatternNonMatchingCurliesToggled; } } // All square brackets, *except* for those that are a) unescaped; and b) form a matching pair, must be // escaped. bool lookingForMatchingCloseBracket = false; int openingBracketPos = -1; QList matchingSquareBracketPositions; for (int i = 0; i < qtRegexPattern.length(); i++) { if (qtRegexPattern[i] == QLatin1Char('[') && !isCharEscaped(qtRegexPattern, i) && !lookingForMatchingCloseBracket) { lookingForMatchingCloseBracket = true; openingBracketPos = i; } if (qtRegexPattern[i] == QLatin1Char(']') && lookingForMatchingCloseBracket && !isCharEscaped(qtRegexPattern, i)) { lookingForMatchingCloseBracket = false; matchingSquareBracketPositions.append(openingBracketPos); matchingSquareBracketPositions.append(i); } } if (matchingSquareBracketPositions.isEmpty()) { // Escape all ['s and ]'s - there are no matching pairs. qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('[')); qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char(']')); } else { // Ensure that every chunk of qtRegexPattern that does *not* contain one of the matching pairs of // square brackets have their square brackets escaped. QString qtRegexPatternNonMatchingSquaresMadeLiteral; int previousNonMatchingSquareBracketPos = 0; // i.e. the position of the last character which is // either not a square bracket, or is a square bracket but // which is not matched. for (int matchingSquareBracketPos : qAsConst(matchingSquareBracketPositions)) { QString chunkExcludingMatchingSquareBrackets = qtRegexPattern.mid(previousNonMatchingSquareBracketPos, matchingSquareBracketPos - previousNonMatchingSquareBracketPos); chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char('[')); chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char(']')); qtRegexPatternNonMatchingSquaresMadeLiteral += chunkExcludingMatchingSquareBrackets + qtRegexPattern[matchingSquareBracketPos]; previousNonMatchingSquareBracketPos = matchingSquareBracketPos + 1; } QString chunkAfterLastMatchingSquareBracket = qtRegexPattern.mid(matchingSquareBracketPositions.last() + 1); chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char('[')); chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char(']')); qtRegexPatternNonMatchingSquaresMadeLiteral += chunkAfterLastMatchingSquareBracket; qtRegexPattern = qtRegexPatternNonMatchingSquaresMadeLiteral; } qtRegexPattern.replace(QLatin1String("\\>"), QLatin1String("\\b")); qtRegexPattern.replace(QLatin1String("\\<"), QLatin1String("\\b")); return qtRegexPattern; } QString KateVi::ensuredCharEscaped(const QString &originalString, QChar charToEscape) { QString escapedString = originalString; for (int i = 0; i < escapedString.length(); i++) { if (escapedString[i] == charToEscape && !isCharEscaped(escapedString, i)) { escapedString.replace(i, 1, QLatin1String("\\") + charToEscape); } } return escapedString; } QString KateVi::withCaseSensitivityMarkersStripped(const QString &originalSearchTerm) { // Only \C is handled, for now - I'll implement \c if someone asks for it. int pos = 0; QString caseSensitivityMarkersStripped = originalSearchTerm; while (pos < caseSensitivityMarkersStripped.length()) { if (caseSensitivityMarkersStripped.at(pos) == QLatin1Char('C') && isCharEscaped(caseSensitivityMarkersStripped, pos)) { caseSensitivityMarkersStripped.remove(pos - 1, 2); pos--; } pos++; } return caseSensitivityMarkersStripped; } QStringList KateVi::reversed(const QStringList &originalList) { QStringList reversedList = originalList; std::reverse(reversedList.begin(), reversedList.end()); return reversedList; } SearchMode::SearchMode(EmulatedCommandBar *emulatedCommandBar, MatchHighlighter *matchHighlighter, InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, QLineEdit *edit) : ActiveMode(emulatedCommandBar, matchHighlighter, viInputModeManager, view) , m_edit(edit) { } void SearchMode::init(SearchMode::SearchDirection searchDirection) { m_searchDirection = searchDirection; m_startingCursorPos = view()->cursorPosition(); } bool SearchMode::handleKeyPress(const QKeyEvent *keyEvent) { Q_UNUSED(keyEvent); return false; } void SearchMode::editTextChanged(const QString &newText) { QString qtRegexPattern = newText; const bool searchBackwards = (m_searchDirection == SearchDirection::Backward); const bool placeCursorAtEndOfMatch = shouldPlaceCursorAtEndOfMatch(qtRegexPattern, searchBackwards); if (isRepeatLastSearch(qtRegexPattern, searchBackwards)) { qtRegexPattern = viInputModeManager()->searcher()->getLastSearchPattern(); } else { qtRegexPattern = withSearchConfigRemoved(qtRegexPattern, searchBackwards); qtRegexPattern = vimRegexToQtRegexPattern(qtRegexPattern); } // Decide case-sensitivity via SmartCase (note: if the expression contains \C, the "case-sensitive" marker, then // we will be case-sensitive "by coincidence", as it were.). bool caseSensitive = true; if (qtRegexPattern.toLower() == qtRegexPattern) { caseSensitive = false; } qtRegexPattern = withCaseSensitivityMarkersStripped(qtRegexPattern); m_currentSearchParams.pattern = qtRegexPattern; m_currentSearchParams.isCaseSensitive = caseSensitive; m_currentSearchParams.isBackwards = searchBackwards; m_currentSearchParams.shouldPlaceCursorAtEndOfMatch = placeCursorAtEndOfMatch; // The "count" for the current search is not shared between Visual & Normal mode, so we need to pick // the right one to handle the counted search. int c = viInputModeManager()->getCurrentViModeHandler()->getCount(); KTextEditor::Range match = viInputModeManager()->searcher()->findPattern(m_currentSearchParams, m_startingCursorPos, c, false /* Don't add incremental searches to search history */); if (match.isValid()) { // The returned range ends one past the last character of the match, so adjust. KTextEditor::Cursor realMatchEnd = KTextEditor::Cursor(match.end().line(), match.end().column() - 1); if (realMatchEnd.column() == -1) { realMatchEnd = KTextEditor::Cursor(realMatchEnd.line() - 1, view()->doc()->lineLength(realMatchEnd.line() - 1)); } moveCursorTo(placeCursorAtEndOfMatch ? realMatchEnd : match.start()); setBarBackground(SearchMode::MatchFound); } else { moveCursorTo(m_startingCursorPos); if (!m_edit->text().isEmpty()) { setBarBackground(SearchMode::NoMatchFound); } else { setBarBackground(SearchMode::Normal); } } updateMatchHighlight(match); } void SearchMode::deactivate(bool wasAborted) { // "Deactivate" can be called multiple times between init()'s, so only reset the cursor once! if (m_startingCursorPos.isValid()) { if (wasAborted) { moveCursorTo(m_startingCursorPos); } } m_startingCursorPos = KTextEditor::Cursor::invalid(); setBarBackground(SearchMode::Normal); // Send a synthetic keypress through the system that signals whether the search was aborted or // not. If not, the keypress will "complete" the search motion, thus triggering it. // We send to KateViewInternal as it updates the status bar and removes the "?". const Qt::Key syntheticSearchCompletedKey = (wasAborted ? static_cast(0) : Qt::Key_Enter); QKeyEvent syntheticSearchCompletedKeyPress(QEvent::KeyPress, syntheticSearchCompletedKey, Qt::NoModifier); m_isSendingSyntheticSearchCompletedKeypress = true; QApplication::sendEvent(view()->focusProxy(), &syntheticSearchCompletedKeyPress); m_isSendingSyntheticSearchCompletedKeypress = false; if (!wasAborted) { // Search was actually executed, so store it as the last search. viInputModeManager()->searcher()->setLastSearchParams(m_currentSearchParams); } // Append the raw text of the search to the search history (i.e. without conversion // from Vim-style regex; without case-sensitivity markers stripped; etc. // Vim does this even if the search was aborted, so we follow suit. viInputModeManager()->globalState()->searchHistory()->append(m_edit->text()); } CompletionStartParams SearchMode::completionInvoked(Completer::CompletionInvocation invocationType) { Q_UNUSED(invocationType); return activateSearchHistoryCompletion(); } void SearchMode::completionChosen() { // Choose completion with Enter/ Return -> close bar (the search will have already taken effect at this point), marking as not aborted . close(false); } CompletionStartParams SearchMode::activateSearchHistoryCompletion() { return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), 0); } void SearchMode::setBarBackground(SearchMode::BarBackgroundStatus status) { QPalette barBackground(m_edit->palette()); switch (status) { - case MatchFound: { - KColorScheme::adjustBackground(barBackground, KColorScheme::PositiveBackground); - break; - } - case NoMatchFound: { - KColorScheme::adjustBackground(barBackground, KColorScheme::NegativeBackground); - break; - } - case Normal: { - barBackground = QPalette(); - break; - } + case MatchFound: { + KColorScheme::adjustBackground(barBackground, KColorScheme::PositiveBackground); + break; + } + case NoMatchFound: { + KColorScheme::adjustBackground(barBackground, KColorScheme::NegativeBackground); + break; + } + case Normal: { + barBackground = QPalette(); + break; + } } m_edit->setPalette(barBackground); } diff --git a/src/vimode/inputmodemanager.cpp b/src/vimode/inputmodemanager.cpp index d4e26a77..121d8c36 100644 --- a/src/vimode/inputmodemanager.cpp +++ b/src/vimode/inputmodemanager.cpp @@ -1,474 +1,474 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008 - 2009 Erlend Hamberg * Copyright (C) 2009 Paul Gideon Dann * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include "completionrecorder.h" #include "completionreplayer.h" #include "globalstate.h" #include "jumps.h" #include "kateconfig.h" #include "kateglobal.h" #include "kateviewinternal.h" #include "kateviinputmode.h" #include "lastchangerecorder.h" #include "macrorecorder.h" #include "macros.h" #include "marks.h" #include "registers.h" #include "searcher.h" #include #include #include #include #include #include #include using namespace KateVi; InputModeManager::InputModeManager(KateViInputMode *inputAdapter, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : m_inputAdapter(inputAdapter) { m_currentViMode = ViMode::NormalMode; m_previousViMode = ViMode::NormalMode; m_viNormalMode = new NormalViMode(this, view, viewInternal); m_viInsertMode = new InsertViMode(this, view, viewInternal); m_viVisualMode = new VisualViMode(this, view, viewInternal); m_viReplaceMode = new ReplaceViMode(this, view, viewInternal); m_view = view; m_viewInternal = viewInternal; m_insideHandlingKeyPressCount = 0; m_keyMapperStack.push(QSharedPointer(new KeyMapper(this, m_view->doc(), m_view))); m_temporaryNormalMode = false; m_jumps = new Jumps(); m_marks = new Marks(this); m_searcher = new Searcher(this); m_completionRecorder = new CompletionRecorder(this); m_completionReplayer = new CompletionReplayer(this); m_macroRecorder = new MacroRecorder(this); m_lastChangeRecorder = new LastChangeRecorder(this); // We have to do this outside of NormalMode, as we don't want // VisualMode (which inherits from NormalMode) to respond // to changes in the document as well. m_viNormalMode->beginMonitoringDocumentChanges(); } InputModeManager::~InputModeManager() { delete m_viNormalMode; delete m_viInsertMode; delete m_viVisualMode; delete m_viReplaceMode; delete m_jumps; delete m_marks; delete m_searcher; delete m_macroRecorder; delete m_completionRecorder; delete m_completionReplayer; delete m_lastChangeRecorder; } bool InputModeManager::handleKeypress(const QKeyEvent *e) { m_insideHandlingKeyPressCount++; bool res = false; bool keyIsPartOfMapping = false; const bool isSyntheticSearchCompletedKeyPress = m_inputAdapter->viModeEmulatedCommandBar()->isSendingSyntheticSearchCompletedKeypress(); // With macros, we want to record the keypresses *before* they are mapped, but if they end up *not* being part // of a mapping, we don't want to record them when they are played back by m_keyMapper, hence // the "!isPlayingBackRejectedKeys()". And obviously, since we're recording keys before they are mapped, we don't // want to also record the executed mapping, as when we replayed the macro, we'd get duplication! if (m_macroRecorder->isRecording() && !m_macroRecorder->isReplaying() && !isSyntheticSearchCompletedKeyPress && !keyMapper()->isExecutingMapping() && !keyMapper()->isPlayingBackRejectedKeys() && !lastChangeRecorder()->isReplaying()) { m_macroRecorder->record(*e); } if (!m_lastChangeRecorder->isReplaying() && !isSyntheticSearchCompletedKeyPress) { if (e->key() == Qt::Key_AltGr) { return true; // do nothing } // Hand off to the key mapper, and decide if this key is part of a mapping. if (e->key() != Qt::Key_Control && e->key() != Qt::Key_Shift && e->key() != Qt::Key_Alt && e->key() != Qt::Key_Meta) { const QChar key = KeyParser::self()->KeyEventToQChar(*e); if (keyMapper()->handleKeypress(key)) { keyIsPartOfMapping = true; res = true; } } } if (!keyIsPartOfMapping) { if (!m_lastChangeRecorder->isReplaying() && !isSyntheticSearchCompletedKeyPress) { // record key press so that it can be repeated via "." m_lastChangeRecorder->record(*e); } if (m_inputAdapter->viModeEmulatedCommandBar()->isActive()) { res = m_inputAdapter->viModeEmulatedCommandBar()->handleKeyPress(e); } else { res = getCurrentViModeHandler()->handleKeypress(e); } } m_insideHandlingKeyPressCount--; Q_ASSERT(m_insideHandlingKeyPressCount >= 0); return res; } void InputModeManager::feedKeyPresses(const QString &keyPresses) const { int key; Qt::KeyboardModifiers mods; QString text; for (const QChar c : keyPresses) { QString decoded = KeyParser::self()->decodeKeySequence(QString(c)); key = -1; mods = Qt::NoModifier; text.clear(); if (decoded.length() > 1) { // special key // remove the angle brackets decoded.remove(0, 1); decoded.remove(decoded.indexOf(QLatin1Char('>')), 1); // check if one or more modifier keys where used if (decoded.indexOf(QLatin1String("s-")) != -1 || decoded.indexOf(QLatin1String("c-")) != -1 || decoded.indexOf(QLatin1String("m-")) != -1 || decoded.indexOf(QLatin1String("a-")) != -1) { int s = decoded.indexOf(QLatin1String("s-")); if (s != -1) { mods |= Qt::ShiftModifier; decoded.remove(s, 2); } int c = decoded.indexOf(QLatin1String("c-")); if (c != -1) { mods |= Qt::ControlModifier; decoded.remove(c, 2); } int a = decoded.indexOf(QLatin1String("a-")); if (a != -1) { mods |= Qt::AltModifier; decoded.remove(a, 2); } int m = decoded.indexOf(QLatin1String("m-")); if (m != -1) { mods |= Qt::MetaModifier; decoded.remove(m, 2); } if (decoded.length() > 1) { key = KeyParser::self()->vi2qt(decoded); } else if (decoded.length() == 1) { key = int(decoded.at(0).toUpper().toLatin1()); text = decoded.at(0); } } else { // no modifiers key = KeyParser::self()->vi2qt(decoded); } } else { key = decoded.at(0).unicode(); text = decoded.at(0); } if (key == -1) continue; // We have to be clever about which widget we dispatch to, as we can trigger // shortcuts if we're not careful (even if Vim mode is configured to steal shortcuts). QKeyEvent k(QEvent::KeyPress, key, mods, text); QWidget *destWidget = nullptr; if (QApplication::activePopupWidget()) { // According to the docs, the activePopupWidget, if present, takes all events. destWidget = QApplication::activePopupWidget(); } else if (QApplication::focusWidget()) { if (QApplication::focusWidget()->focusProxy()) { destWidget = QApplication::focusWidget()->focusProxy(); } else { destWidget = QApplication::focusWidget(); } } else { destWidget = m_view->focusProxy(); } QApplication::sendEvent(destWidget, &k); } } bool InputModeManager::isHandlingKeypress() const { return m_insideHandlingKeyPressCount > 0; } void InputModeManager::storeLastChangeCommand() { m_lastChange = m_lastChangeRecorder->encodedChanges(); m_lastChangeCompletionsLog = m_completionRecorder->currentChangeCompletionsLog(); } void InputModeManager::repeatLastChange() { m_lastChangeRecorder->replay(m_lastChange, m_lastChangeCompletionsLog); } void InputModeManager::clearCurrentChangeLog() { m_lastChangeRecorder->clear(); m_completionRecorder->clearCurrentChangeCompletionsLog(); } void InputModeManager::doNotLogCurrentKeypress() { m_macroRecorder->dropLast(); m_lastChangeRecorder->dropLast(); } void InputModeManager::changeViMode(ViMode newMode) { m_previousViMode = m_currentViMode; m_currentViMode = newMode; } ViMode InputModeManager::getCurrentViMode() const { return m_currentViMode; } KTextEditor::View::ViewMode InputModeManager::getCurrentViewMode() const { switch (m_currentViMode) { - case ViMode::InsertMode: - return KTextEditor::View::ViModeInsert; - case ViMode::VisualMode: - return KTextEditor::View::ViModeVisual; - case ViMode::VisualLineMode: - return KTextEditor::View::ViModeVisualLine; - case ViMode::VisualBlockMode: - return KTextEditor::View::ViModeVisualBlock; - case ViMode::ReplaceMode: - return KTextEditor::View::ViModeReplace; - case ViMode::NormalMode: - default: - return KTextEditor::View::ViModeNormal; + case ViMode::InsertMode: + return KTextEditor::View::ViModeInsert; + case ViMode::VisualMode: + return KTextEditor::View::ViModeVisual; + case ViMode::VisualLineMode: + return KTextEditor::View::ViModeVisualLine; + case ViMode::VisualBlockMode: + return KTextEditor::View::ViModeVisualBlock; + case ViMode::ReplaceMode: + return KTextEditor::View::ViModeReplace; + case ViMode::NormalMode: + default: + return KTextEditor::View::ViModeNormal; } } ViMode InputModeManager::getPreviousViMode() const { return m_previousViMode; } bool InputModeManager::isAnyVisualMode() const { return ((m_currentViMode == ViMode::VisualMode) || (m_currentViMode == ViMode::VisualLineMode) || (m_currentViMode == ViMode::VisualBlockMode)); } ::ModeBase *InputModeManager::getCurrentViModeHandler() const { switch (m_currentViMode) { - case ViMode::NormalMode: - return m_viNormalMode; - case ViMode::InsertMode: - return m_viInsertMode; - case ViMode::VisualMode: - case ViMode::VisualLineMode: - case ViMode::VisualBlockMode: - return m_viVisualMode; - case ViMode::ReplaceMode: - return m_viReplaceMode; + case ViMode::NormalMode: + return m_viNormalMode; + case ViMode::InsertMode: + return m_viInsertMode; + case ViMode::VisualMode: + case ViMode::VisualLineMode: + case ViMode::VisualBlockMode: + return m_viVisualMode; + case ViMode::ReplaceMode: + return m_viReplaceMode; } return nullptr; } void InputModeManager::viEnterNormalMode() { bool moveCursorLeft = (m_currentViMode == ViMode::InsertMode || m_currentViMode == ViMode::ReplaceMode) && m_viewInternal->cursorPosition().column() > 0; if (!m_lastChangeRecorder->isReplaying() && (m_currentViMode == ViMode::InsertMode || m_currentViMode == ViMode::ReplaceMode)) { // '^ is the insert mark and "^ is the insert register, // which holds the last inserted text KTextEditor::Range r(m_view->cursorPosition(), m_marks->getInsertStopped()); if (r.isValid()) { QString insertedText = m_view->doc()->text(r); m_inputAdapter->globalState()->registers()->setInsertStopped(insertedText); } m_marks->setInsertStopped(KTextEditor::Cursor(m_view->cursorPosition())); } changeViMode(ViMode::NormalMode); if (moveCursorLeft) { m_viewInternal->cursorPrevChar(); } m_inputAdapter->setCaretStyle(KateRenderer::Block); m_viewInternal->update(); } void InputModeManager::viEnterInsertMode() { changeViMode(ViMode::InsertMode); m_marks->setInsertStopped(KTextEditor::Cursor(m_view->cursorPosition())); if (getTemporaryNormalMode()) { // Ensure the key log contains a request to re-enter Insert mode, else the keystrokes made // after returning from temporary normal mode will be treated as commands! m_lastChangeRecorder->record(QKeyEvent(QEvent::KeyPress, Qt::Key_I, Qt::NoModifier, QStringLiteral("i"))); } m_inputAdapter->setCaretStyle(KateRenderer::Line); setTemporaryNormalMode(false); m_viewInternal->update(); } void InputModeManager::viEnterVisualMode(ViMode mode) { changeViMode(mode); // If the selection is inclusive, the caret should be a block. // If the selection is exclusive, the caret should be a line. m_inputAdapter->setCaretStyle(KateRenderer::Block); m_viewInternal->update(); getViVisualMode()->setVisualModeType(mode); getViVisualMode()->init(); } void InputModeManager::viEnterReplaceMode() { changeViMode(ViMode::ReplaceMode); m_marks->setStartEditYanked(KTextEditor::Cursor(m_view->cursorPosition())); m_inputAdapter->setCaretStyle(KateRenderer::Underline); m_viewInternal->update(); } NormalViMode *InputModeManager::getViNormalMode() { return m_viNormalMode; } InsertViMode *InputModeManager::getViInsertMode() { return m_viInsertMode; } VisualViMode *InputModeManager::getViVisualMode() { return m_viVisualMode; } ReplaceViMode *InputModeManager::getViReplaceMode() { return m_viReplaceMode; } const QString InputModeManager::getVerbatimKeys() const { QString cmd; switch (getCurrentViMode()) { - case ViMode::NormalMode: - cmd = m_viNormalMode->getVerbatimKeys(); - break; - case ViMode::InsertMode: - case ViMode::ReplaceMode: - // ... - break; - case ViMode::VisualMode: - case ViMode::VisualLineMode: - case ViMode::VisualBlockMode: - cmd = m_viVisualMode->getVerbatimKeys(); - break; + case ViMode::NormalMode: + cmd = m_viNormalMode->getVerbatimKeys(); + break; + case ViMode::InsertMode: + case ViMode::ReplaceMode: + // ... + break; + case ViMode::VisualMode: + case ViMode::VisualLineMode: + case ViMode::VisualBlockMode: + cmd = m_viVisualMode->getVerbatimKeys(); + break; } return cmd; } void InputModeManager::readSessionConfig(const KConfigGroup &config) { m_jumps->readSessionConfig(config); m_marks->readSessionConfig(config); } void InputModeManager::writeSessionConfig(KConfigGroup &config) { m_jumps->writeSessionConfig(config); m_marks->writeSessionConfig(config); } void InputModeManager::reset() { if (m_viVisualMode) { m_viVisualMode->reset(); } } KeyMapper *InputModeManager::keyMapper() { return m_keyMapperStack.top().data(); } void InputModeManager::updateCursor(const KTextEditor::Cursor &c) { m_inputAdapter->updateCursor(c); } GlobalState *InputModeManager::globalState() const { return m_inputAdapter->globalState(); } KTextEditor::ViewPrivate *InputModeManager::view() const { return m_view; } void InputModeManager::pushKeyMapper(QSharedPointer mapper) { m_keyMapperStack.push(mapper); } void InputModeManager::popKeyMapper() { m_keyMapperStack.pop(); } diff --git a/src/vimode/mappings.cpp b/src/vimode/mappings.cpp index f3ccc136..f39c7887 100644 --- a/src/vimode/mappings.cpp +++ b/src/vimode/mappings.cpp @@ -1,198 +1,198 @@ /* This file is part of the KDE libraries * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katepartdebug.h" #include "kateviinputmode.h" #include #include #include #include using namespace KateVi; void Mappings::readConfig(const KConfigGroup &config) { readMappings(config, QStringLiteral("Normal"), NormalModeMapping); readMappings(config, QStringLiteral("Visual"), VisualModeMapping); readMappings(config, QStringLiteral("Insert"), InsertModeMapping); readMappings(config, QStringLiteral("Command"), CommandModeMapping); } void Mappings::writeConfig(KConfigGroup &config) const { writeMappings(config, QStringLiteral("Normal"), NormalModeMapping); writeMappings(config, QStringLiteral("Visual"), VisualModeMapping); writeMappings(config, QStringLiteral("Insert"), InsertModeMapping); writeMappings(config, QStringLiteral("Command"), CommandModeMapping); } void Mappings::writeMappings(KConfigGroup &config, const QString &mappingModeName, MappingMode mappingMode) const { config.writeEntry(mappingModeName + QLatin1String(" Mode Mapping Keys"), getAll(mappingMode, true)); QStringList l; QList recursives; const auto all = getAll(mappingMode); l.reserve(all.size()); recursives.reserve(all.size()); for (const QString &s : all) { l << KeyParser::self()->decodeKeySequence(get(mappingMode, s)); recursives << isRecursive(mappingMode, s); } config.writeEntry(mappingModeName + QLatin1String(" Mode Mappings"), l); config.writeEntry(mappingModeName + QLatin1String(" Mode Mappings Recursion"), recursives); QChar leader = (m_leader.isNull()) ? QChar::fromLatin1('\\') : m_leader; config.writeEntry(QStringLiteral("Map Leader"), QString(leader)); } void Mappings::readMappings(const KConfigGroup &config, const QString &mappingModeName, MappingMode mappingMode) { const QStringList keys = config.readEntry(mappingModeName + QLatin1String(" Mode Mapping Keys"), QStringList()); const QStringList mappings = config.readEntry(mappingModeName + QLatin1String(" Mode Mappings"), QStringList()); const QList isRecursive = config.readEntry(mappingModeName + QLatin1String(" Mode Mappings Recursion"), QList()); const QString &mapLeader = config.readEntry(QStringLiteral("Map Leader"), QString()); m_leader = (mapLeader.isEmpty()) ? QChar::fromLatin1('\\') : mapLeader[0]; // sanity check if (keys.length() == mappings.length()) { for (int i = 0; i < keys.length(); i++) { // "Recursion" is a newly-introduced part of the config that some users won't have, // so rather than abort (and lose our mappings) if there are not enough entries, simply // treat any missing ones as Recursive (for backwards compatibility). MappingRecursion recursion = Recursive; if (isRecursive.size() > i && !isRecursive.at(i)) { recursion = NonRecursive; } add(mappingMode, keys.at(i), mappings.at(i), recursion); } } else { qCDebug(LOG_KTE) << "Error when reading mappings from " << mappingModeName << " config: number of keys != number of values"; } } void Mappings::add(MappingMode mode, const QString &from, const QString &to, MappingRecursion recursion) { const QString &encodedMapping = KeyParser::self()->encodeKeySequence(from); if (from.isEmpty()) { return; } const QString encodedTo = KeyParser::self()->encodeKeySequence(to); Mapping mapping = {encodedTo, (recursion == Recursive), false}; // Add this mapping as is. m_mappings[mode][encodedMapping] = mapping; // In normal mode replace the with its value. if (mode == NormalModeMapping) { QString other = from; other.replace(QLatin1String(""), m_leader); other = KeyParser::self()->encodeKeySequence(other); if (other != encodedMapping) { mapping.temporary = true; m_mappings[mode][other] = mapping; } } } void Mappings::remove(MappingMode mode, const QString &from) { const QString &encodedMapping = KeyParser::self()->encodeKeySequence(from); m_mappings[mode].remove(encodedMapping); } void Mappings::clear(MappingMode mode) { m_mappings[mode].clear(); } QString Mappings::get(MappingMode mode, const QString &from, bool decode, bool includeTemporary) const { if (!m_mappings[mode].contains(from)) { return QString(); } const Mapping &mapping = m_mappings[mode][from]; if (mapping.temporary && !includeTemporary) { return QString(); } const QString &ret = mapping.encoded; if (decode) { return KeyParser::self()->decodeKeySequence(ret); } return ret; } QStringList Mappings::getAll(MappingMode mode, bool decode, bool includeTemporary) const { QStringList mappings; const QHash mappingsForMode = m_mappings[mode]; for (auto i = mappingsForMode.begin(); i != mappingsForMode.end(); i++) { if (!includeTemporary && i.value().temporary) { continue; } if (decode) { mappings << KeyParser::self()->decodeKeySequence(i.key()); } else { mappings << i.key(); } } return mappings; } bool Mappings::isRecursive(MappingMode mode, const QString &from) const { if (!m_mappings[mode].contains(from)) { return false; } return m_mappings[mode][from].recursive; } void Mappings::setLeader(const QChar &leader) { m_leader = leader; } Mappings::MappingMode Mappings::mappingModeForCurrentViMode(KateViInputMode *viInputMode) { if (viInputMode->viModeEmulatedCommandBar()->isActive()) { return CommandModeMapping; } const ViMode mode = viInputMode->viInputModeManager()->getCurrentViMode(); switch (mode) { - case ViMode::NormalMode: - return NormalModeMapping; - case ViMode::VisualMode: - case ViMode::VisualLineMode: - case ViMode::VisualBlockMode: - return VisualModeMapping; - case ViMode::InsertMode: - case ViMode::ReplaceMode: - return InsertModeMapping; - default: - Q_ASSERT(false && "unrecognised ViMode!"); - return NormalModeMapping; // Return arbitrary mode to satisfy compiler. + case ViMode::NormalMode: + return NormalModeMapping; + case ViMode::VisualMode: + case ViMode::VisualLineMode: + case ViMode::VisualBlockMode: + return VisualModeMapping; + case ViMode::InsertMode: + case ViMode::ReplaceMode: + return InsertModeMapping; + default: + Q_ASSERT(false && "unrecognised ViMode!"); + return NormalModeMapping; // Return arbitrary mode to satisfy compiler. } } diff --git a/src/vimode/modes/insertvimode.cpp b/src/vimode/modes/insertvimode.cpp index 0e193341..49f445ae 100644 --- a/src/vimode/modes/insertvimode.cpp +++ b/src/vimode/modes/insertvimode.cpp @@ -1,588 +1,588 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2011 Erlend Hamberg * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katecompletiontree.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "kateglobal.h" #include "katepartdebug.h" #include "kateview.h" #include "kateviewinternal.h" #include "kateviinputmode.h" #include #include #include #include #include #include #include #include #include using namespace KateVi; InsertViMode::InsertViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : ModeBase() { m_view = view; m_viewInternal = viewInternal; m_viInputModeManager = viInputModeManager; m_waitingRegister = false; m_blockInsert = None; m_eolPos = 0; m_count = 1; m_countedRepeatsBeginOnNewLine = false; m_isExecutingCompletion = false; connect(doc(), SIGNAL(textInserted(KTextEditor::Document *, KTextEditor::Range)), this, SLOT(textInserted(KTextEditor::Document *, KTextEditor::Range))); } InsertViMode::~InsertViMode() { } bool InsertViMode::commandInsertFromAbove() { KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() <= 0) { return false; } QString line = doc()->line(c.line() - 1); int tabWidth = doc()->config()->tabWidth(); QChar ch = getCharAtVirtualColumn(line, m_view->virtualCursorColumn(), tabWidth); if (ch == QChar::Null) { return false; } return doc()->insertText(c, ch); } bool InsertViMode::commandInsertFromBelow() { KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() >= doc()->lines() - 1) { return false; } QString line = doc()->line(c.line() + 1); int tabWidth = doc()->config()->tabWidth(); QChar ch = getCharAtVirtualColumn(line, m_view->virtualCursorColumn(), tabWidth); if (ch == QChar::Null) { return false; } return doc()->insertText(c, ch); } bool InsertViMode::commandDeleteWord() { KTextEditor::Cursor c1(m_view->cursorPosition()); KTextEditor::Cursor c2; c2 = findPrevWordStart(c1.line(), c1.column()); if (c2.line() != c1.line()) { if (c1.column() == 0) { c2.setColumn(doc()->line(c2.line()).length()); } else { c2.setColumn(0); c2.setLine(c2.line() + 1); } } Range r(c2, c1, ExclusiveMotion); return deleteRange(r, CharWise, false); } bool InsertViMode::commandDeleteLine() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), 0, c.line(), c.column(), ExclusiveMotion); if (c.column() == 0) { // Try to move the current line to the end of the previous line. if (c.line() == 0) { return true; } else { r.startColumn = doc()->line(c.line() - 1).length(); r.startLine--; } } else { /* * Remove backwards until the first non-space character. If no * non-space was found, remove backwards to the first column. */ QRegExp nonSpace(QLatin1String("\\S")); r.startColumn = getLine().indexOf(nonSpace); if (r.startColumn == -1 || r.startColumn >= c.column()) { r.startColumn = 0; } } return deleteRange(r, CharWise, false); } bool InsertViMode::commandDeleteCharBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column() - getCount(), c.line(), c.column(), ExclusiveMotion); if (c.column() == 0) { if (c.line() == 0) { return true; } else { r.startColumn = doc()->line(c.line() - 1).length(); r.startLine--; } } return deleteRange(r, CharWise); } bool InsertViMode::commandNewLine() { doc()->newLine(m_view); return true; } bool InsertViMode::commandIndent() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line(), 0), 1); return true; } bool InsertViMode::commandUnindent() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line(), 0), -1); return true; } bool InsertViMode::commandToFirstCharacterInFile() { KTextEditor::Cursor c(0, 0); updateCursor(c); return true; } bool InsertViMode::commandToLastCharacterInFile() { int lines = doc()->lines() - 1; KTextEditor::Cursor c(lines, doc()->line(lines).length()); updateCursor(c); return true; } bool InsertViMode::commandMoveOneWordLeft() { KTextEditor::Cursor c(m_view->cursorPosition()); c = findPrevWordStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); } updateCursor(c); return true; } bool InsertViMode::commandMoveOneWordRight() { KTextEditor::Cursor c(m_view->cursorPosition()); c = findNextWordStart(c.line(), c.column()); if (!c.isValid()) { c = doc()->documentEnd(); } updateCursor(c); return true; } bool InsertViMode::commandCompleteNext() { if (m_view->completionWidget()->isCompletionActive()) { const QModelIndex oldCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex(); m_view->completionWidget()->cursorDown(); const QModelIndex newCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex(); if (newCompletionItem == oldCompletionItem) { // Wrap to top. m_view->completionWidget()->top(); } } else { m_view->userInvokedCompletion(); } return true; } bool InsertViMode::commandCompletePrevious() { if (m_view->completionWidget()->isCompletionActive()) { const QModelIndex oldCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex(); m_view->completionWidget()->cursorUp(); const QModelIndex newCompletionItem = m_view->completionWidget()->treeView()->selectionModel()->currentIndex(); if (newCompletionItem == oldCompletionItem) { // Wrap to bottom. m_view->completionWidget()->bottom(); } } else { m_view->userInvokedCompletion(); m_view->completionWidget()->bottom(); } return true; } bool InsertViMode::commandInsertContentOfRegister() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor cAfter = c; QChar reg = getChosenRegister(m_register); OperationMode m = getRegisterFlag(reg); QString textToInsert = getRegisterContent(reg); if (textToInsert.isNull()) { error(i18n("Nothing in register %1", reg)); return false; } if (m == LineWise) { textToInsert.chop(1); // remove the last \n c.setColumn(doc()->lineLength(c.line())); // paste after the current line and ... textToInsert.prepend(QLatin1Char('\n')); // ... prepend a \n, so the text starts on a new line cAfter.setLine(cAfter.line() + 1); cAfter.setColumn(0); } else { cAfter.setColumn(cAfter.column() + textToInsert.length()); } doc()->insertText(c, textToInsert, m == Block); updateCursor(cAfter); return true; } // Start Normal mode just for one command and return to Insert mode bool InsertViMode::commandSwitchToNormalModeForJustOneCommand() { m_viInputModeManager->setTemporaryNormalMode(true); m_viInputModeManager->changeViMode(ViMode::NormalMode); const KTextEditor::Cursor cursorPos = m_view->cursorPosition(); // If we're at end of the line, move the cursor back one step, as in Vim. if (doc()->line(cursorPos.line()).length() == cursorPos.column()) { m_view->setCursorPosition(KTextEditor::Cursor(cursorPos.line(), cursorPos.column() - 1)); } m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); emit m_view->viewModeChanged(m_view, m_view->viewMode()); m_viewInternal->repaint(); return true; } /** * checks if the key is a valid command * @return true if a command was completed and executed, false otherwise */ bool InsertViMode::handleKeypress(const QKeyEvent *e) { // backspace should work even if the shift key is down if (e->modifiers() != Qt::ControlModifier && e->key() == Qt::Key_Backspace) { m_view->backspace(); return true; } if (m_keys.isEmpty() && !m_waitingRegister) { if (e->modifiers() == Qt::NoModifier) { switch (e->key()) { - case Qt::Key_Escape: - leaveInsertMode(); + case Qt::Key_Escape: + leaveInsertMode(); + return true; + case Qt::Key_Left: + m_view->cursorLeft(); + return true; + case Qt::Key_Right: + m_view->cursorRight(); + return true; + case Qt::Key_Up: + m_view->up(); + return true; + case Qt::Key_Down: + m_view->down(); + return true; + case Qt::Key_Insert: + startReplaceMode(); + return true; + case Qt::Key_Delete: + m_view->keyDelete(); + return true; + case Qt::Key_Home: + m_view->home(); + return true; + case Qt::Key_End: + m_view->end(); + return true; + case Qt::Key_PageUp: + m_view->pageUp(); + return true; + case Qt::Key_PageDown: + m_view->pageDown(); + return true; + case Qt::Key_Enter: + case Qt::Key_Return: + if (m_view->completionWidget()->isCompletionActive() && !m_viInputModeManager->macroRecorder()->isReplaying() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { + // Filter out Enter/ Return's that trigger a completion when recording macros/ last change stuff; they + // will be replaced with the special code "ctrl-space". + // (This is why there is a "!m_viInputModeManager->isReplayingMacro()" above.) + m_viInputModeManager->doNotLogCurrentKeypress(); + + m_isExecutingCompletion = true; + m_textInsertedByCompletion.clear(); + m_view->completionWidget()->execute(); + completionFinished(); + m_isExecutingCompletion = false; return true; - case Qt::Key_Left: - m_view->cursorLeft(); - return true; - case Qt::Key_Right: - m_view->cursorRight(); - return true; - case Qt::Key_Up: - m_view->up(); - return true; - case Qt::Key_Down: - m_view->down(); - return true; - case Qt::Key_Insert: - startReplaceMode(); - return true; - case Qt::Key_Delete: - m_view->keyDelete(); - return true; - case Qt::Key_Home: - m_view->home(); - return true; - case Qt::Key_End: - m_view->end(); - return true; - case Qt::Key_PageUp: - m_view->pageUp(); - return true; - case Qt::Key_PageDown: - m_view->pageDown(); - return true; - case Qt::Key_Enter: - case Qt::Key_Return: - if (m_view->completionWidget()->isCompletionActive() && !m_viInputModeManager->macroRecorder()->isReplaying() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { - // Filter out Enter/ Return's that trigger a completion when recording macros/ last change stuff; they - // will be replaced with the special code "ctrl-space". - // (This is why there is a "!m_viInputModeManager->isReplayingMacro()" above.) - m_viInputModeManager->doNotLogCurrentKeypress(); - - m_isExecutingCompletion = true; - m_textInsertedByCompletion.clear(); - m_view->completionWidget()->execute(); - completionFinished(); - m_isExecutingCompletion = false; - return true; - } - Q_FALLTHROUGH(); - default: - return false; + } + Q_FALLTHROUGH(); + default: + return false; } } else if (e->modifiers() == Qt::ControlModifier) { switch (e->key()) { - case Qt::Key_BracketLeft: - case Qt::Key_3: - leaveInsertMode(); - return true; - case Qt::Key_Space: - // We use Ctrl-space as a special code in macros/ last change, which means: if replaying - // a macro/ last change, fetch and execute the next completion for this macro/ last change ... - if (!m_viInputModeManager->macroRecorder()->isReplaying() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { - commandCompleteNext(); - // ... therefore, we should not record ctrl-space indiscriminately. - m_viInputModeManager->doNotLogCurrentKeypress(); - } else { - m_viInputModeManager->completionReplayer()->replay(); - } - return true; - case Qt::Key_C: - leaveInsertMode(true); - return true; - case Qt::Key_D: - commandUnindent(); - return true; - case Qt::Key_E: - commandInsertFromBelow(); - return true; - case Qt::Key_N: - if (!m_viInputModeManager->macroRecorder()->isReplaying()) { - commandCompleteNext(); - } - return true; - case Qt::Key_P: - if (!m_viInputModeManager->macroRecorder()->isReplaying()) { - commandCompletePrevious(); - } - return true; - case Qt::Key_T: - commandIndent(); - return true; - case Qt::Key_W: - commandDeleteWord(); - return true; - case Qt::Key_U: - return commandDeleteLine(); - case Qt::Key_J: - commandNewLine(); - return true; - case Qt::Key_H: - commandDeleteCharBackward(); - return true; - case Qt::Key_Y: - commandInsertFromAbove(); - return true; - case Qt::Key_O: - commandSwitchToNormalModeForJustOneCommand(); - return true; - case Qt::Key_Home: - commandToFirstCharacterInFile(); - return true; - case Qt::Key_R: - m_waitingRegister = true; - return true; - case Qt::Key_End: - commandToLastCharacterInFile(); - return true; - case Qt::Key_Left: - commandMoveOneWordLeft(); - return true; - case Qt::Key_Right: - commandMoveOneWordRight(); - return true; - default: - return false; + case Qt::Key_BracketLeft: + case Qt::Key_3: + leaveInsertMode(); + return true; + case Qt::Key_Space: + // We use Ctrl-space as a special code in macros/ last change, which means: if replaying + // a macro/ last change, fetch and execute the next completion for this macro/ last change ... + if (!m_viInputModeManager->macroRecorder()->isReplaying() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { + commandCompleteNext(); + // ... therefore, we should not record ctrl-space indiscriminately. + m_viInputModeManager->doNotLogCurrentKeypress(); + } else { + m_viInputModeManager->completionReplayer()->replay(); + } + return true; + case Qt::Key_C: + leaveInsertMode(true); + return true; + case Qt::Key_D: + commandUnindent(); + return true; + case Qt::Key_E: + commandInsertFromBelow(); + return true; + case Qt::Key_N: + if (!m_viInputModeManager->macroRecorder()->isReplaying()) { + commandCompleteNext(); + } + return true; + case Qt::Key_P: + if (!m_viInputModeManager->macroRecorder()->isReplaying()) { + commandCompletePrevious(); + } + return true; + case Qt::Key_T: + commandIndent(); + return true; + case Qt::Key_W: + commandDeleteWord(); + return true; + case Qt::Key_U: + return commandDeleteLine(); + case Qt::Key_J: + commandNewLine(); + return true; + case Qt::Key_H: + commandDeleteCharBackward(); + return true; + case Qt::Key_Y: + commandInsertFromAbove(); + return true; + case Qt::Key_O: + commandSwitchToNormalModeForJustOneCommand(); + return true; + case Qt::Key_Home: + commandToFirstCharacterInFile(); + return true; + case Qt::Key_R: + m_waitingRegister = true; + return true; + case Qt::Key_End: + commandToLastCharacterInFile(); + return true; + case Qt::Key_Left: + commandMoveOneWordLeft(); + return true; + case Qt::Key_Right: + commandMoveOneWordRight(); + return true; + default: + return false; } } return false; } else if (m_waitingRegister) { // ignore modifier keys alone if (e->key() == Qt::Key_Shift || e->key() == Qt::Key_Control || e->key() == Qt::Key_Alt || e->key() == Qt::Key_Meta) { return false; } QChar key = KeyParser::self()->KeyEventToQChar(*e); key = key.toLower(); m_waitingRegister = false; // is it register ? // TODO: add registers such as '/'. See :h if ((key >= QLatin1Char('0') && key <= QLatin1Char('9')) || (key >= QLatin1Char('a') && key <= QLatin1Char('z')) || key == QLatin1Char('_') || key == QLatin1Char('+') || key == QLatin1Char('*') || key == QLatin1Char('"')) { m_register = key; } else { return false; } commandInsertContentOfRegister(); return true; } return false; } // leave insert mode when esc, etc, is pressed. if leaving block // prepend/append, the inserted text will be added to all block lines. if // ctrl-c is used to exit insert mode this is not done. void InsertViMode::leaveInsertMode(bool force) { m_view->abortCompletion(); if (!force) { if (m_blockInsert != None) { // block append/prepend // make sure cursor haven't been moved if (m_blockRange.startLine == m_view->cursorPosition().line()) { int start, len; QString added; KTextEditor::Cursor c; switch (m_blockInsert) { - case Append: - case Prepend: - if (m_blockInsert == Append) { - start = m_blockRange.endColumn + 1; - } else { - start = m_blockRange.startColumn; - } - - len = m_view->cursorPosition().column() - start; - added = getLine().mid(start, len); - - c = KTextEditor::Cursor(m_blockRange.startLine, start); - for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) { - c.setLine(i); - doc()->insertText(c, added); - } - break; - case AppendEOL: - start = m_eolPos; - len = m_view->cursorPosition().column() - start; - added = getLine().mid(start, len); - - c = KTextEditor::Cursor(m_blockRange.startLine, start); - for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) { - c.setLine(i); - c.setColumn(doc()->lineLength(i)); - doc()->insertText(c, added); - } - break; - default: - error(QStringLiteral("not supported")); + case Append: + case Prepend: + if (m_blockInsert == Append) { + start = m_blockRange.endColumn + 1; + } else { + start = m_blockRange.startColumn; + } + + len = m_view->cursorPosition().column() - start; + added = getLine().mid(start, len); + + c = KTextEditor::Cursor(m_blockRange.startLine, start); + for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) { + c.setLine(i); + doc()->insertText(c, added); + } + break; + case AppendEOL: + start = m_eolPos; + len = m_view->cursorPosition().column() - start; + added = getLine().mid(start, len); + + c = KTextEditor::Cursor(m_blockRange.startLine, start); + for (int i = m_blockRange.startLine + 1; i <= m_blockRange.endLine; i++) { + c.setLine(i); + c.setColumn(doc()->lineLength(i)); + doc()->insertText(c, added); + } + break; + default: + error(QStringLiteral("not supported")); } } m_blockInsert = None; } else { const QString added = doc()->text(KTextEditor::Range(m_viInputModeManager->marks()->getStartEditYanked(), m_view->cursorPosition())); if (m_count > 1) { for (unsigned int i = 0; i < m_count - 1; i++) { if (m_countedRepeatsBeginOnNewLine) { doc()->newLine(m_view); } doc()->insertText(m_view->cursorPosition(), added); } } } } m_countedRepeatsBeginOnNewLine = false; startNormalMode(); } void InsertViMode::setBlockPrependMode(Range blockRange) { // ignore if not more than one line is selected if (blockRange.startLine != blockRange.endLine) { m_blockInsert = Prepend; m_blockRange = blockRange; } } void InsertViMode::setBlockAppendMode(Range blockRange, BlockInsert b) { Q_ASSERT(b == Append || b == AppendEOL); // ignore if not more than one line is selected if (blockRange.startLine != blockRange.endLine) { m_blockRange = blockRange; m_blockInsert = b; if (b == AppendEOL) { m_eolPos = doc()->lineLength(m_blockRange.startLine); } } else { qCDebug(LOG_KTE) << "cursor moved. ignoring block append/prepend"; } } void InsertViMode::completionFinished() { Completion::CompletionType completionType = Completion::PlainText; if (m_view->cursorPosition() != m_textInsertedByCompletionEndPos) { completionType = Completion::FunctionWithArgs; } else if (m_textInsertedByCompletion.endsWith(QLatin1String("()")) || m_textInsertedByCompletion.endsWith(QLatin1String("();"))) { completionType = Completion::FunctionWithoutArgs; } m_viInputModeManager->completionRecorder()->logCompletionEvent(Completion(m_textInsertedByCompletion, KateViewConfig::global()->wordCompletionRemoveTail(), completionType)); } void InsertViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range) { if (m_isExecutingCompletion) { m_textInsertedByCompletion += document->text(range); m_textInsertedByCompletionEndPos = range.end(); } } diff --git a/src/vimode/modes/modebase.cpp b/src/vimode/modes/modebase.cpp index a2e9f78b..caaed8c5 100644 --- a/src/vimode/modes/modebase.cpp +++ b/src/vimode/modes/modebase.cpp @@ -1,1346 +1,1346 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008 - 2012 Erlend Hamberg * Copyright (C) 2009 Paul Gideon Dann * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katelayoutcache.h" #include "katerenderer.h" #include "kateviewinternal.h" #include "kateviinputmode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KateVi; // TODO: the "previous word/WORD [end]" methods should be optimized. now they're being called in a // loop and all calculations done up to finding a match are trown away when called with a count > 1 // because they will simply be called again from the last found position. // They should take the count as a parameter and collect the positions in a QList, then return // element (count - 1) //////////////////////////////////////////////////////////////////////////////// // HELPER METHODS //////////////////////////////////////////////////////////////////////////////// void ModeBase::yankToClipBoard(QChar chosen_register, const QString &text) { // only yank to the clipboard if no register was specified, // textlength > 1 and there is something else then whitespace if ((chosen_register == QLatin1Char('0') || chosen_register == QLatin1Char('-')) && text.length() > 1 && !text.trimmed().isEmpty()) { KTextEditor::EditorPrivate::self()->copyToClipboard(text); } } bool ModeBase::deleteRange(Range &r, OperationMode mode, bool addToRegister) { r.normalize(); bool res = false; QString removedText = getRange(r, mode); if (mode == LineWise) { doc()->editStart(); for (int i = 0; i < r.endLine - r.startLine + 1; i++) { res = doc()->removeLine(r.startLine); } doc()->editEnd(); } else { res = doc()->removeText(r.toEditorRange(), mode == Block); } QChar chosenRegister = getChosenRegister(ZeroRegister); if (addToRegister) { if (r.startLine == r.endLine) { chosenRegister = getChosenRegister(SmallDeleteRegister); fillRegister(chosenRegister, removedText, mode); } else { fillRegister(chosenRegister, removedText, mode); } } yankToClipBoard(chosenRegister, removedText); return res; } const QString ModeBase::getRange(Range &r, OperationMode mode) const { r.normalize(); QString s; if (mode == LineWise) { r.startColumn = 0; r.endColumn = getLine(r.endLine).length(); } if (r.motionType == InclusiveMotion) { r.endColumn++; } KTextEditor::Range range = r.toEditorRange(); if (mode == LineWise) { s = doc()->textLines(range).join(QLatin1Char('\n')); s.append(QLatin1Char('\n')); } else if (mode == Block) { s = doc()->text(range, true); } else { s = doc()->text(range); } return s; } const QString ModeBase::getLine(int line) const { return (line < 0) ? m_view->currentTextLine() : doc()->line(line); } const QChar ModeBase::getCharUnderCursor() const { KTextEditor::Cursor c(m_view->cursorPosition()); QString line = getLine(c.line()); if (line.length() == 0 && c.column() >= line.length()) { return QChar::Null; } return line.at(c.column()); } const QString ModeBase::getWordUnderCursor() const { return doc()->text(getWordRangeUnderCursor()); } const KTextEditor::Range ModeBase::getWordRangeUnderCursor() const { KTextEditor::Cursor c(m_view->cursorPosition()); // find first character that is a “word letter” and start the search there QChar ch = doc()->characterAt(c); int i = 0; while (!ch.isLetterOrNumber() && !ch.isMark() && ch != QLatin1Char('_') && m_extraWordCharacters.indexOf(ch) == -1) { // advance cursor one position c.setColumn(c.column() + 1); if (c.column() > doc()->lineLength(c.line())) { c.setColumn(0); c.setLine(c.line() + 1); if (c.line() == doc()->lines()) { return KTextEditor::Range::invalid(); } } ch = doc()->characterAt(c); i++; // count characters that were advanced so we know where to start the search } // move cursor the word (if cursor was placed on e.g. a paren, this will move // it to the right updateCursor(c); KTextEditor::Cursor c1 = findPrevWordStart(c.line(), c.column() + 1 + i, true); KTextEditor::Cursor c2 = findWordEnd(c1.line(), c1.column() + i - 1, true); c2.setColumn(c2.column() + 1); return KTextEditor::Range(c1, c2); } KTextEditor::Cursor ModeBase::findNextWordStart(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); // the start of word pattern need to take m_extraWordCharacters into account if defined QString startOfWordPattern = QStringLiteral("\\b(\\w"); if (m_extraWordCharacters.length() > 0) { startOfWordPattern.append(QLatin1String("|[") + m_extraWordCharacters + QLatin1Char(']')); } startOfWordPattern.append(QLatin1Char(')')); QRegExp startOfWord(startOfWordPattern); // start of a word QRegExp nonSpaceAfterSpace(QLatin1String("\\s\\S")); // non-space right after space QRegExp nonWordAfterWord(QLatin1String("\\b(?!\\s)\\W")); // word-boundary followed by a non-word which is not a space int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = startOfWord.indexIn(line, c + 1); int c2 = nonSpaceAfterSpace.indexIn(line, c); int c3 = nonWordAfterWord.indexIn(line, c + 1); if (c1 == -1 && c2 == -1 && c3 == -1) { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l >= doc()->lines() - 1) { c = qMax(line.length() - 1, 0); return KTextEditor::Cursor::invalid(); } else { c = 0; l++; line = getLine(l); if (line.length() == 0 || !line.at(c).isSpace()) { found = true; } continue; } } c2++; // the second regexp will match one character *before* the character we want to go to if (c1 <= 0) { c1 = line.length() - 1; } if (c2 <= 0) { c2 = line.length() - 1; } if (c3 <= 0) { c3 = line.length() - 1; } c = qMin(c1, qMin(c2, c3)); found = true; } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findNextWORDStart(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(); int l = fromLine; int c = fromColumn; bool found = false; QRegExp startOfWORD(QLatin1String("\\s\\S")); while (!found) { c = startOfWORD.indexIn(line, c); if (c == -1) { if (onlyCurrentLine) { return KTextEditor::Cursor(l, c); } else if (l >= doc()->lines() - 1) { c = line.length() - 1; break; } else { c = 0; l++; line = getLine(l); if (line.length() == 0 || !line.at(c).isSpace()) { found = true; } continue; } } else { c++; found = true; } } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findPrevWordEnd(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); QString endOfWordPattern = QStringLiteral("\\S\\s|\\S$|\\w\\W|\\S\\b|^$"); if (m_extraWordCharacters.length() > 0) { endOfWordPattern.append(QLatin1String("|[") + m_extraWordCharacters + QLatin1String("][^") + m_extraWordCharacters + QLatin1Char(']')); } QRegExp endOfWord(endOfWordPattern); int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = endOfWord.lastIndexIn(line, c - 1); if (c1 != -1 && c - 1 != -1) { found = true; c = c1; } else { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l > 0) { line = getLine(--l); c = line.length(); continue; } else { return KTextEditor::Cursor::invalid(); } } } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findPrevWORDEnd(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); const QRegExp endOfWORDPattern(QLatin1String("\\S\\s|\\S$|^$")); int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = endOfWORDPattern.lastIndexIn(line, c - 1); if (c1 != -1 && c - 1 != -1) { found = true; c = c1; } else { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l > 0) { line = getLine(--l); c = line.length(); continue; } else { c = 0; return KTextEditor::Cursor::invalid(); } } } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findPrevWordStart(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); // the start of word pattern need to take m_extraWordCharacters into account if defined QString startOfWordPattern = QStringLiteral("\\b(\\w"); if (m_extraWordCharacters.length() > 0) { startOfWordPattern.append(QLatin1String("|[") + m_extraWordCharacters + QLatin1Char(']')); } startOfWordPattern.append(QLatin1Char(')')); QRegExp startOfWord(startOfWordPattern); // start of a word QRegExp nonSpaceAfterSpace(QLatin1String("\\s\\S")); // non-space right after space QRegExp nonWordAfterWord(QLatin1String("\\b(?!\\s)\\W")); // word-boundary followed by a non-word which is not a space QRegExp startOfLine(QLatin1String("^\\S")); // non-space at start of line int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = startOfWord.lastIndexIn(line, -line.length() + c - 1); int c2 = nonSpaceAfterSpace.lastIndexIn(line, -line.length() + c - 2); int c3 = nonWordAfterWord.lastIndexIn(line, -line.length() + c - 1); int c4 = startOfLine.lastIndexIn(line, -line.length() + c - 1); if (c1 == -1 && c2 == -1 && c3 == -1 && c4 == -1) { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l <= 0) { return KTextEditor::Cursor::invalid(); } else { line = getLine(--l); c = line.length(); if (line.length() == 0) { c = 0; found = true; } continue; } } c2++; // the second regexp will match one character *before* the character we want to go to if (c1 <= 0) { c1 = 0; } if (c2 <= 0) { c2 = 0; } if (c3 <= 0) { c3 = 0; } if (c4 <= 0) { c4 = 0; } c = qMax(c1, qMax(c2, qMax(c3, c4))); found = true; } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findPrevWORDStart(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); QRegExp startOfWORD(QLatin1String("\\s\\S")); QRegExp startOfLineWORD(QLatin1String("^\\S")); int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = startOfWORD.lastIndexIn(line, -line.length() + c - 2); int c2 = startOfLineWORD.lastIndexIn(line, -line.length() + c - 1); if (c1 == -1 && c2 == -1) { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l <= 0) { return KTextEditor::Cursor::invalid(); } else { line = getLine(--l); c = line.length(); if (line.length() == 0) { c = 0; found = true; } continue; } } c1++; // the startOfWORD pattern matches one character before the word c = qMax(c1, c2); if (c <= 0) { c = 0; } found = true; } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findWordEnd(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); QString endOfWordPattern = QStringLiteral("\\S\\s|\\S$|\\w\\W|\\S\\b"); if (m_extraWordCharacters.length() > 0) { endOfWordPattern.append(QLatin1String("|[") + m_extraWordCharacters + QLatin1String("][^") + m_extraWordCharacters + QLatin1Char(']')); } QRegExp endOfWORD(endOfWordPattern); int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = endOfWORD.indexIn(line, c + 1); if (c1 != -1) { found = true; c = c1; } else { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l >= doc()->lines() - 1) { c = line.length() - 1; return KTextEditor::Cursor::invalid(); } else { c = -1; line = getLine(++l); continue; } } } return KTextEditor::Cursor(l, c); } KTextEditor::Cursor ModeBase::findWORDEnd(int fromLine, int fromColumn, bool onlyCurrentLine) const { QString line = getLine(fromLine); QRegExp endOfWORD(QLatin1String("\\S\\s|\\S$")); int l = fromLine; int c = fromColumn; bool found = false; while (!found) { int c1 = endOfWORD.indexIn(line, c + 1); if (c1 != -1) { found = true; c = c1; } else { if (onlyCurrentLine) { return KTextEditor::Cursor::invalid(); } else if (l >= doc()->lines() - 1) { c = line.length() - 1; return KTextEditor::Cursor::invalid(); } else { c = -1; line = getLine(++l); continue; } } } return KTextEditor::Cursor(l, c); } Range innerRange(Range range, bool inner) { Range r = range; if (inner) { const int columnDistance = qAbs(r.startColumn - r.endColumn); if ((r.startLine == r.endLine) && columnDistance == 1) { // Start and end are right next to each other; there is nothing inside them. return Range::invalid(); } r.startColumn++; r.endColumn--; } return r; } Range ModeBase::findSurroundingQuotes(const QChar &c, bool inner) const { KTextEditor::Cursor cursor(m_view->cursorPosition()); Range r; r.startLine = cursor.line(); r.endLine = cursor.line(); QString line = doc()->line(cursor.line()); // If cursor on the quote we should choose the best direction. if (line.at(cursor.column()) == c) { int attribute = m_view->doc()->kateTextLine(cursor.line())->attribute(cursor.column()); // If at the beginning of the line - then we might search the end. if (doc()->kateTextLine(cursor.line())->attribute(cursor.column() + 1) == attribute && doc()->kateTextLine(cursor.line())->attribute(cursor.column() - 1) != attribute) { r.startColumn = cursor.column(); r.endColumn = line.indexOf(c, cursor.column() + 1); return innerRange(r, inner); } // If at the end of the line - then we might search the beginning. if (doc()->kateTextLine(cursor.line())->attribute(cursor.column() + 1) != attribute && doc()->kateTextLine(cursor.line())->attribute(cursor.column() - 1) == attribute) { r.startColumn = line.lastIndexOf(c, cursor.column() - 1); r.endColumn = cursor.column(); return innerRange(r, inner); } // Try to search the quote to right int c1 = line.indexOf(c, cursor.column() + 1); if (c1 != -1) { r.startColumn = cursor.column(); r.endColumn = c1; return innerRange(r, inner); } // Try to search the quote to left int c2 = line.lastIndexOf(c, cursor.column() - 1); if (c2 != -1) { r.startColumn = c2; r.endColumn = cursor.column(); return innerRange(r, inner); } // Nothing found - give up :) return Range::invalid(); } r.startColumn = line.lastIndexOf(c, cursor.column()); r.endColumn = line.indexOf(c, cursor.column()); if (r.startColumn == -1 || r.endColumn == -1 || r.startColumn > r.endColumn) { return Range::invalid(); } return innerRange(r, inner); } Range ModeBase::findSurroundingBrackets(const QChar &c1, const QChar &c2, bool inner, const QChar &nested1, const QChar &nested2) const { KTextEditor::Cursor cursor(m_view->cursorPosition()); Range r(cursor, InclusiveMotion); int line = cursor.line(); int column = cursor.column(); int catalan; // Chars should not differ. For equal chars use findSurroundingQuotes. Q_ASSERT(c1 != c2); const QString &l = m_view->doc()->line(line); if (column < l.size() && l.at(column) == c2) { r.endLine = line; r.endColumn = column; } else { if (column < l.size() && l.at(column) == c1) { column++; } for (catalan = 1; line < m_view->doc()->lines(); line++) { const QString &l = m_view->doc()->line(line); for (; column < l.size(); column++) { const QChar &c = l.at(column); if (c == nested1) { catalan++; } else if (c == nested2) { catalan--; } if (!catalan) { break; } } if (!catalan) { break; } column = 0; } if (catalan != 0) { return Range::invalid(); } r.endLine = line; r.endColumn = column; } // Same algorithm but backwards. line = cursor.line(); column = cursor.column(); if (column < l.size() && l.at(column) == c1) { r.startLine = line; r.startColumn = column; } else { if (column < l.size() && l.at(column) == c2) { column--; } for (catalan = 1; line >= 0; line--) { const QString &l = m_view->doc()->line(line); for (; column >= 0; column--) { const QChar &c = l.at(column); if (c == nested1) { catalan--; } else if (c == nested2) { catalan++; } if (!catalan) { break; } } if (!catalan || !line) { break; } column = m_view->doc()->line(line - 1).size() - 1; } if (catalan != 0) { return Range::invalid(); } r.startColumn = column; r.startLine = line; } return innerRange(r, inner); } Range ModeBase::findSurrounding(const QRegExp &c1, const QRegExp &c2, bool inner) const { KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); int col1 = line.lastIndexOf(c1, cursor.column()); int col2 = line.indexOf(c2, cursor.column()); Range r(cursor.line(), col1, cursor.line(), col2, InclusiveMotion); if (col1 == -1 || col2 == -1 || col1 > col2) { return Range::invalid(); } if (inner) { r.startColumn++; r.endColumn--; } return r; } int ModeBase::findLineStartingWitchChar(const QChar &c, int count, bool forward) const { int line = m_view->cursorPosition().line(); int lines = doc()->lines(); int hits = 0; if (forward) { line++; } else { line--; } while (line < lines && line >= 0 && hits < count) { QString l = getLine(line); if (l.length() > 0 && l.at(0) == c) { hits++; } if (hits != count) { if (forward) { line++; } else { line--; } } } if (hits == getCount()) { return line; } return -1; } void ModeBase::updateCursor(const KTextEditor::Cursor &c) const { m_viInputModeManager->updateCursor(c); } /** * @return the register given for the command. If no register was given, defaultReg is returned. */ QChar ModeBase::getChosenRegister(const QChar &defaultReg) const { return (m_register != QChar::Null) ? m_register : defaultReg; } QString ModeBase::getRegisterContent(const QChar ®) { QString r = m_viInputModeManager->globalState()->registers()->getContent(reg); if (r.isNull()) { error(i18n("Nothing in register %1", reg)); } return r; } OperationMode ModeBase::getRegisterFlag(const QChar ®) const { return m_viInputModeManager->globalState()->registers()->getFlag(reg); } void ModeBase::fillRegister(const QChar ®, const QString &text, OperationMode flag) { m_viInputModeManager->globalState()->registers()->set(reg, text, flag); } KTextEditor::Cursor ModeBase::getNextJump(KTextEditor::Cursor cursor) const { return m_viInputModeManager->jumps()->next(cursor); } KTextEditor::Cursor ModeBase::getPrevJump(KTextEditor::Cursor cursor) const { return m_viInputModeManager->jumps()->prev(cursor); } Range ModeBase::goLineDown() { return goLineUpDown(getCount()); } Range ModeBase::goLineUp() { return goLineUpDown(-getCount()); } /** * method for moving up or down one or more lines * note: the sticky column is always a virtual column */ Range ModeBase::goLineUpDown(int lines) { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); int tabstop = doc()->config()->tabWidth(); // if in an empty document, just return if (lines == 0) { return r; } r.endLine += lines; // limit end line to be from line 0 through the last line if (r.endLine < 0) { r.endLine = 0; } else if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } Kate::TextLine startLine = doc()->plainKateTextLine(c.line()); Kate::TextLine endLine = doc()->plainKateTextLine(r.endLine); int endLineLen = doc()->lineLength(r.endLine) - 1; if (endLineLen < 0) { endLineLen = 0; } int endLineLenVirt = endLine->toVirtualColumn(endLineLen, tabstop); int virtColumnStart = startLine->toVirtualColumn(c.column(), tabstop); // if sticky column isn't set, set end column and set sticky column to its virtual column if (m_stickyColumn == -1) { r.endColumn = endLine->fromVirtualColumn(virtColumnStart, tabstop); m_stickyColumn = virtColumnStart; } else { // sticky is set - set end column to its value r.endColumn = endLine->fromVirtualColumn(m_stickyColumn, tabstop); } // make sure end column won't be after the last column of a line if (r.endColumn > endLineLen) { r.endColumn = endLineLen; } // if we move to a line shorter than the current column, go to its end if (virtColumnStart > endLineLenVirt) { r.endColumn = endLineLen; } return r; } Range ModeBase::goVisualLineUpDown(int lines) { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); int tabstop = doc()->config()->tabWidth(); if (lines == 0) { // We're not moving anywhere. return r; } KateLayoutCache *cache = m_viInputModeManager->inputAdapter()->layoutCache(); // Work out the real and visual line pair of the beginning of the visual line we'd end up // on by moving lines visual lines. We ignore the column, for now. int finishVisualLine = cache->viewLine(m_view->cursorPosition()); int finishRealLine = m_view->cursorPosition().line(); int count = qAbs(lines); bool invalidPos = false; if (lines > 0) { // Find the beginning of the visual line "lines" visual lines down. while (count > 0) { finishVisualLine++; if (finishVisualLine >= cache->line(finishRealLine)->viewLineCount()) { finishRealLine++; finishVisualLine = 0; } if (finishRealLine >= doc()->lines()) { invalidPos = true; break; } count--; } } else { // Find the beginning of the visual line "lines" visual lines up. while (count > 0) { finishVisualLine--; if (finishVisualLine < 0) { finishRealLine--; if (finishRealLine < 0) { invalidPos = true; break; } finishVisualLine = cache->line(finishRealLine)->viewLineCount() - 1; } count--; } } if (invalidPos) { r.endLine = -1; r.endColumn = -1; return r; } // We know the final (real) line ... r.endLine = finishRealLine; // ... now work out the final (real) column. if (m_stickyColumn == -1 || !m_lastMotionWasVisualLineUpOrDown) { // Compute new sticky column. It is a *visual* sticky column. int startVisualLine = cache->viewLine(m_view->cursorPosition()); int startRealLine = m_view->cursorPosition().line(); const Kate::TextLine startLine = doc()->plainKateTextLine(c.line()); // Adjust for the fact that if the portion of the line before wrapping is indented, // the continuations are also "invisibly" (i.e. without any spaces in the text itself) indented. const bool isWrappedContinuation = (cache->textLayout(startRealLine, startVisualLine).lineLayout().lineNumber() != 0); const int numInvisibleIndentChars = isWrappedContinuation ? startLine->toVirtualColumn(cache->line(startRealLine)->textLine()->nextNonSpaceChar(0), tabstop) : 0; const int realLineStartColumn = cache->textLayout(startRealLine, startVisualLine).startCol(); const int lineStartVirtualColumn = startLine->toVirtualColumn(realLineStartColumn, tabstop); const int visualColumnNoInvisibleIndent = startLine->toVirtualColumn(c.column(), tabstop) - lineStartVirtualColumn; m_stickyColumn = visualColumnNoInvisibleIndent + numInvisibleIndentChars; Q_ASSERT(m_stickyColumn >= 0); } // The "real" (non-virtual) beginning of the current "line", which might be a wrapped continuation of a // "real" line. const int realLineStartColumn = cache->textLayout(finishRealLine, finishVisualLine).startCol(); const Kate::TextLine endLine = doc()->plainKateTextLine(r.endLine); // Adjust for the fact that if the portion of the line before wrapping is indented, // the continuations are also "invisibly" (i.e. without any spaces in the text itself) indented. const bool isWrappedContinuation = (cache->textLayout(finishRealLine, finishVisualLine).lineLayout().lineNumber() != 0); const int numInvisibleIndentChars = isWrappedContinuation ? endLine->toVirtualColumn(cache->line(finishRealLine)->textLine()->nextNonSpaceChar(0), tabstop) : 0; if (m_stickyColumn == (unsigned int)KateVi::EOL) { const int visualEndColumn = cache->textLayout(finishRealLine, finishVisualLine).lineLayout().textLength() - 1; r.endColumn = endLine->fromVirtualColumn(visualEndColumn + realLineStartColumn - numInvisibleIndentChars, tabstop); } else { // Algorithm: find the "real" column corresponding to the start of the line. Offset from that // until the "visual" column is equal to the "visual" sticky column. int realOffsetToVisualStickyColumn = 0; const int lineStartVirtualColumn = endLine->toVirtualColumn(realLineStartColumn, tabstop); while (true) { const int visualColumn = endLine->toVirtualColumn(realLineStartColumn + realOffsetToVisualStickyColumn, tabstop) - lineStartVirtualColumn + numInvisibleIndentChars; if (visualColumn >= m_stickyColumn) { break; } realOffsetToVisualStickyColumn++; } r.endColumn = realLineStartColumn + realOffsetToVisualStickyColumn; } m_currentMotionWasVisualLineUpOrDown = true; return r; } bool ModeBase::startNormalMode() { /* store the key presses for this "insert mode session" so that it can be repeated with the * '.' command * - ignore transition from Visual Modes */ if (!(m_viInputModeManager->isAnyVisualMode() || m_viInputModeManager->lastChangeRecorder()->isReplaying())) { m_viInputModeManager->storeLastChangeCommand(); m_viInputModeManager->clearCurrentChangeLog(); } m_viInputModeManager->viEnterNormalMode(); m_view->doc()->setUndoMergeAllEdits(false); emit m_view->viewModeChanged(m_view, m_view->viewMode()); return true; } bool ModeBase::startInsertMode() { m_viInputModeManager->viEnterInsertMode(); m_view->doc()->setUndoMergeAllEdits(true); emit m_view->viewModeChanged(m_view, m_view->viewMode()); return true; } bool ModeBase::startReplaceMode() { m_view->doc()->setUndoMergeAllEdits(true); m_viInputModeManager->viEnterReplaceMode(); emit m_view->viewModeChanged(m_view, m_view->viewMode()); return true; } bool ModeBase::startVisualMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { m_viInputModeManager->getViVisualMode()->setVisualModeType(ViMode::VisualMode); m_viInputModeManager->changeViMode(ViMode::VisualMode); } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { m_viInputModeManager->getViVisualMode()->setVisualModeType(ViMode::VisualMode); m_viInputModeManager->changeViMode(ViMode::VisualMode); } else { m_viInputModeManager->viEnterVisualMode(); } emit m_view->viewModeChanged(m_view, m_view->viewMode()); return true; } bool ModeBase::startVisualBlockMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode) { m_viInputModeManager->getViVisualMode()->setVisualModeType(ViMode::VisualBlockMode); m_viInputModeManager->changeViMode(ViMode::VisualBlockMode); } else { m_viInputModeManager->viEnterVisualMode(ViMode::VisualBlockMode); } emit m_view->viewModeChanged(m_view, m_view->viewMode()); return true; } bool ModeBase::startVisualLineMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode) { m_viInputModeManager->getViVisualMode()->setVisualModeType(ViMode::VisualLineMode); m_viInputModeManager->changeViMode(ViMode::VisualLineMode); } else { m_viInputModeManager->viEnterVisualMode(ViMode::VisualLineMode); } emit m_view->viewModeChanged(m_view, m_view->viewMode()); return true; } void ModeBase::error(const QString &errorMsg) { delete m_infoMessage; m_infoMessage = new KTextEditor::Message(errorMsg, KTextEditor::Message::Error); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(2000); // 2 seconds m_infoMessage->setView(m_view); m_view->doc()->postMessage(m_infoMessage); } void ModeBase::message(const QString &msg) { delete m_infoMessage; m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Positive); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(2000); // 2 seconds m_infoMessage->setView(m_view); m_view->doc()->postMessage(m_infoMessage); } QString ModeBase::getVerbatimKeys() const { return m_keysVerbatim; } const QChar ModeBase::getCharAtVirtualColumn(const QString &line, int virtualColumn, int tabWidth) const { int column = 0; int tempCol = 0; // sanity check: if the line is empty, there are no chars if (line.length() == 0) { return QChar::Null; } while (tempCol < virtualColumn) { if (line.at(column) == QLatin1Char('\t')) { tempCol += tabWidth - (tempCol % tabWidth); } else { tempCol++; } if (tempCol <= virtualColumn) { column++; if (column >= line.length()) { return QChar::Null; } } } if (line.length() > column) { return line.at(column); } return QChar::Null; } void ModeBase::addToNumberUnderCursor(int count) { KTextEditor::Cursor c(m_view->cursorPosition()); QString line = getLine(); if (line.isEmpty()) { return; } int numberStartPos = -1; QString numberAsString; QRegExp numberRegex(QLatin1String("(0x)([0-9a-fA-F]+)|\\-?\\d+")); const int cursorColumn = c.column(); const int currentLineLength = doc()->lineLength(c.line()); const KTextEditor::Cursor prevWordStart = findPrevWordStart(c.line(), cursorColumn); int wordStartPos = prevWordStart.column(); if (prevWordStart.line() < c.line()) { // The previous word starts on the previous line: ignore. wordStartPos = 0; } if (wordStartPos > 0 && line.at(wordStartPos - 1) == QLatin1Char('-')) { wordStartPos--; } for (int searchFromColumn = wordStartPos; searchFromColumn < currentLineLength; searchFromColumn++) { numberStartPos = numberRegex.indexIn(line, searchFromColumn); numberAsString = numberRegex.cap(); const bool numberEndedBeforeCursor = (numberStartPos + numberAsString.length() <= c.column()); if (!numberEndedBeforeCursor) { // This is the first number-like string under or after the cursor - this'll do! break; } } if (numberStartPos == -1) { // None found. return; } bool parsedNumberSuccessfully = false; int base = numberRegex.cap(1).isEmpty() ? 10 : 16; if (base != 16 && numberAsString.startsWith(QLatin1Char('0')) && numberAsString.length() > 1) { // If a non-hex number with a leading 0 can be parsed as octal, then assume // it is octal. numberAsString.toInt(&parsedNumberSuccessfully, 8); if (parsedNumberSuccessfully) { base = 8; } } const int originalNumber = numberAsString.toInt(&parsedNumberSuccessfully, base); if (!parsedNumberSuccessfully) { // conversion to int failed. give up. return; } QString basePrefix; if (base == 16) { basePrefix = QStringLiteral("0x"); } else if (base == 8) { basePrefix = QStringLiteral("0"); } const QStringRef withoutBase = numberAsString.midRef(basePrefix.length()); const int newNumber = originalNumber + count; // Create the new text string to be inserted. Prepend with “0x” if in base 16, and "0" if base 8. // For non-decimal numbers, try to keep the length of the number the same (including leading 0's). const QString newNumberPadded = (base == 10) ? QStringLiteral("%1").arg(newNumber, 0, base) : QStringLiteral("%1").arg(newNumber, withoutBase.length(), base, QLatin1Char('0')); const QString newNumberText = basePrefix + newNumberPadded; // Replace the old number string with the new. doc()->editStart(); doc()->removeText(KTextEditor::Range(c.line(), numberStartPos, c.line(), numberStartPos + numberAsString.length())); doc()->insertText(KTextEditor::Cursor(c.line(), numberStartPos), newNumberText); doc()->editEnd(); updateCursor(KTextEditor::Cursor(m_view->cursorPosition().line(), numberStartPos + newNumberText.length() - 1)); } void ModeBase::switchView(Direction direction) { QList visible_views; const auto views = KTextEditor::EditorPrivate::self()->views(); for (KTextEditor::ViewPrivate *view : views) { if (view->isVisible()) { visible_views.push_back(view); } } QPoint current_point = m_view->mapToGlobal(m_view->pos()); int curr_x1 = current_point.x(); int curr_x2 = current_point.x() + m_view->width(); int curr_y1 = current_point.y(); int curr_y2 = current_point.y() + m_view->height(); const KTextEditor::Cursor cursorPos = m_view->cursorPosition(); const QPoint globalPos = m_view->mapToGlobal(m_view->cursorToCoordinate(cursorPos)); int curr_cursor_y = globalPos.y(); int curr_cursor_x = globalPos.x(); KTextEditor::ViewPrivate *bestview = nullptr; int best_x1 = -1, best_x2 = -1, best_y1 = -1, best_y2 = -1, best_center_y = -1, best_center_x = -1; if (direction == Next && visible_views.count() != 1) { for (int i = 0; i < visible_views.count(); i++) { if (visible_views.at(i) == m_view) { if (i != visible_views.count() - 1) { bestview = visible_views.at(i + 1); } else { bestview = visible_views.at(0); } } } } else { for (KTextEditor::ViewPrivate *view : qAsConst(visible_views)) { QPoint point = view->mapToGlobal(view->pos()); int x1 = point.x(); int x2 = point.x() + view->width(); int y1 = point.y(); int y2 = point.y() + m_view->height(); int center_y = (y1 + y2) / 2; int center_x = (x1 + x2) / 2; switch (direction) { - case Left: - if (view != m_view && x2 <= curr_x1 && (x2 > best_x2 || (x2 == best_x2 && qAbs(curr_cursor_y - center_y) < qAbs(curr_cursor_y - best_center_y)) || bestview == nullptr)) { - bestview = view; - best_x2 = x2; - best_center_y = center_y; - } - break; - case Right: - if (view != m_view && x1 >= curr_x2 && (x1 < best_x1 || (x1 == best_x1 && qAbs(curr_cursor_y - center_y) < qAbs(curr_cursor_y - best_center_y)) || bestview == nullptr)) { - bestview = view; - best_x1 = x1; - best_center_y = center_y; - } - break; - case Down: - if (view != m_view && y1 >= curr_y2 && (y1 < best_y1 || (y1 == best_y1 && qAbs(curr_cursor_x - center_x) < qAbs(curr_cursor_x - best_center_x)) || bestview == nullptr)) { - bestview = view; - best_y1 = y1; - best_center_x = center_x; - } - break; - case Up: - if (view != m_view && y2 <= curr_y1 && (y2 > best_y2 || (y2 == best_y2 && qAbs(curr_cursor_x - center_x) < qAbs(curr_cursor_x - best_center_x)) || bestview == nullptr)) { - bestview = view; - best_y2 = y2; - best_center_x = center_x; - } - break; - default: - return; + case Left: + if (view != m_view && x2 <= curr_x1 && (x2 > best_x2 || (x2 == best_x2 && qAbs(curr_cursor_y - center_y) < qAbs(curr_cursor_y - best_center_y)) || bestview == nullptr)) { + bestview = view; + best_x2 = x2; + best_center_y = center_y; + } + break; + case Right: + if (view != m_view && x1 >= curr_x2 && (x1 < best_x1 || (x1 == best_x1 && qAbs(curr_cursor_y - center_y) < qAbs(curr_cursor_y - best_center_y)) || bestview == nullptr)) { + bestview = view; + best_x1 = x1; + best_center_y = center_y; + } + break; + case Down: + if (view != m_view && y1 >= curr_y2 && (y1 < best_y1 || (y1 == best_y1 && qAbs(curr_cursor_x - center_x) < qAbs(curr_cursor_x - best_center_x)) || bestview == nullptr)) { + bestview = view; + best_y1 = y1; + best_center_x = center_x; + } + break; + case Up: + if (view != m_view && y2 <= curr_y1 && (y2 > best_y2 || (y2 == best_y2 && qAbs(curr_cursor_x - center_x) < qAbs(curr_cursor_x - best_center_x)) || bestview == nullptr)) { + bestview = view; + best_y2 = y2; + best_center_x = center_x; + } + break; + default: + return; } } } if (bestview != nullptr) { bestview->setFocus(); bestview->setInputMode(KTextEditor::View::ViInputMode); } } Range ModeBase::motionFindPrev() { return m_viInputModeManager->searcher()->motionFindPrev(getCount()); } Range ModeBase::motionFindNext() { return m_viInputModeManager->searcher()->motionFindNext(getCount()); } void ModeBase::goToPos(const Range &r) { KTextEditor::Cursor c; c.setLine(r.endLine); c.setColumn(r.endColumn); if (!c.isValid()) { return; } if (r.jump) { m_viInputModeManager->jumps()->add(m_view->cursorPosition()); } if (c.line() >= doc()->lines()) { c.setLine(doc()->lines() - 1); } updateCursor(c); } unsigned int ModeBase::linesDisplayed() const { return m_viInputModeManager->inputAdapter()->linesDisplayed(); } void ModeBase::scrollViewLines(int l) { m_viInputModeManager->inputAdapter()->scrollViewLines(l); } int ModeBase::getCount() const { if (m_oneTimeCountOverride != -1) { return m_oneTimeCountOverride; } return (m_count > 0) ? m_count : 1; } diff --git a/src/vimode/modes/normalvimode.cpp b/src/vimode/modes/normalvimode.cpp index c14d416a..af7559ec 100644 --- a/src/vimode/modes/normalvimode.cpp +++ b/src/vimode/modes/normalvimode.cpp @@ -1,4107 +1,4107 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2009 Erlend Hamberg * Copyright (C) 2008 Evgeniy Ivanov * Copyright (C) 2009 Paul Gideon Dann * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katebuffer.h" #include "katecmd.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "kateglobal.h" #include "katepartdebug.h" #include "kateundomanager.h" #include "kateviewhelpers.h" #include "kateviewinternal.h" #include "kateviinputmode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KateVi; #define ADDCMD(STR, FUNC, FLGS) m_commands.push_back(new Command(this, QStringLiteral(STR), &NormalViMode::FUNC, FLGS)); #define ADDMOTION(STR, FUNC, FLGS) m_motions.push_back(new Motion(this, QStringLiteral(STR), &NormalViMode::FUNC, FLGS)); NormalViMode::NormalViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : ModeBase() { m_view = view; m_viewInternal = viewInternal; m_viInputModeManager = viInputModeManager; m_stickyColumn = -1; m_lastMotionWasVisualLineUpOrDown = false; m_currentMotionWasVisualLineUpOrDown = false; // FIXME: make configurable m_extraWordCharacters = QString(); m_matchingItems[QStringLiteral("/*")] = QStringLiteral("*/"); m_matchingItems[QStringLiteral("*/")] = QStringLiteral("-/*"); m_matchItemRegex = generateMatchingItemRegex(); m_scroll_count_limit = 1000; // Limit of count for scroll commands. initializeCommands(); m_pendingResetIsDueToExit = false; m_isRepeatedTFcommand = false; m_lastMotionWasLinewiseInnerBlock = false; m_motionCanChangeWholeVisualModeSelection = false; resetParser(); // initialise with start configuration m_isUndo = false; connect(doc()->undoManager(), SIGNAL(undoStart(KTextEditor::Document *)), this, SLOT(undoBeginning())); connect(doc()->undoManager(), SIGNAL(undoEnd(KTextEditor::Document *)), this, SLOT(undoEnded())); updateYankHighlightAttrib(); connect(view, SIGNAL(configChanged()), this, SLOT(updateYankHighlightAttrib())); connect(doc(), SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearYankHighlight())); connect(doc(), SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(aboutToDeleteMovingInterfaceContent())); } NormalViMode::~NormalViMode() { qDeleteAll(m_commands); qDeleteAll(m_motions); qDeleteAll(m_highlightedYanks); } /** * parses a key stroke to check if it's a valid (part of) a command * @return true if a command was completed and executed, false otherwise */ bool NormalViMode::handleKeypress(const QKeyEvent *e) { const int keyCode = e->key(); // ignore modifier keys alone if (keyCode == Qt::Key_Shift || keyCode == Qt::Key_Control || keyCode == Qt::Key_Alt || keyCode == Qt::Key_Meta) { return false; } clearYankHighlight(); if (keyCode == Qt::Key_Escape || (keyCode == Qt::Key_C && e->modifiers() == Qt::ControlModifier) || (keyCode == Qt::Key_BracketLeft && e->modifiers() == Qt::ControlModifier)) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); m_pendingResetIsDueToExit = true; // Vim in weird as if we e.g. i it claims (in the status bar) to still be in insert mode, // but behaves as if it's in normal mode. I'm treating the status bar thing as a bug and just exiting // insert mode altogether. m_viInputModeManager->setTemporaryNormalMode(false); reset(); return true; } const QChar key = KeyParser::self()->KeyEventToQChar(*e); const QChar lastChar = m_keys.isEmpty() ? QChar::Null : m_keys.at(m_keys.size() - 1); const bool waitingForRegisterOrCharToSearch = this->waitingForRegisterOrCharToSearch(); // Use replace caret when reading a character for "r" if (key == QLatin1Char('r') && !waitingForRegisterOrCharToSearch) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Underline); } m_keysVerbatim.append(KeyParser::self()->decodeKeySequence(key)); if ((keyCode >= Qt::Key_0 && keyCode <= Qt::Key_9 && lastChar != QLatin1Char('"')) // key 0-9 && (m_countTemp != 0 || keyCode != Qt::Key_0) // first digit can't be 0 && (!waitingForRegisterOrCharToSearch) // Not in the middle of "find char" motions or replacing char. && e->modifiers() == Qt::NoModifier) { m_countTemp *= 10; m_countTemp += keyCode - Qt::Key_0; return true; } else if (m_countTemp != 0) { m_count = getCount() * m_countTemp; m_countTemp = 0; m_iscounted = true; } m_keys.append(key); if (m_viInputModeManager->macroRecorder()->isRecording() && key == QLatin1Char('q')) { // Need to special case this "finish macro" q, as the "begin macro" q // needs a parameter whereas the finish macro does not. m_viInputModeManager->macroRecorder()->stop(); resetParser(); return true; } if ((key == QLatin1Char('/') || key == QLatin1Char('?')) && !waitingForRegisterOrCharToSearch) { // Special case for "/" and "?": these should be motions, but this is complicated by // the fact that the user must interact with the search bar before the range of the // motion can be determined. // We hack around this by showing the search bar immediately, and, when the user has // finished interacting with it, have the search bar send a "synthetic" keypresses // that will either abort everything (if the search was aborted) or "complete" the motion // otherwise. m_positionWhenIncrementalSearchBegan = m_view->cursorPosition(); if (key == QLatin1Char('/')) { commandSearchForward(); } else { commandSearchBackward(); } return true; } // Special case: "cw" and "cW" work the same as "ce" and "cE" if the cursor is // on a non-blank. This is because Vim interprets "cw" as change-word, and a // word does not include the following white space. (:help cw in vim) if ((m_keys == QLatin1String("cw") || m_keys == QLatin1String("cW")) && !getCharUnderCursor().isSpace()) { // Special case of the special case: :-) // If the cursor is at the end of the current word rewrite to "cl" const bool isWORD = (m_keys.at(1) == QLatin1Char('W')); const KTextEditor::Cursor currentPosition(m_view->cursorPosition()); const KTextEditor::Cursor endOfWordOrWORD = (isWORD ? findWORDEnd(currentPosition.line(), currentPosition.column() - 1, true) : findWordEnd(currentPosition.line(), currentPosition.column() - 1, true)); if (currentPosition == endOfWordOrWORD) { m_keys = QStringLiteral("cl"); } else { if (isWORD) { m_keys = QStringLiteral("cE"); } else { m_keys = QStringLiteral("ce"); } } } if (m_keys[0] == Qt::Key_QuoteDbl) { if (m_keys.size() < 2) { return true; // waiting for a register } else { QChar r = m_keys[1].toLower(); if ((r >= QLatin1Char('0') && r <= QLatin1Char('9')) || (r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*') || r == QLatin1Char('#') || r == QLatin1Char('^')) { m_register = r; m_keys.clear(); return true; } else { resetParser(); return true; } } } // if we have any matching commands so far, check which ones still match if (!m_matchingCommands.isEmpty()) { int n = m_matchingCommands.size() - 1; // remove commands not matching anymore for (int i = n; i >= 0; i--) { if (!m_commands.at(m_matchingCommands.at(i))->matches(m_keys)) { if (m_commands.at(m_matchingCommands.at(i))->needsMotion()) { // "cache" command needing a motion for later m_motionOperatorIndex = m_matchingCommands.at(i); } m_matchingCommands.remove(i); } } // check if any of the matching commands need a motion/text object, if so // push the current command length to m_awaitingMotionOrTextObject so one // knows where to split the command between the operator and the motion for (int i = 0; i < m_matchingCommands.size(); i++) { if (m_commands.at(m_matchingCommands.at(i))->needsMotion()) { m_awaitingMotionOrTextObject.push(m_keys.size()); break; } } } else { // go through all registered commands and put possible matches in m_matchingCommands for (int i = 0; i < m_commands.size(); i++) { if (m_commands.at(i)->matches(m_keys)) { m_matchingCommands.push_back(i); if (m_commands.at(i)->needsMotion() && m_commands.at(i)->pattern().length() == m_keys.size()) { m_awaitingMotionOrTextObject.push(m_keys.size()); } } } } // this indicates where in the command string one should start looking for a motion command int checkFrom = (m_awaitingMotionOrTextObject.isEmpty() ? 0 : m_awaitingMotionOrTextObject.top()); // Use operator-pending caret when reading a motion for an operator // in normal mode. We need to check that we are indeed in normal mode // since visual mode inherits from it. if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode && !m_awaitingMotionOrTextObject.isEmpty()) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Half); } // look for matching motion commands from position 'checkFrom' // FIXME: if checkFrom hasn't changed, only motions whose index is in // m_matchingMotions should be checked bool motionExecuted = false; if (checkFrom < m_keys.size()) { for (int i = 0; i < m_motions.size(); i++) { if (m_motions.at(i)->matches(m_keys.mid(checkFrom))) { m_lastMotionWasLinewiseInnerBlock = false; m_matchingMotions.push_back(i); // if it matches exact, we have found the motion command to execute if (m_motions.at(i)->matchesExact(m_keys.mid(checkFrom))) { m_currentMotionWasVisualLineUpOrDown = false; motionExecuted = true; if (checkFrom == 0) { // no command given before motion, just move the cursor to wherever // the motion says it should go to Range r = m_motions.at(i)->execute(); m_motionCanChangeWholeVisualModeSelection = m_motions.at(i)->canChangeWholeVisualModeSelection(); // jump over folding regions since we are just moving the cursor int currLine = m_view->cursorPosition().line(); int delta = r.endLine - currLine; int vline = m_view->textFolding().lineToVisibleLine(currLine); r.endLine = m_view->textFolding().visibleLineToLine(qMax(vline + delta, 0) /* ensure we have a valid line */); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } // make sure the position is valid before moving the cursor there // TODO: can this be simplified? :/ if (r.valid && r.endLine >= 0 && (r.endLine == 0 || r.endLine <= doc()->lines() - 1) && r.endColumn >= 0) { if (r.endColumn >= doc()->lineLength(r.endLine) && doc()->lineLength(r.endLine) > 0) { r.endColumn = doc()->lineLength(r.endLine) - 1; } goToPos(r); // in the case of VisualMode we need to remember the motion commands as well. if (!m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->clearCurrentChangeLog(); } } else { qCDebug(LOG_KTE) << "Invalid position: (" << r.endLine << "," << r.endColumn << ")"; } resetParser(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } m_lastMotionWasVisualLineUpOrDown = m_currentMotionWasVisualLineUpOrDown; break; } else { // execute the specified command and supply the position returned from // the motion m_commandRange = m_motions.at(i)->execute(); m_linewiseCommand = m_motions.at(i)->isLineWise(); // if we didn't get an explicit start position, use the current cursor position if (m_commandRange.startLine == -1) { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); } // special case: When using the "w" motion in combination with an operator and // the last word moved over is at the end of a line, the end of that word // becomes the end of the operated text, not the first word in the next line. if (m_motions.at(i)->pattern() == QLatin1String("w") || m_motions.at(i)->pattern() == QLatin1String("W")) { if (m_commandRange.endLine != m_commandRange.startLine && m_commandRange.endColumn == getFirstNonBlank(m_commandRange.endLine)) { m_commandRange.endLine--; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine); } } m_commandWithMotion = true; if (m_commandRange.valid) { executeCommand(m_commands.at(m_motionOperatorIndex)); } else { qCDebug(LOG_KTE) << "Invalid range: " << "from (" << m_commandRange.startLine << "," << m_commandRange.startColumn << ")" << "to (" << m_commandRange.endLine << "," << m_commandRange.endColumn << ")"; } if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } m_commandWithMotion = false; reset(); break; } } } } } if (this->waitingForRegisterOrCharToSearch()) { // If we are waiting for a char to search or a new register, // don't translate next character; we need the actual character so that e.g. // 'ab' is translated to 'fb' if the mappings 'a' -> 'f' and 'b' -> something else // exist. m_viInputModeManager->keyMapper()->setDoNotMapNextKeypress(); } if (motionExecuted) { return true; } // if we have only one match, check if it is a perfect match and if so, execute it // if it's not waiting for a motion or a text object if (m_matchingCommands.size() == 1) { if (m_commands.at(m_matchingCommands.at(0))->matchesExact(m_keys) && !m_commands.at(m_matchingCommands.at(0))->needsMotion()) { if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } Command *cmd = m_commands.at(m_matchingCommands.at(0)); executeCommand(cmd); // check if reset() should be called. some commands in visual mode should not end visual mode if (cmd->shouldReset()) { reset(); m_view->setBlockSelection(false); } resetParser(); return true; } } else if (m_matchingCommands.size() == 0 && m_matchingMotions.size() == 0) { resetParser(); // A bit ugly: we haven't made use of the key event, // but don't want "typeable" keypresses (e.g. a, b, 3, etc) to be marked // as unused as they will then be added to the document, but we don't // want to swallow all keys in case this was a shortcut. // So say we made use of it if and only if it was *not* a shortcut. return e->type() != QEvent::ShortcutOverride; } m_matchingMotions.clear(); return true; // TODO - need to check this - it's currently required for making tests pass, but seems odd. } /** * (re)set to start configuration. This is done when a command is completed * executed or when a command is aborted */ void NormalViMode::resetParser() { m_keys.clear(); m_keysVerbatim.clear(); m_count = 0; m_oneTimeCountOverride = -1; m_iscounted = false; m_countTemp = 0; m_register = QChar::Null; m_findWaitingForChar = false; m_matchingCommands.clear(); m_matchingMotions.clear(); m_awaitingMotionOrTextObject.clear(); m_motionOperatorIndex = 0; m_commandWithMotion = false; m_linewiseCommand = true; m_deleteCommand = false; m_commandShouldKeepSelection = false; m_currentChangeEndMarker = KTextEditor::Cursor::invalid(); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } } // reset the command parser void NormalViMode::reset() { resetParser(); m_commandRange.startLine = -1; m_commandRange.startColumn = -1; } void NormalViMode::beginMonitoringDocumentChanges() { connect(doc(), &KTextEditor::DocumentPrivate::textInserted, this, &NormalViMode::textInserted); connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &NormalViMode::textRemoved); } void NormalViMode::executeCommand(const Command *cmd) { const ViMode originalViMode = m_viInputModeManager->getCurrentViMode(); cmd->execute(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } // if the command was a change, and it didn't enter insert mode, store the key presses so that // they can be repeated with '.' if (m_viInputModeManager->getCurrentViMode() != ViMode::InsertMode && m_viInputModeManager->getCurrentViMode() != ViMode::ReplaceMode) { if (cmd->isChange() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { m_viInputModeManager->storeLastChangeCommand(); } // when we transition to visual mode, remember the command in the keys history (V, v, ctrl-v, ...) // this will later result in buffer filled with something like this "Vjj>" which we can use later with repeat "." const bool commandSwitchedToVisualMode = ((originalViMode == ViMode::NormalMode) && m_viInputModeManager->isAnyVisualMode()); if (!commandSwitchedToVisualMode) { m_viInputModeManager->clearCurrentChangeLog(); } } // make sure the cursor does not end up after the end of the line KTextEditor::Cursor c(m_view->cursorPosition()); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { int lineLength = doc()->lineLength(c.line()); if (c.column() >= lineLength) { if (lineLength == 0) { c.setColumn(0); } else { c.setColumn(lineLength - 1); } } updateCursor(c); } } //////////////////////////////////////////////////////////////////////////////// // COMMANDS AND OPERATORS //////////////////////////////////////////////////////////////////////////////// /** * enter insert mode at the cursor position */ bool NormalViMode::commandEnterInsertMode() { m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode after the current character */ bool NormalViMode::commandEnterInsertModeAppend() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(c.column() + 1); // if empty line, the cursor should start at column 0 if (doc()->lineLength(c.line()) == 0) { c.setColumn(0); } // cursor should never be in a column > number of columns if (c.column() > doc()->lineLength(c.line())) { c.setColumn(doc()->lineLength(c.line())); } updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * start insert mode after the last character of the line */ bool NormalViMode::commandEnterInsertModeAppendEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } bool NormalViMode::commandEnterInsertModeBeforeFirstNonBlankInLine() { KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); cursor.setColumn(c); updateCursor(cursor); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode at the last insert position */ bool NormalViMode::commandEnterInsertModeLast() { KTextEditor::Cursor c = m_viInputModeManager->marks()->getInsertStopped(); if (c.isValid()) { updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandEnterVisualLineMode() { if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { reset(); return true; } return startVisualLineMode(); } bool NormalViMode::commandEnterVisualBlockMode() { if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { reset(); return true; } return startVisualBlockMode(); } bool NormalViMode::commandReselectVisual() { // start last visual mode and set start = `< and cursor = `> KTextEditor::Cursor c1 = m_viInputModeManager->marks()->getSelectionStart(); KTextEditor::Cursor c2 = m_viInputModeManager->marks()->getSelectionFinish(); // we should either get two valid cursors or two invalid cursors Q_ASSERT(c1.isValid() == c2.isValid()); if (c1.isValid() && c2.isValid()) { m_viInputModeManager->getViVisualMode()->setStart(c1); bool returnValue = false; switch (m_viInputModeManager->getViVisualMode()->getLastVisualMode()) { - case ViMode::VisualMode: - returnValue = commandEnterVisualMode(); - break; - case ViMode::VisualLineMode: - returnValue = commandEnterVisualLineMode(); - break; - case ViMode::VisualBlockMode: - returnValue = commandEnterVisualBlockMode(); - break; - default: - Q_ASSERT("invalid visual mode"); + case ViMode::VisualMode: + returnValue = commandEnterVisualMode(); + break; + case ViMode::VisualLineMode: + returnValue = commandEnterVisualLineMode(); + break; + case ViMode::VisualBlockMode: + returnValue = commandEnterVisualBlockMode(); + break; + default: + Q_ASSERT("invalid visual mode"); } m_viInputModeManager->getViVisualMode()->goToPos(c2); return returnValue; } else { error(QStringLiteral("No previous visual selection")); } return false; } bool NormalViMode::commandEnterVisualMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode) { reset(); return true; } return startVisualMode(); } bool NormalViMode::commandToOtherEnd() { if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->switchStartEnd(); return true; } return false; } bool NormalViMode::commandEnterReplaceMode() { m_stickyColumn = -1; m_viInputModeManager->getViReplaceMode()->setCount(getCount()); return startReplaceMode(); } bool NormalViMode::commandDeleteLine() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r; r.startLine = c.line(); r.endLine = c.line() + getCount() - 1; int column = c.column(); bool ret = deleteRange(r, LineWise); c = m_view->cursorPosition(); if (column > doc()->lineLength(c.line()) - 1) { column = doc()->lineLength(c.line()) - 1; } if (column < 0) { column = 0; } if (c.line() > doc()->lines() - 1) { c.setLine(doc()->lines() - 1); } c.setColumn(column); m_stickyColumn = -1; updateCursor(c); m_deleteCommand = true; return ret; } bool NormalViMode::commandDelete() { m_deleteCommand = true; return deleteRange(m_commandRange, getOperationMode()); } bool NormalViMode::commandDeleteToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = CharWise; m_commandRange.endColumn = KateVi::EOL; switch (m_viInputModeManager->getCurrentViMode()) { - case ViMode::NormalMode: - m_commandRange.startLine = c.line(); - m_commandRange.startColumn = c.column(); - m_commandRange.endLine = c.line() + getCount() - 1; - break; - case ViMode::VisualMode: - case ViMode::VisualLineMode: - m = LineWise; - break; - case ViMode::VisualBlockMode: - m_commandRange.normalize(); - m = Block; - break; - default: - /* InsertMode and ReplaceMode will never call this method. */ - Q_ASSERT(false); + case ViMode::NormalMode: + m_commandRange.startLine = c.line(); + m_commandRange.startColumn = c.column(); + m_commandRange.endLine = c.line() + getCount() - 1; + break; + case ViMode::VisualMode: + case ViMode::VisualLineMode: + m = LineWise; + break; + case ViMode::VisualBlockMode: + m_commandRange.normalize(); + m = Block; + break; + default: + /* InsertMode and ReplaceMode will never call this method. */ + Q_ASSERT(false); } bool r = deleteRange(m_commandRange, m); switch (m) { - case CharWise: - c.setColumn(doc()->lineLength(c.line()) - 1); - break; - case LineWise: - c.setLine(m_commandRange.startLine); - c.setColumn(getFirstNonBlank(qMin(doc()->lastLine(), m_commandRange.startLine))); - break; - case Block: - c.setLine(m_commandRange.startLine); - c.setColumn(m_commandRange.startColumn - 1); - break; + case CharWise: + c.setColumn(doc()->lineLength(c.line()) - 1); + break; + case LineWise: + c.setLine(m_commandRange.startLine); + c.setColumn(getFirstNonBlank(qMin(doc()->lastLine(), m_commandRange.startLine))); + break; + case Block: + c.setLine(m_commandRange.startLine); + c.setColumn(m_commandRange.startColumn - 1); + break; } // make sure cursor position is valid after deletion if (c.line() < 0) { c.setLine(0); } if (c.line() > doc()->lastLine()) { c.setLine(doc()->lastLine()); } if (c.column() > doc()->lineLength(c.line()) - 1) { c.setColumn(doc()->lineLength(c.line()) - 1); } if (c.column() < 0) { c.setColumn(0); } updateCursor(c); m_deleteCommand = true; return r; } bool NormalViMode::commandMakeLowercase() { KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text.chop(1); // don't need '\n' at the end; } QString lowerCase = text.toLower(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, lowerCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeLowercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeLowercase(); } bool NormalViMode::commandMakeUppercase() { if (!m_commandRange.valid) { return false; } KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text.chop(1); // don't need '\n' at the end; } QString upperCase = text.toUpper(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, upperCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeUppercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeUppercase(); } bool NormalViMode::commandChangeCase() { switchView(); QString text; KTextEditor::Range range; KTextEditor::Cursor c(m_view->cursorPosition()); // in visual mode, the range is from start position to end position... if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode || m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(c2.column() + 1); } else { c.setColumn(c.column() + 1); } range.setRange(c, c2); // ... in visual line mode, the range is from column 0 on the first line to // the line length of the last line... } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(doc()->lineLength(c2.line())); c.setColumn(0); } else { c.setColumn(doc()->lineLength(c.line())); c2.setColumn(0); } range.setRange(c, c2); // ... and in normal mode the range is from the current position to the // current position + count } else { KTextEditor::Cursor c2 = c; c2.setColumn(c.column() + getCount()); if (c2.column() > doc()->lineLength(c.line())) { c2.setColumn(doc()->lineLength(c.line())); } range.setRange(c, c2); } bool block = m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode; // get the text the command should operate on text = doc()->text(range, block); // for every character, switch its case for (int i = 0; i < text.length(); i++) { if (text.at(i).isUpper()) { text[i] = text.at(i).toLower(); } else if (text.at(i).isLower()) { text[i] = text.at(i).toUpper(); } } // replace the old text with the modified text doc()->replaceText(range, text, block); // in normal mode, move the cursor to the right, in visual mode move the // cursor to the start of the selection if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(range.end()); } else { updateCursor(range.start()); } return true; } bool NormalViMode::commandChangeCaseRange() { OperationMode m = getOperationMode(); QString changedCase = getRange(m_commandRange, m); if (m == LineWise) { changedCase.chop(1); // don't need '\n' at the end; } KTextEditor::Range range = KTextEditor::Range(m_commandRange.startLine, m_commandRange.startColumn, m_commandRange.endLine, m_commandRange.endColumn); // get the text the command should operate on // for every character, switch its case for (int i = 0; i < changedCase.length(); i++) { if (changedCase.at(i).isUpper()) { changedCase[i] = changedCase.at(i).toLower(); } else if (changedCase.at(i).isLower()) { changedCase[i] = changedCase.at(i).toUpper(); } } doc()->replaceText(range, changedCase, m == Block); return true; } bool NormalViMode::commandChangeCaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; // -1 is for excluding '\0' if (!commandChangeCaseRange()) { return false; } KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); if (getCount() > 1) { updateCursor(c); } else { updateCursor(start); } return true; } bool NormalViMode::commandOpenNewLineUnder() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); doc()->newLine(m_view); m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandOpenNewLineOver() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() == 0) { doc()->insertLine(0, QString()); c.setColumn(0); c.setLine(0); updateCursor(c); } else { c.setLine(c.line() - 1); c.setColumn(getLine(c.line()).length()); updateCursor(c); doc()->newLine(m_view); } m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandJoinLines() { KTextEditor::Cursor c(m_view->cursorPosition()); unsigned int from = c.line(); unsigned int to = c.line() + ((getCount() == 1) ? 1 : getCount() - 1); // if we were given a range of lines, this information overrides the previous if (m_commandRange.startLine != -1 && m_commandRange.endLine != -1) { m_commandRange.normalize(); c.setLine(m_commandRange.startLine); from = m_commandRange.startLine; to = m_commandRange.endLine; } if (to >= (unsigned int)doc()->lines()) { return false; } bool nonEmptyLineFound = false; for (unsigned int lineNum = from; lineNum <= to; lineNum++) { if (!doc()->line(lineNum).isEmpty()) { nonEmptyLineFound = true; } } const int firstNonWhitespaceOnLastLine = doc()->kateTextLine(to)->firstChar(); QString leftTrimmedLastLine; if (firstNonWhitespaceOnLastLine != -1) { leftTrimmedLastLine = doc()->line(to).mid(firstNonWhitespaceOnLastLine); } joinLines(from, to); if (nonEmptyLineFound && leftTrimmedLastLine.isEmpty()) { // joinLines won't have added a trailing " ", whereas Vim does - follow suit. doc()->insertText(KTextEditor::Cursor(from, doc()->lineLength(from)), QStringLiteral(" ")); } // Position cursor just before first non-whitesspace character of what was the last line joined. c.setColumn(doc()->lineLength(from) - leftTrimmedLastLine.length() - 1); if (c.column() >= 0) { updateCursor(c); } m_deleteCommand = true; return true; } bool NormalViMode::commandChange() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = getOperationMode(); doc()->setUndoMergeAllEdits(true); commandDelete(); if (m == LineWise) { // if we deleted several lines, insert an empty line and put the cursor there. doc()->insertLine(m_commandRange.startLine, QString()); c.setLine(m_commandRange.startLine); c.setColumn(0); } else if (m == Block) { // block substitute can be simulated by first deleting the text // (done above) and then starting block prepend. return commandPrependToBlock(); } else { if (m_commandRange.startLine < m_commandRange.endLine) { c.setLine(m_commandRange.startLine); } c.setColumn(m_commandRange.startColumn); } updateCursor(c); setCount(0); // The count was for the motion, not the insertion. commandEnterInsertMode(); // correct indentation level if (m == LineWise) { m_view->align(); } m_deleteCommand = true; return true; } bool NormalViMode::commandChangeToEOL() { commandDeleteToEOL(); if (getOperationMode() == Block) { return commandPrependToBlock(); } m_deleteCommand = true; return commandEnterInsertModeAppend(); } bool NormalViMode::commandChangeLine() { m_deleteCommand = true; KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(0); updateCursor(c); doc()->setUndoMergeAllEdits(true); // if count >= 2 start by deleting the whole lines if (getCount() >= 2) { Range r(c.line(), 0, c.line() + getCount() - 2, 0, InclusiveMotion); deleteRange(r); } // ... then delete the _contents_ of the last line, but keep the line Range r(c.line(), c.column(), c.line(), doc()->lineLength(c.line()) - 1, InclusiveMotion); deleteRange(r, CharWise, true); // ... then enter insert mode if (getOperationMode() == Block) { return commandPrependToBlock(); } commandEnterInsertModeAppend(); // correct indentation level m_view->align(); return true; } bool NormalViMode::commandSubstituteChar() { if (commandDeleteChar()) { // The count is only used for deletion of chars; the inserted text is not repeated setCount(0); return commandEnterInsertMode(); } m_deleteCommand = true; return false; } bool NormalViMode::commandSubstituteLine() { m_deleteCommand = true; return commandChangeLine(); } bool NormalViMode::commandYank() { bool r = false; QString yankedText; OperationMode m = getOperationMode(); yankedText = getRange(m_commandRange, m); highlightYank(m_commandRange, m); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return r; } bool NormalViMode::commandYankLine() { KTextEditor::Cursor c(m_view->cursorPosition()); QString lines; int linenum = c.line(); for (int i = 0; i < getCount(); i++) { lines.append(getLine(linenum + i) + QLatin1Char('\n')); } Range yankRange(linenum, 0, linenum + getCount() - 1, getLine(linenum + getCount() - 1).length(), InclusiveMotion); highlightYank(yankRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, lines, LineWise); yankToClipBoard(chosen_register, lines); return true; } bool NormalViMode::commandYankToEOL() { OperationMode m = CharWise; KTextEditor::Cursor c(m_view->cursorPosition()); MotionType motion = m_commandRange.motionType; m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine) - 1; m_commandRange.motionType = InclusiveMotion; switch (m_viInputModeManager->getCurrentViMode()) { - case ViMode::NormalMode: - m_commandRange.startLine = c.line(); - m_commandRange.startColumn = c.column(); - break; - case ViMode::VisualMode: - case ViMode::VisualLineMode: - m = LineWise; - { - VisualViMode *visual = static_cast(this); - visual->setStart(KTextEditor::Cursor(visual->getStart().line(), 0)); - } - break; - case ViMode::VisualBlockMode: - m = Block; - break; - default: - /* InsertMode and ReplaceMode will never call this method. */ - Q_ASSERT(false); + case ViMode::NormalMode: + m_commandRange.startLine = c.line(); + m_commandRange.startColumn = c.column(); + break; + case ViMode::VisualMode: + case ViMode::VisualLineMode: + m = LineWise; + { + VisualViMode *visual = static_cast(this); + visual->setStart(KTextEditor::Cursor(visual->getStart().line(), 0)); + } + break; + case ViMode::VisualBlockMode: + m = Block; + break; + default: + /* InsertMode and ReplaceMode will never call this method. */ + Q_ASSERT(false); } const QString &yankedText = getRange(m_commandRange, m); m_commandRange.motionType = motion; highlightYank(m_commandRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return true; } // Insert the text in the given register after the cursor position. // This is the non-g version of paste, so the cursor will usually // end up on the last character of the pasted text, unless the text // was multi-line or linewise in which case it will end up // on the *first* character of the pasted text(!) // If linewise, will paste after the current line. bool NormalViMode::commandPaste() { return paste(AfterCurrentPosition, false, false); } // As with commandPaste, except that the text is pasted *at* the cursor position bool NormalViMode::commandPasteBefore() { return paste(AtCurrentPosition, false, false); } // As with commandPaste, except that the cursor will generally be placed *after* the // last pasted character (assuming the last pasted character is not at the end of the line). // If linewise, cursor will be at the beginning of the line *after* the last line of pasted text, // unless that line is the last line of the document; then it will be placed at the beginning of the // last line pasted. bool NormalViMode::commandgPaste() { return paste(AfterCurrentPosition, true, false); } // As with commandgPaste, except that it pastes *at* the current cursor position or, if linewise, // at the current line. bool NormalViMode::commandgPasteBefore() { return paste(AtCurrentPosition, true, false); } bool NormalViMode::commandIndentedPaste() { return paste(AfterCurrentPosition, false, true); } bool NormalViMode::commandIndentedPasteBefore() { return paste(AtCurrentPosition, false, true); } bool NormalViMode::commandDeleteChar() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column(), c.line(), c.column() + getCount(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.endColumn > doc()->lineLength(r.startLine)) { r.endColumn = doc()->lineLength(r.startLine); } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandDeleteCharBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column() - getCount(), c.line(), c.column(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.startColumn < 0) { r.startColumn = 0; } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandReplaceCharacter() { QString key = KeyParser::self()->decodeKeySequence(m_keys.right(1)); // Filter out some special keys. const int keyCode = KeyParser::self()->encoded2qt(m_keys.right(1)); switch (keyCode) { - case Qt::Key_Left: - case Qt::Key_Right: - case Qt::Key_Up: - case Qt::Key_Down: - case Qt::Key_Home: - case Qt::Key_End: - case Qt::Key_PageUp: - case Qt::Key_PageDown: - case Qt::Key_Delete: - case Qt::Key_Insert: - case Qt::Key_Backspace: - case Qt::Key_CapsLock: - return true; - case Qt::Key_Return: - case Qt::Key_Enter: - key = QStringLiteral("\n"); + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Delete: + case Qt::Key_Insert: + case Qt::Key_Backspace: + case Qt::Key_CapsLock: + return true; + case Qt::Key_Return: + case Qt::Key_Enter: + key = QStringLiteral("\n"); } bool r; if (m_viInputModeManager->isAnyVisualMode()) { OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text.chop(1); // don't need '\n' at the end; } text.replace(QRegExp(QLatin1String("[^\n]")), key); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); r = doc()->replaceText(range, text, m == Block); } else { KTextEditor::Cursor c1(m_view->cursorPosition()); KTextEditor::Cursor c2(m_view->cursorPosition()); c2.setColumn(c2.column() + getCount()); if (c2.column() > doc()->lineLength(m_view->cursorPosition().line())) { return false; } r = doc()->replaceText(KTextEditor::Range(c1, c2), key.repeated(getCount())); updateCursor(c1); } return r; } bool NormalViMode::commandSwitchToCmdLine() { QString initialText; if (m_viInputModeManager->isAnyVisualMode()) { // if in visual mode, make command range == visual selection m_viInputModeManager->getViVisualMode()->saveRangeMarks(); initialText = QStringLiteral("'<,'>"); } else if (getCount() != 1) { // if a count is given, the range [current line] to [current line] + // count should be prepended to the command line initialText = QLatin1String(".,.+") + QString::number(getCount() - 1); } m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::Command, initialText); m_commandShouldKeepSelection = true; return true; } bool NormalViMode::commandSearchBackward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchBackward); return true; } bool NormalViMode::commandSearchForward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchForward); return true; } bool NormalViMode::commandUndo() { // See BUG #328277 m_viInputModeManager->clearCurrentChangeLog(); if (doc()->undoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) doc()->editEnd(); doc()->undo(); if (mapped) doc()->editBegin(); if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandRedo() { if (doc()->redoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) doc()->editEnd(); doc()->redo(); if (mapped) doc()->editBegin(); if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandSetMark() { KTextEditor::Cursor c(m_view->cursorPosition()); QChar mark = m_keys.at(m_keys.size() - 1); m_viInputModeManager->marks()->setUserMark(mark, c); return true; } bool NormalViMode::commandIndentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), 1); return true; } bool NormalViMode::commandUnindentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), -1); return true; } bool NormalViMode::commandIndentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; int col = getLine(line2).length(); doc()->indent(KTextEditor::Range(line1, 0, line2, col), getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandUnindentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; doc()->indent(KTextEditor::Range(line1, 0, line2, doc()->lineLength(line2)), -getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandScrollPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageDown(); } } return true; } bool NormalViMode::commandScrollPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageUp(); } } return true; } bool NormalViMode::commandScrollHalfPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageUp(false, true); } } return true; } bool NormalViMode::commandScrollHalfPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageDown(false, true); } } return true; } bool NormalViMode::commandCenterView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine() + linesDisplayed() / 2; const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandCenterViewOnNonBlank() { return commandCenterView(true); } bool NormalViMode::commandCenterViewOnCursor() { return commandCenterView(false); } bool NormalViMode::commandTopView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandTopViewOnNonBlank() { return commandTopView(true); } bool NormalViMode::commandTopViewOnCursor() { return commandTopView(false); } bool NormalViMode::commandBottomView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->endLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandBottomViewOnNonBlank() { return commandBottomView(true); } bool NormalViMode::commandBottomViewOnCursor() { return commandBottomView(false); } bool NormalViMode::commandAbort() { m_pendingResetIsDueToExit = true; reset(); return true; } bool NormalViMode::commandPrintCharacterCode() { QChar ch = getCharUnderCursor(); if (ch == QChar::Null) { message(QStringLiteral("NUL")); } else { int code = ch.unicode(); QString dec = QString::number(code); QString hex = QString::number(code, 16); QString oct = QString::number(code, 8); if (oct.length() < 3) { oct.prepend(QLatin1Char('0')); } if (code > 0x80 && code < 0x1000) { hex.prepend((code < 0x100 ? QLatin1String("00") : QLatin1String("0"))); } message(i18n("'%1' %2, Hex %3, Octal %4", ch, dec, hex, oct)); } return true; } bool NormalViMode::commandRepeatLastChange() { const int repeatCount = getCount(); resetParser(); if (repeatCount > 1) { m_oneTimeCountOverride = repeatCount; } doc()->editStart(); m_viInputModeManager->repeatLastChange(); doc()->editEnd(); return true; } bool NormalViMode::commandAlignLine() { const int line = m_view->cursorPosition().line(); KTextEditor::Range alignRange(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, 0)); doc()->align(m_view, alignRange); return true; } bool NormalViMode::commandAlignLines() { m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, 0); KTextEditor::Cursor end(m_commandRange.endLine, 0); doc()->align(m_view, KTextEditor::Range(start, end)); return true; } bool NormalViMode::commandAddToNumber() { addToNumberUnderCursor(getCount()); return true; } bool NormalViMode::commandSubtractFromNumber() { addToNumberUnderCursor(-getCount()); return true; } bool NormalViMode::commandPrependToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); // move cursor to top left corner of selection m_commandRange.normalize(); c.setColumn(m_commandRange.startColumn); c.setLine(m_commandRange.startLine); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setBlockPrependMode(m_commandRange); return startInsertMode(); } bool NormalViMode::commandAppendToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.normalize(); if (m_stickyColumn == (unsigned int)KateVi::EOL) { // append to EOL // move cursor to end of first line c.setLine(m_commandRange.startLine); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, AppendEOL); } else { m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, Append); // move cursor to top right corner of selection c.setColumn(m_commandRange.endColumn + 1); c.setLine(m_commandRange.startLine); updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandGoToNextJump() { KTextEditor::Cursor c = getNextJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandGoToPrevJump() { KTextEditor::Cursor c = getPrevJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandSwitchToLeftView() { switchView(Left); return true; } bool NormalViMode::commandSwitchToDownView() { switchView(Down); return true; } bool NormalViMode::commandSwitchToUpView() { switchView(Up); return true; } bool NormalViMode::commandSwitchToRightView() { switchView(Right); return true; } bool NormalViMode::commandSwitchToNextView() { switchView(Next); return true; } bool NormalViMode::commandSplitHoriz() { return executeKateCommand(QStringLiteral("split")); } bool NormalViMode::commandSplitVert() { return executeKateCommand(QStringLiteral("vsplit")); } bool NormalViMode::commandCloseView() { return executeKateCommand(QStringLiteral("close")); } bool NormalViMode::commandSwitchToNextTab() { QString command = QStringLiteral("bn"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandSwitchToPrevTab() { QString command = QStringLiteral("bp"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandFormatLine() { KTextEditor::Cursor c(m_view->cursorPosition()); reformatLines(c.line(), c.line() + getCount() - 1); return true; } bool NormalViMode::commandFormatLines() { reformatLines(m_commandRange.startLine, m_commandRange.endLine); return true; } bool NormalViMode::commandCollapseToplevelNodes() { #if 0 //FIXME FOLDING doc()->foldingTree()->collapseToplevelNodes(); #endif return true; } bool NormalViMode::commandStartRecordingMacro() { const QChar reg = m_keys[m_keys.size() - 1]; m_viInputModeManager->macroRecorder()->start(reg); return true; } bool NormalViMode::commandReplayMacro() { // "@" will have been added to the log; it needs to be cleared // *before* we replay the macro keypresses, else it can cause an infinite loop // if the macro contains a "." m_viInputModeManager->clearCurrentChangeLog(); const QChar reg = m_keys[m_keys.size() - 1]; const unsigned int count = getCount(); resetParser(); doc()->editBegin(); for (unsigned int i = 0; i < count; i++) { m_viInputModeManager->macroRecorder()->replay(reg); } doc()->editEnd(); return true; } bool NormalViMode::commandCloseNocheck() { return executeKateCommand(QStringLiteral("q!")); } bool NormalViMode::commandCloseWrite() { return executeKateCommand(QStringLiteral("wq")); } bool NormalViMode::commandCollapseLocal() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->collapseOne(c.line(), c.column()); #endif return true; } bool NormalViMode::commandExpandAll() { #if 0 //FIXME FOLDING doc()->foldingTree()->expandAll(); #endif return true; } bool NormalViMode::commandExpandLocal() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->expandOne(c.line() + 1, c.column()); #endif return true; } bool NormalViMode::commandToggleRegionVisibility() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->toggleRegionVisibility(c.line()); #endif return true; } //////////////////////////////////////////////////////////////////////////////// // MOTIONS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::motionDown() { return goLineDown(); } Range NormalViMode::motionUp() { return goLineUp(); } Range NormalViMode::motionLeft() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn -= getCount(); if (r.endColumn < 0) { r.endColumn = 0; } return r; } Range NormalViMode::motionRight() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn += getCount(); // make sure end position isn't > line length if (r.endColumn > doc()->lineLength(r.endLine)) { r.endColumn = doc()->lineLength(r.endLine); } return r; } Range NormalViMode::motionPageDown() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine += linesDisplayed(); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } return r; } Range NormalViMode::motionPageUp() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine -= linesDisplayed(); if (r.endLine < 0) { r.endLine = 0; } return r; } Range NormalViMode::motionHalfPageDown() { if (commandScrollHalfPageDown()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionHalfPageUp() { if (commandScrollHalfPageUp()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionDownToFirstNonBlank() { Range r = goLineDown(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionUpToFirstNonBlank() { Range r = goLineUp(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionWordForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; // Special case: If we're already on the very last character in the document, the motion should be // inclusive so the last character gets included if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { r.motionType = InclusiveMotion; } else { for (int i = 0; i < getCount(); i++) { c = findNextWordStart(c.line(), c.column()); // stop when at the last char in the document if (!c.isValid()) { c = doc()->documentEnd(); // if we still haven't "used up the count", make the motion inclusive, so that the last char // is included if (i < getCount()) { r.motionType = InclusiveMotion; } break; } } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWordBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findNextWORDStart(c.line(), c.column()); // stop when at the last char in the document if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWordEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWORDEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfPrevWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEndOfPrevWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); // set sticky column to a ridiculously high value so that the cursor will stick to EOL, // but only if it's a regular motion if (m_keys.size() == 1) { m_stickyColumn = KateVi::EOL; } unsigned int line = c.line() + (getCount() - 1); Range r(line, doc()->lineLength(line) - 1, InclusiveMotion); return r; } Range NormalViMode::motionToColumn0() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); Range r(cursor.line(), 0, ExclusiveMotion); return r; } Range NormalViMode::motionToFirstCharacterOfLine() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); Range r(cursor.line(), c, ExclusiveMotion); return r; } Range NormalViMode::motionFindChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = cursor.column(); for (int i = 0; i < getCount(); i++) { matchColumn = line.indexOf(m_keys.rightRef(1), matchColumn + 1); if (matchColumn == -1) { break; } } Range r; if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionFindCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = -1; int hits = 0; int i = cursor.column() - 1; while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } Range r(cursor, ExclusiveMotion); if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionToChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; Range r; r.endColumn = -1; r.endLine = -1; int matchColumn = cursor.column() + (m_isRepeatedTFcommand ? 2 : 1); for (int i = 0; i < getCount(); i++) { const int lastColumn = matchColumn; matchColumn = line.indexOf(m_keys.right(1), matchColumn + ((i > 0) ? 1 : 0)); if (matchColumn == -1) { if (m_isRepeatedTFcommand) { matchColumn = lastColumn; } else { return Range::invalid(); } break; } } r.endColumn = matchColumn - 1; r.endLine = cursor.line(); m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionToCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); const int originalColumn = cursor.column(); m_stickyColumn = -1; int matchColumn = originalColumn - 1; int hits = 0; int i = cursor.column() - (m_isRepeatedTFcommand ? 2 : 1); Range r(cursor, ExclusiveMotion); while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } if (hits == getCount()) { r.endColumn = matchColumn + 1; r.endLine = cursor.line(); } else { r.valid = false; } m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionRepeatlastTF() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToChar(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToCharBackward(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionRepeatlastTFBackward() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToCharBackward(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToChar(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionToLineFirst() { Range r(getCount() - 1, 0, InclusiveMotion); m_stickyColumn = -1; if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToLineLast() { Range r(doc()->lines() - 1, 0, InclusiveMotion); m_stickyColumn = -1; // don't use getCount() here, no count and a count of 1 is different here... if (m_count != 0) { r.endLine = m_count - 1; } if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToScreenColumn() { m_stickyColumn = -1; KTextEditor::Cursor c(m_view->cursorPosition()); int column = getCount() - 1; if (doc()->lineLength(c.line()) - 1 < (int)getCount() - 1) { column = doc()->lineLength(c.line()) - 1; } return Range(c.line(), column, ExclusiveMotion); } Range NormalViMode::motionToMark() { Range r; m_stickyColumn = -1; QChar reg = m_keys.at(m_keys.size() - 1); KTextEditor::Cursor c = m_viInputModeManager->marks()->getMarkPosition(reg); if (c.isValid()) { r.endLine = c.line(); r.endColumn = c.column(); } else { error(i18n("Mark not set: %1", m_keys.right(1))); r.valid = false; } r.jump = true; return r; } Range NormalViMode::motionToMarkLine() { Range r = motionToMark(); r.endColumn = getFirstNonBlank(r.endLine); r.jump = true; m_stickyColumn = -1; return r; } Range NormalViMode::motionToMatchingItem() { Range r; int lines = doc()->lines(); // If counted, then it's not a motion to matching item anymore, // but a motion to the N'th percentage of the document if (isCounted()) { int count = getCount(); if (count > 100) { return r; } r.endLine = qRound(lines * count / 100.0) - 1; r.endColumn = 0; return r; } KTextEditor::Cursor c(m_view->cursorPosition()); QString l = getLine(); int n1 = l.indexOf(m_matchItemRegex, c.column()); m_stickyColumn = -1; if (n1 < 0) { return Range::invalid(); } QRegExp brackets(QLatin1String("[(){}\\[\\]]")); // use Kate's built-in matching bracket finder for brackets if (brackets.indexIn(l, n1) == n1) { // findMatchingBracket requires us to move the cursor to the // first bracket, but we don't want the cursor to really move // in case this is e.g. a yank, so restore it to its original // position afterwards. c.setColumn(n1 + 1); const KTextEditor::Cursor oldCursorPos = m_view->cursorPosition(); updateCursor(c); // find the matching one c = m_viewInternal->findMatchingBracket(); if (c > m_view->cursorPosition()) { c.setColumn(c.column() - 1); } m_view->setCursorPosition(oldCursorPos); } else { // text item we want to find a matching item for int n2 = l.indexOf(QRegExp(QLatin1String("\\b|\\s|$")), n1); QString item = l.mid(n1, n2 - n1); QString matchingItem = m_matchingItems[item]; int toFind = 1; int line = c.line(); int column = n2 - item.length(); bool reverse = false; if (matchingItem.startsWith(QLatin1Char('-'))) { matchingItem.remove(0, 1); // remove the '-' reverse = true; } // make sure we don't hit the text item we started the search from if (column == 0 && reverse) { column -= item.length(); } int itemIdx; int matchItemIdx; while (toFind > 0) { if (reverse) { itemIdx = l.lastIndexOf(item, column - 1); matchItemIdx = l.lastIndexOf(matchingItem, column - 1); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx > matchItemIdx)) { ++toFind; } } else { itemIdx = l.indexOf(item, column); matchItemIdx = l.indexOf(matchingItem, column); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx < matchItemIdx)) { ++toFind; } } if (matchItemIdx != -1 || itemIdx != -1) { if (!reverse) { column = qMin((unsigned int)itemIdx, (unsigned int)matchItemIdx); } else { column = qMax(itemIdx, matchItemIdx); } } if (matchItemIdx != -1) { // match on current line if (matchItemIdx == column) { --toFind; c.setLine(line); c.setColumn(column); } } else { // no match, advance one line if possible (reverse) ? --line : ++line; column = 0; if ((!reverse && line >= lines) || (reverse && line < 0)) { r.valid = false; break; } else { l = getLine(line); } } } } r.endLine = c.line(); r.endColumn = c.column(); r.jump = true; return r; } Range NormalViMode::motionToNextBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '{' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // With a command, do not include the { or the cursor position. r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '}' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextOccurrence() { const QString word = getWordUnderCursor(); const Range match = m_viInputModeManager->searcher()->findWordForMotion(word, false, getWordRangeUnderCursor().start(), getCount()); return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToPrevOccurrence() { const QString word = getWordUnderCursor(); const Range match = m_viInputModeManager->searcher()->findWordForMotion(word, true, getWordRangeUnderCursor().start(), getCount()); return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToFirstLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int)m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() - m_view->cursorPosition().line() + 1; } else { lines_to_go = -m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToMiddleLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int)m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() / 2 - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() / 2 - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToLastLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int)m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToNextVisualLine() { return goVisualLineUpDown(getCount()); } Range NormalViMode::motionToPrevVisualLine() { return goVisualLineUpDown(-getCount()); } Range NormalViMode::motionToPreviousSentence() { KTextEditor::Cursor c = findSentenceStart(); int linenum = c.line(), column; const bool skipSpaces = doc()->line(linenum).isEmpty(); if (skipSpaces) { linenum--; if (linenum >= 0) { column = doc()->line(linenum).size() - 1; } } else { column = c.column(); } for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } if (column < 0 && !line.isEmpty()) { column = line.size() - 1; } for (int j = column; j >= 0; j--) { if (skipSpaces || QStringLiteral(".?!").indexOf(line.at(j)) != -1) { c.setLine(i); c.setColumn(j); updateCursor(c); c = findSentenceStart(); return Range(c, InclusiveMotion); } } column = line.size() - 1; } return Range(0, 0, InclusiveMotion); } Range NormalViMode::motionToNextSentence() { KTextEditor::Cursor c = findSentenceEnd(); int linenum = c.line(), column = c.column() + 1; const bool skipSpaces = doc()->line(linenum).isEmpty(); for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } for (int j = column; j < line.size(); j++) { if (!line.at(j).isSpace()) { return Range(i, j, InclusiveMotion); } } column = 0; } c = doc()->documentEnd(); return Range(c, InclusiveMotion); } Range NormalViMode::motionToBeforeParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line--; } while (line >= 0 && getLine(line + 1).length() == 0); while (line > 0 && getLine(line).length() != 0) { line--; } } if (line < 0) { line = 0; } Range r(line, 0, InclusiveMotion); return r; } Range NormalViMode::motionToAfterParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line++; } while (line <= doc()->lines() - 1 && getLine(line - 1).length() == 0); while (line < doc()->lines() - 1 && getLine(line).length() != 0) { line++; } } if (line >= doc()->lines()) { line = doc()->lines() - 1; } // if we ended up on the last line, the cursor should be placed on the last column int column = (line == doc()->lines() - 1) ? qMax(getLine(line).length() - 1, 0) : 0; return Range(line, column, InclusiveMotion); } Range NormalViMode::motionToIncrementalSearchMatch() { return Range(m_positionWhenIncrementalSearchBegan.line(), m_positionWhenIncrementalSearchBegan.column(), m_view->cursorPosition().line(), m_view->cursorPosition().column(), ExclusiveMotion); } //////////////////////////////////////////////////////////////////////////////// // TEXT OBJECTS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::textObjectAWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWordEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } // need to start search in column-1 because it might be a one-character word KTextEditor::Cursor c2(c.line(), c.column() - 1); for (int i = 0; i < getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } Range NormalViMode::textObjectAWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWORDEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } KTextEditor::Cursor c2(c); for (int i = 0; i < getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } KTextEditor::Cursor NormalViMode::findSentenceStart() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(), column = c.column(); int prev = column; for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (i != linenum) { column = line.size() - 1; } // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor((i != linenum) ? i + 1 : i, prev); } prev = column; for (int j = column; j >= 0; j--) { if (line.at(j).isSpace()) { int lastSpace = j--; for (; j >= 0 && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j--) ; if (j >= 0 && QStringLiteral(".!?").indexOf(line.at(j)) != -1) { return KTextEditor::Cursor(i, prev); } j = lastSpace; } else prev = j; } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findSentenceEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(), column = c.column(); int j = 0, prev = 0; for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor(linenum, j); } // Iterating over the line to reach any '.', '!', '?' for (j = column; j < line.size(); j++) { if (QStringLiteral(".!?").indexOf(line.at(j)) != -1) { prev = j++; // Skip possible closing characters. for (; j < line.size() && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j++) ; if (j >= line.size()) { return KTextEditor::Cursor(i, j - 1); } // And hopefully we're done... if (line.at(j).isSpace()) { return KTextEditor::Cursor(i, j - 1); } j = prev; } } linenum = i; prev = column; column = 0; } return KTextEditor::Cursor(linenum, j - 1); } KTextEditor::Cursor NormalViMode::findParagraphStart() { KTextEditor::Cursor c(m_view->cursorPosition()); const bool firstBlank = doc()->line(c.line()).isEmpty(); int prev = c.line(); for (int i = prev; i >= 0; i--) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i + 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i--; for (; i >= 0 && doc()->line(i).isEmpty(); i--, prev--) ; } return KTextEditor::Cursor(prev, 0); } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findParagraphEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int prev = c.line(), lines = doc()->lines(); const bool firstBlank = doc()->line(prev).isEmpty(); for (int i = prev; i < lines; i++) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i - 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i++; for (; i < lines && doc()->line(i).isEmpty(); i++, prev++) ; } int length = doc()->lineLength(prev); return KTextEditor::Cursor(prev, (length <= 0) ? 0 : length - 1); } } return doc()->documentEnd(); } Range NormalViMode::textObjectInnerSentence() { Range r; KTextEditor::Cursor c1 = findSentenceStart(); KTextEditor::Cursor c2 = findSentenceEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectASentence() { int i; Range r = textObjectInnerSentence(); const QString &line = doc()->line(r.endLine); // Skip whitespaces and tabs. for (i = r.endColumn + 1; i < line.size(); i++) { if (!line.at(i).isSpace()) { break; } } r.endColumn = i - 1; // Remove preceding spaces. if (r.startColumn != 0) { if (r.endColumn == line.size() - 1 && !line.at(r.endColumn).isSpace()) { const QString &line = doc()->line(r.startLine); for (i = r.startColumn - 1; i >= 0; i--) { if (!line.at(i).isSpace()) { break; } } r.startColumn = i + 1; } } return r; } Range NormalViMode::textObjectInnerParagraph() { Range r; KTextEditor::Cursor c1 = findParagraphStart(); KTextEditor::Cursor c2 = findParagraphEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectAParagraph() { Range r = textObjectInnerParagraph(); int lines = doc()->lines(); if (r.endLine + 1 < lines) { // If the next line is empty, remove all subsequent empty lines. // Otherwise we'll grab the next paragraph. if (doc()->line(r.endLine + 1).isEmpty()) { for (int i = r.endLine + 1; i < lines && doc()->line(i).isEmpty(); i++) { r.endLine++; } r.endColumn = 0; } else { KTextEditor::Cursor prev = m_view->cursorPosition(); KTextEditor::Cursor c(r.endLine + 1, 0); updateCursor(c); c = findParagraphEnd(); updateCursor(prev); r.endLine = c.line(); r.endColumn = c.column(); } } else if (doc()->lineLength(r.startLine) > 0) { // We went too far, but maybe we can grab previous empty lines. for (int i = r.startLine - 1; i >= 0 && doc()->line(i).isEmpty(); i--) { r.startLine--; } r.startColumn = 0; updateCursor(KTextEditor::Cursor(r.startLine, r.startColumn)); } else { // We went too far and we're on empty lines, do nothing. return Range::invalid(); } return r; } Range NormalViMode::textObjectAQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), false); } Range NormalViMode::textObjectInnerQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), true); } Range NormalViMode::textObjectAQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), false); } Range NormalViMode::textObjectInnerQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), true); } Range NormalViMode::textObjectABackQuote() { return findSurroundingQuotes(QLatin1Char('`'), false); } Range NormalViMode::textObjectInnerBackQuote() { return findSurroundingQuotes(QLatin1Char('`'), true); } Range NormalViMode::textObjectAParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), false, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectInnerParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), true, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectABracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), false, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectInnerBracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), true, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectACurlyBracket() { return findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), false, QLatin1Char('{'), QLatin1Char('}')); } Range NormalViMode::textObjectInnerCurlyBracket() { const Range allBetweenCurlyBrackets = findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), true, QLatin1Char('{'), QLatin1Char('}')); // Emulate the behaviour of vim, which tries to leave the closing bracket on its own line // if it was originally on a line different to that of the opening bracket. Range innerCurlyBracket(allBetweenCurlyBrackets); if (innerCurlyBracket.startLine != innerCurlyBracket.endLine) { const bool openingBraceIsLastCharOnLine = innerCurlyBracket.startColumn == doc()->line(innerCurlyBracket.startLine).length(); const bool stuffToDeleteIsAllOnEndLine = openingBraceIsLastCharOnLine && innerCurlyBracket.endLine == innerCurlyBracket.startLine + 1; const QString textLeadingClosingBracket = doc()->line(innerCurlyBracket.endLine).mid(0, innerCurlyBracket.endColumn + 1); const bool closingBracketHasLeadingNonWhitespace = !textLeadingClosingBracket.trimmed().isEmpty(); if (stuffToDeleteIsAllOnEndLine) { if (!closingBracketHasLeadingNonWhitespace) { // Nothing there to select - abort. return Range::invalid(); } else { // Shift the beginning of the range to the start of the line containing the closing bracket. innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; } } else { if (openingBraceIsLastCharOnLine && !closingBracketHasLeadingNonWhitespace) { innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; m_lastMotionWasLinewiseInnerBlock = true; } { // The line containing the end bracket is left alone if the end bracket is preceded by just whitespace, // else we need to delete everything (i.e. end up with "{}") if (!closingBracketHasLeadingNonWhitespace) { // Shrink the endpoint of the range so that it ends at the end of the line above, // leaving the closing bracket on its own line. innerCurlyBracket.endLine--; innerCurlyBracket.endColumn = doc()->line(innerCurlyBracket.endLine).length(); } } } } return innerCurlyBracket; } Range NormalViMode::textObjectAInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), false, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectInnerInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), true, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectAComma() { return textObjectComma(false); } Range NormalViMode::textObjectInnerComma() { return textObjectComma(true); } // add commands // when adding commands here, remember to add them to visual mode too (if applicable) void NormalViMode::initializeCommands() { ADDCMD("a", commandEnterInsertModeAppend, IS_CHANGE); ADDCMD("A", commandEnterInsertModeAppendEOL, IS_CHANGE); ADDCMD("i", commandEnterInsertMode, IS_CHANGE); ADDCMD("", commandEnterInsertMode, IS_CHANGE); ADDCMD("I", commandEnterInsertModeBeforeFirstNonBlankInLine, IS_CHANGE); ADDCMD("gi", commandEnterInsertModeLast, IS_CHANGE); ADDCMD("v", commandEnterVisualMode, 0); ADDCMD("V", commandEnterVisualLineMode, 0); ADDCMD("", commandEnterVisualBlockMode, 0); ADDCMD("gv", commandReselectVisual, SHOULD_NOT_RESET); ADDCMD("o", commandOpenNewLineUnder, IS_CHANGE); ADDCMD("O", commandOpenNewLineOver, IS_CHANGE); ADDCMD("J", commandJoinLines, IS_CHANGE); ADDCMD("c", commandChange, IS_CHANGE | NEEDS_MOTION); ADDCMD("C", commandChangeToEOL, IS_CHANGE); ADDCMD("cc", commandChangeLine, IS_CHANGE); ADDCMD("s", commandSubstituteChar, IS_CHANGE); ADDCMD("S", commandSubstituteLine, IS_CHANGE); ADDCMD("dd", commandDeleteLine, IS_CHANGE); ADDCMD("d", commandDelete, IS_CHANGE | NEEDS_MOTION); ADDCMD("D", commandDeleteToEOL, IS_CHANGE); ADDCMD("x", commandDeleteChar, IS_CHANGE); ADDCMD("", commandDeleteChar, IS_CHANGE); ADDCMD("X", commandDeleteCharBackward, IS_CHANGE); ADDCMD("gu", commandMakeLowercase, IS_CHANGE | NEEDS_MOTION); ADDCMD("guu", commandMakeLowercaseLine, IS_CHANGE); ADDCMD("gU", commandMakeUppercase, IS_CHANGE | NEEDS_MOTION); ADDCMD("gUU", commandMakeUppercaseLine, IS_CHANGE); ADDCMD("y", commandYank, NEEDS_MOTION); ADDCMD("yy", commandYankLine, 0); ADDCMD("Y", commandYankToEOL, 0); ADDCMD("p", commandPaste, IS_CHANGE); ADDCMD("P", commandPasteBefore, IS_CHANGE); ADDCMD("gp", commandgPaste, IS_CHANGE); ADDCMD("gP", commandgPasteBefore, IS_CHANGE); ADDCMD("]p", commandIndentedPaste, IS_CHANGE); ADDCMD("[p", commandIndentedPasteBefore, IS_CHANGE); ADDCMD("r.", commandReplaceCharacter, IS_CHANGE | REGEX_PATTERN); ADDCMD("R", commandEnterReplaceMode, IS_CHANGE); ADDCMD(":", commandSwitchToCmdLine, 0); ADDCMD("u", commandUndo, 0); ADDCMD("", commandRedo, 0); ADDCMD("U", commandRedo, 0); ADDCMD("m.", commandSetMark, REGEX_PATTERN); ADDCMD(">>", commandIndentLine, IS_CHANGE); ADDCMD("<<", commandUnindentLine, IS_CHANGE); ADDCMD(">", commandIndentLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("<", commandUnindentLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("", commandScrollPageDown, 0); ADDCMD("", commandScrollPageDown, 0); ADDCMD("", commandScrollPageUp, 0); ADDCMD("", commandScrollPageUp, 0); ADDCMD("", commandScrollHalfPageUp, 0); ADDCMD("", commandScrollHalfPageDown, 0); ADDCMD("z.", commandCenterViewOnNonBlank, 0); ADDCMD("zz", commandCenterViewOnCursor, 0); ADDCMD("z", commandTopViewOnNonBlank, 0); ADDCMD("zt", commandTopViewOnCursor, 0); ADDCMD("z-", commandBottomViewOnNonBlank, 0); ADDCMD("zb", commandBottomViewOnCursor, 0); ADDCMD("ga", commandPrintCharacterCode, SHOULD_NOT_RESET); ADDCMD(".", commandRepeatLastChange, 0); ADDCMD("==", commandAlignLine, IS_CHANGE); ADDCMD("=", commandAlignLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("~", commandChangeCase, IS_CHANGE); ADDCMD("g~", commandChangeCaseRange, IS_CHANGE | NEEDS_MOTION); ADDCMD("g~~", commandChangeCaseLine, IS_CHANGE); ADDCMD("", commandAddToNumber, IS_CHANGE); ADDCMD("", commandSubtractFromNumber, IS_CHANGE); ADDCMD("", commandGoToPrevJump, 0); ADDCMD("", commandGoToNextJump, 0); ADDCMD("h", commandSwitchToLeftView, 0); ADDCMD("", commandSwitchToLeftView, 0); ADDCMD("", commandSwitchToLeftView, 0); ADDCMD("j", commandSwitchToDownView, 0); ADDCMD("", commandSwitchToDownView, 0); ADDCMD("", commandSwitchToDownView, 0); ADDCMD("k", commandSwitchToUpView, 0); ADDCMD("", commandSwitchToUpView, 0); ADDCMD("", commandSwitchToUpView, 0); ADDCMD("l", commandSwitchToRightView, 0); ADDCMD("", commandSwitchToRightView, 0); ADDCMD("", commandSwitchToRightView, 0); ADDCMD("w", commandSwitchToNextView, 0); ADDCMD("", commandSwitchToNextView, 0); ADDCMD("s", commandSplitHoriz, 0); ADDCMD("S", commandSplitHoriz, 0); ADDCMD("", commandSplitHoriz, 0); ADDCMD("v", commandSplitVert, 0); ADDCMD("", commandSplitVert, 0); ADDCMD("c", commandCloseView, 0); ADDCMD("gt", commandSwitchToNextTab, 0); ADDCMD("gT", commandSwitchToPrevTab, 0); ADDCMD("gqq", commandFormatLine, IS_CHANGE); ADDCMD("gq", commandFormatLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("zo", commandExpandLocal, 0); ADDCMD("zc", commandCollapseLocal, 0); ADDCMD("za", commandToggleRegionVisibility, 0); ADDCMD("zr", commandExpandAll, 0); ADDCMD("zm", commandCollapseToplevelNodes, 0); ADDCMD("q.", commandStartRecordingMacro, REGEX_PATTERN); ADDCMD("@.", commandReplayMacro, REGEX_PATTERN); ADDCMD("ZZ", commandCloseWrite, 0); ADDCMD("ZQ", commandCloseNocheck, 0); // regular motions ADDMOTION("h", motionLeft, 0); ADDMOTION("", motionLeft, 0); ADDMOTION("", motionLeft, 0); ADDMOTION("j", motionDown, 0); ADDMOTION("", motionDown, 0); ADDMOTION("", motionDownToFirstNonBlank, 0); ADDMOTION("", motionDownToFirstNonBlank, 0); ADDMOTION("k", motionUp, 0); ADDMOTION("", motionUp, 0); ADDMOTION("-", motionUpToFirstNonBlank, 0); ADDMOTION("l", motionRight, 0); ADDMOTION("", motionRight, 0); ADDMOTION(" ", motionRight, 0); ADDMOTION("$", motionToEOL, 0); ADDMOTION("", motionToEOL, 0); ADDMOTION("0", motionToColumn0, 0); ADDMOTION("", motionToColumn0, 0); ADDMOTION("^", motionToFirstCharacterOfLine, 0); ADDMOTION("f.", motionFindChar, REGEX_PATTERN); ADDMOTION("F.", motionFindCharBackward, REGEX_PATTERN); ADDMOTION("t.", motionToChar, REGEX_PATTERN); ADDMOTION("T.", motionToCharBackward, REGEX_PATTERN); ADDMOTION(";", motionRepeatlastTF, 0); ADDMOTION(",", motionRepeatlastTFBackward, 0); ADDMOTION("n", motionFindNext, 0); ADDMOTION("N", motionFindPrev, 0); ADDMOTION("gg", motionToLineFirst, 0); ADDMOTION("G", motionToLineLast, 0); ADDMOTION("w", motionWordForward, IS_NOT_LINEWISE); ADDMOTION("W", motionWORDForward, IS_NOT_LINEWISE); ADDMOTION("", motionWordForward, IS_NOT_LINEWISE); ADDMOTION("", motionWordBackward, IS_NOT_LINEWISE); ADDMOTION("b", motionWordBackward, 0); ADDMOTION("B", motionWORDBackward, 0); ADDMOTION("e", motionToEndOfWord, 0); ADDMOTION("E", motionToEndOfWORD, 0); ADDMOTION("ge", motionToEndOfPrevWord, 0); ADDMOTION("gE", motionToEndOfPrevWORD, 0); ADDMOTION("|", motionToScreenColumn, 0); ADDMOTION("%", motionToMatchingItem, IS_NOT_LINEWISE); ADDMOTION("`[a-zA-Z^><\\.\\[\\]]", motionToMark, REGEX_PATTERN); ADDMOTION("'[a-zA-Z^><]", motionToMarkLine, REGEX_PATTERN); ADDMOTION("[[", motionToPreviousBraceBlockStart, IS_NOT_LINEWISE); ADDMOTION("]]", motionToNextBraceBlockStart, IS_NOT_LINEWISE); ADDMOTION("[]", motionToPreviousBraceBlockEnd, IS_NOT_LINEWISE); ADDMOTION("][", motionToNextBraceBlockEnd, IS_NOT_LINEWISE); ADDMOTION("*", motionToNextOccurrence, 0); ADDMOTION("#", motionToPrevOccurrence, 0); ADDMOTION("H", motionToFirstLineOfWindow, 0); ADDMOTION("M", motionToMiddleLineOfWindow, 0); ADDMOTION("L", motionToLastLineOfWindow, 0); ADDMOTION("gj", motionToNextVisualLine, 0); ADDMOTION("gk", motionToPrevVisualLine, 0); ADDMOTION("(", motionToPreviousSentence, 0); ADDMOTION(")", motionToNextSentence, 0); ADDMOTION("{", motionToBeforeParagraph, 0); ADDMOTION("}", motionToAfterParagraph, 0); // text objects ADDMOTION("iw", textObjectInnerWord, 0); ADDMOTION("aw", textObjectAWord, IS_NOT_LINEWISE); ADDMOTION("iW", textObjectInnerWORD, 0); ADDMOTION("aW", textObjectAWORD, IS_NOT_LINEWISE); ADDMOTION("is", textObjectInnerSentence, IS_NOT_LINEWISE); ADDMOTION("as", textObjectASentence, IS_NOT_LINEWISE); ADDMOTION("ip", textObjectInnerParagraph, IS_NOT_LINEWISE); ADDMOTION("ap", textObjectAParagraph, IS_NOT_LINEWISE); ADDMOTION("i\"", textObjectInnerQuoteDouble, IS_NOT_LINEWISE); ADDMOTION("a\"", textObjectAQuoteDouble, IS_NOT_LINEWISE); ADDMOTION("i'", textObjectInnerQuoteSingle, IS_NOT_LINEWISE); ADDMOTION("a'", textObjectAQuoteSingle, IS_NOT_LINEWISE); ADDMOTION("i`", textObjectInnerBackQuote, IS_NOT_LINEWISE); ADDMOTION("a`", textObjectABackQuote, IS_NOT_LINEWISE); ADDMOTION("i[()b]", textObjectInnerParen, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[()b]", textObjectAParen, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[{}B]", textObjectInnerCurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[{}B]", textObjectACurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[><]", textObjectInnerInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[><]", textObjectAInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[\\[\\]]", textObjectInnerBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[\\[\\]]", textObjectABracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i,", textObjectInnerComma, IS_NOT_LINEWISE); ADDMOTION("a,", textObjectAComma, IS_NOT_LINEWISE); ADDMOTION("/", motionToIncrementalSearchMatch, IS_NOT_LINEWISE); ADDMOTION("?", motionToIncrementalSearchMatch, IS_NOT_LINEWISE); } QRegExp NormalViMode::generateMatchingItemRegex() const { QString pattern(QStringLiteral("\\[|\\]|\\{|\\}|\\(|\\)|")); QList keys = m_matchingItems.keys(); for (int i = 0; i < keys.size(); i++) { QString s = m_matchingItems[keys[i]]; s.replace(QRegExp(QLatin1String("^-")), QChar()); s.replace(QRegExp(QLatin1String("\\*")), QStringLiteral("\\*")); s.replace(QRegExp(QLatin1String("\\+")), QStringLiteral("\\+")); s.replace(QRegExp(QLatin1String("\\[")), QStringLiteral("\\[")); s.replace(QRegExp(QLatin1String("\\]")), QStringLiteral("\\]")); s.replace(QRegExp(QLatin1String("\\(")), QStringLiteral("\\(")); s.replace(QRegExp(QLatin1String("\\)")), QStringLiteral("\\)")); s.replace(QRegExp(QLatin1String("\\{")), QStringLiteral("\\{")); s.replace(QRegExp(QLatin1String("\\}")), QStringLiteral("\\}")); pattern.append(s); if (i != keys.size() - 1) { pattern.append(QLatin1Char('|')); } } return QRegExp(pattern); } // returns the operation mode that should be used. this is decided by using the following heuristic: // 1. if we're in visual block mode, it should be Block // 2. if we're in visual line mode OR the range spans several lines, it should be LineWise // 3. if neither of these is true, CharWise is returned // 4. there are some motion that makes all operator charwise, if we have one of them mode will be CharWise OperationMode NormalViMode::getOperationMode() const { OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { m = Block; } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode || (m_commandRange.startLine != m_commandRange.endLine && m_viInputModeManager->getCurrentViMode() != ViMode::VisualMode)) { m = LineWise; } if (m_commandWithMotion && !m_linewiseCommand) { m = CharWise; } if (m_lastMotionWasLinewiseInnerBlock) { m = LineWise; } return m; } bool NormalViMode::paste(PasteLocation pasteLocation, bool isgPaste, bool isIndentedPaste) { KTextEditor::Cursor pasteAt(m_view->cursorPosition()); KTextEditor::Cursor cursorAfterPaste = pasteAt; QChar reg = getChosenRegister(UnnamedRegister); OperationMode m = getRegisterFlag(reg); QString textToInsert = getRegisterContent(reg); const bool isTextMultiLine = textToInsert.count(QLatin1Char('\n')) > 0; // In temporary normal mode, p/P act as gp/gP. isgPaste |= m_viInputModeManager->getTemporaryNormalMode(); if (textToInsert.isEmpty()) { error(i18n("Nothing in register %1", reg)); return false; } if (getCount() > 1) { textToInsert = textToInsert.repeated(getCount()); // FIXME: does this make sense for blocks? } if (m == LineWise) { pasteAt.setColumn(0); if (isIndentedPaste) { // Note that this does indeed work if there is no non-whitespace on the current line or if // the line is empty! const QString leadingWhiteSpaceOnCurrentLine = doc()->line(pasteAt.line()).mid(0, doc()->line(pasteAt.line()).indexOf(QRegExp(QLatin1String("[^\\s]")))); const QString leadingWhiteSpaceOnFirstPastedLine = textToInsert.mid(0, textToInsert.indexOf(QRegExp(QLatin1String("[^\\s]")))); // QString has no "left trim" method, bizarrely. while (textToInsert[0].isSpace()) { textToInsert = textToInsert.mid(1); } textToInsert.prepend(leadingWhiteSpaceOnCurrentLine); // Remove the last \n, temporarily: we're going to alter the indentation of each pasted line // by doing a search and replace on '\n's, but don't want to alter this one. textToInsert.chop(1); textToInsert.replace(QLatin1Char('\n') + leadingWhiteSpaceOnFirstPastedLine, QLatin1Char('\n') + leadingWhiteSpaceOnCurrentLine); textToInsert.append(QLatin1Char('\n')); // Re-add the temporarily removed last '\n'. } if (pasteLocation == AfterCurrentPosition) { textToInsert.chop(1); // remove the last \n pasteAt.setColumn(doc()->lineLength(pasteAt.line())); // paste after the current line and ... textToInsert.prepend(QLatin1Char('\n')); // ... prepend a \n, so the text starts on a new line cursorAfterPaste.setLine(cursorAfterPaste.line() + 1); } if (isgPaste) { cursorAfterPaste.setLine(cursorAfterPaste.line() + textToInsert.count(QLatin1Char('\n'))); } } else { if (pasteLocation == AfterCurrentPosition) { // Move cursor forward one before we paste. The position after the paste must also // be updated accordingly. if (getLine(pasteAt.line()).length() > 0) { pasteAt.setColumn(pasteAt.column() + 1); } cursorAfterPaste = pasteAt; } const bool leaveCursorAtStartOfPaste = isTextMultiLine && !isgPaste; if (!leaveCursorAtStartOfPaste) { cursorAfterPaste = cursorPosAtEndOfPaste(pasteAt, textToInsert); if (!isgPaste) { cursorAfterPaste.setColumn(cursorAfterPaste.column() - 1); } } } doc()->editBegin(); if (m_view->selection()) { pasteAt = m_view->selectionRange().start(); doc()->removeText(m_view->selectionRange()); } doc()->insertText(pasteAt, textToInsert, m == Block); doc()->editEnd(); if (cursorAfterPaste.line() >= doc()->lines()) { cursorAfterPaste.setLine(doc()->lines() - 1); } updateCursor(cursorAfterPaste); return true; } KTextEditor::Cursor NormalViMode::cursorPosAtEndOfPaste(const KTextEditor::Cursor &pasteLocation, const QString &pastedText) const { KTextEditor::Cursor cAfter = pasteLocation; const int lineCount = pastedText.count(QLatin1Char('\n')) + 1; if (lineCount == 1) { cAfter.setColumn(cAfter.column() + pastedText.length()); } else { const int lastLineLength = pastedText.size() - (pastedText.lastIndexOf(QLatin1Char('\n')) + 1); cAfter.setColumn(lastLineLength); cAfter.setLine(cAfter.line() + lineCount - 1); } return cAfter; } void NormalViMode::joinLines(unsigned int from, unsigned int to) const { // make sure we don't try to join lines past the document end if (to >= (unsigned int)(doc()->lines())) { to = doc()->lines() - 1; } // joining one line is a no-op if (from == to) { return; } doc()->joinLines(from, to); } void NormalViMode::reformatLines(unsigned int from, unsigned int to) const { joinLines(from, to); doc()->wrapText(from, to); } int NormalViMode::getFirstNonBlank(int line) const { if (line < 0) { line = m_view->cursorPosition().line(); } // doc()->plainKateTextLine returns NULL if the line is out of bounds. Kate::TextLine l = doc()->plainKateTextLine(line); Q_ASSERT(l); int c = l->firstChar(); return (c < 0) ? 0 : c; } // Tries to shrinks toShrink so that it fits tightly around rangeToShrinkTo. void NormalViMode::shrinkRangeAroundCursor(Range &toShrink, const Range &rangeToShrinkTo) const { if (!toShrink.valid || !rangeToShrinkTo.valid) { return; } KTextEditor::Cursor cursorPos = m_view->cursorPosition(); if (rangeToShrinkTo.startLine >= cursorPos.line()) { if (rangeToShrinkTo.startLine > cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.startLine == cursorPos.line()); if (rangeToShrinkTo.startColumn > cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (rangeToShrinkTo.endLine <= cursorPos.line()) { if (rangeToShrinkTo.endLine < cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.endLine == cursorPos.line()); if (rangeToShrinkTo.endColumn < cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (toShrink.startLine <= rangeToShrinkTo.startLine) { if (toShrink.startLine < rangeToShrinkTo.startLine) { toShrink.startLine = rangeToShrinkTo.startLine; toShrink.startColumn = rangeToShrinkTo.startColumn; } Q_ASSERT(toShrink.startLine == rangeToShrinkTo.startLine); if (toShrink.startColumn < rangeToShrinkTo.startColumn) { toShrink.startColumn = rangeToShrinkTo.startColumn; } } if (toShrink.endLine >= rangeToShrinkTo.endLine) { if (toShrink.endLine > rangeToShrinkTo.endLine) { toShrink.endLine = rangeToShrinkTo.endLine; toShrink.endColumn = rangeToShrinkTo.endColumn; } Q_ASSERT(toShrink.endLine == rangeToShrinkTo.endLine); if (toShrink.endColumn > rangeToShrinkTo.endColumn) { toShrink.endColumn = rangeToShrinkTo.endColumn; } } } Range NormalViMode::textObjectComma(bool inner) const { // Basic algorithm: look left and right of the cursor for all combinations // of enclosing commas and the various types of brackets, and pick the pair // closest to the cursor that surrounds the cursor. Range r(0, 0, m_view->doc()->lines(), m_view->doc()->line(m_view->doc()->lastLine()).length(), InclusiveMotion); shrinkRangeAroundCursor(r, findSurroundingQuotes(QLatin1Char(','), inner)); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(']'), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(','), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('['), QLatin1Char(','), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char(','), inner, QLatin1Char('{'), QLatin1Char('}'))); return r; } void NormalViMode::updateYankHighlightAttrib() { if (!m_highlightYankAttribute) { m_highlightYankAttribute = new KTextEditor::Attribute; } const QColor &yankedColor = m_view->renderer()->config()->savedLineColor(); m_highlightYankAttribute->setBackground(yankedColor); KTextEditor::Attribute::Ptr mouseInAttribute(new KTextEditor::Attribute()); mouseInAttribute->setFontBold(true); m_highlightYankAttribute->setDynamicAttribute(KTextEditor::Attribute::ActivateMouseIn, mouseInAttribute); m_highlightYankAttribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)->setBackground(yankedColor); } void NormalViMode::highlightYank(const Range &range, const OperationMode mode) { clearYankHighlight(); // current MovingRange doesn't support block mode selection so split the // block range into per-line ranges if (mode == Block) { for (int i = range.startLine; i <= range.endLine; i++) { addHighlightYank(KTextEditor::Range(i, range.startColumn, i, range.endColumn)); } } else { addHighlightYank(KTextEditor::Range(range.startLine, range.startColumn, range.endLine, range.endColumn)); } } void NormalViMode::addHighlightYank(const KTextEditor::Range &yankRange) { KTextEditor::MovingRange *highlightedYank = m_view->doc()->newMovingRange(yankRange, Kate::TextRange::DoNotExpand); highlightedYank->setView(m_view); // show only in this view highlightedYank->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlightedYank->setZDepth(-10000.0); highlightedYank->setAttribute(m_highlightYankAttribute); highlightedYankForDocument().insert(highlightedYank); } void NormalViMode::clearYankHighlight() { QSet &pHighlightedYanks = highlightedYankForDocument(); qDeleteAll(pHighlightedYanks); pHighlightedYanks.clear(); } void NormalViMode::aboutToDeleteMovingInterfaceContent() { QSet &pHighlightedYanks = highlightedYankForDocument(); // Prevent double-deletion in case this NormalMode is deleted. pHighlightedYanks.clear(); } QSet &NormalViMode::highlightedYankForDocument() { // Work around the fact that both Normal and Visual mode will have their own m_highlightedYank - // make Normal's the canonical one. return m_viInputModeManager->getViNormalMode()->m_highlightedYanks; } bool NormalViMode::waitingForRegisterOrCharToSearch() { // r, q, @ are never preceded by operators. There will always be a keys size of 1 for them. // f, t, F, T can be preceded by a delete/replace/yank/indent operator. size = 2 in that case. // f, t, F, T can be preceded by 'g' case/formatting operators. size = 3 in that case. const int keysSize = m_keys.size(); if (keysSize < 1) { // Just being defensive there. return false; } if (keysSize > 1) { // Multi-letter operation. QChar cPrefix = m_keys[0]; if (keysSize == 2) { // delete/replace/yank/indent operator? if (cPrefix != QLatin1Char('c') && cPrefix != QLatin1Char('d') && cPrefix != QLatin1Char('y') && cPrefix != QLatin1Char('=') && cPrefix != QLatin1Char('>') && cPrefix != QLatin1Char('<')) { return false; } } else if (keysSize == 3) { // We need to look deeper. Is it a g motion? QChar cNextfix = m_keys[1]; if (cPrefix != QLatin1Char('g') || (cNextfix != QLatin1Char('U') && cNextfix != QLatin1Char('u') && cNextfix != QLatin1Char('~') && cNextfix != QLatin1Char('q') && cNextfix != QLatin1Char('w') && cNextfix != QLatin1Char('@'))) { return false; } } else { return false; } } QChar ch = m_keys[keysSize - 1]; return (ch == QLatin1Char('f') || ch == QLatin1Char('t') || ch == QLatin1Char('F') || ch == QLatin1Char('T') // c/d prefix unapplicable for the following cases. || (keysSize == 1 && (ch == QLatin1Char('r') || ch == QLatin1Char('q') || ch == QLatin1Char('@')))); } void NormalViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range) { Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); const bool continuesInsertion = range.start().line() == m_currentChangeEndMarker.line() && range.start().column() == m_currentChangeEndMarker.column(); const bool beginsWithNewline = doc()->text(range).at(0) == QLatin1Char('\n'); if (!continuesInsertion) { KTextEditor::Cursor newBeginMarkerPos = range.start(); if (beginsWithNewline && !isInsertReplaceMode) { // Presumably a linewise paste, in which case we ignore the leading '\n' newBeginMarkerPos = KTextEditor::Cursor(newBeginMarkerPos.line() + 1, 0); } m_viInputModeManager->marks()->setStartEditYanked(newBeginMarkerPos); } m_viInputModeManager->marks()->setLastChange(range.start()); KTextEditor::Cursor editEndMarker = range.end(); if (!isInsertReplaceMode) { editEndMarker.setColumn(editEndMarker.column() - 1); } m_viInputModeManager->marks()->setFinishEditYanked(editEndMarker); m_currentChangeEndMarker = range.end(); if (m_isUndo) { const bool addsMultipleLines = range.start().line() != range.end().line(); m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line(), 0)); if (addsMultipleLines) { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + 1, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + 1, 0)); } else { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line(), 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line(), 0)); } } } void NormalViMode::textRemoved(KTextEditor::Document *document, KTextEditor::Range range) { Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); m_viInputModeManager->marks()->setLastChange(range.start()); if (!isInsertReplaceMode) { // Don't go resetting [ just because we did a Ctrl-h! m_viInputModeManager->marks()->setStartEditYanked(range.start()); } else { // Don't go disrupting our continued insertion just because we did a Ctrl-h! m_currentChangeEndMarker = range.start(); } m_viInputModeManager->marks()->setFinishEditYanked(range.start()); if (m_isUndo) { // Slavishly follow Vim's weird rules: if an undo removes several lines, then all markers should // be at the beginning of the line after the last line removed, else they should at the beginning // of the line above that. const int markerLineAdjustment = (range.start().line() != range.end().line()) ? 1 : 0; m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + markerLineAdjustment, 0)); } } void NormalViMode::undoBeginning() { m_isUndo = true; } void NormalViMode::undoEnded() { m_isUndo = false; } bool NormalViMode::executeKateCommand(const QString &command) { KTextEditor::Command *p = KateCmd::self()->queryCommand(command); if (!p) { return false; } QString msg; return p->exec(m_view, command, msg); } diff --git a/src/vimode/modes/replacevimode.cpp b/src/vimode/modes/replacevimode.cpp index bb36981e..793ab4bd 100644 --- a/src/vimode/modes/replacevimode.cpp +++ b/src/vimode/modes/replacevimode.cpp @@ -1,243 +1,243 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008 Erlend Hamberg * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include using namespace KateVi; ReplaceViMode::ReplaceViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : ModeBase() { m_view = view; m_viewInternal = viewInternal; m_viInputModeManager = viInputModeManager; m_count = 1; } ReplaceViMode::~ReplaceViMode() { /* There's nothing to do here. */ } bool ReplaceViMode::commandInsertFromLine(int offset) { KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() + offset >= doc()->lines() || c.line() + offset < 0) { return false; } // Fetch the new character from the specified line. KTextEditor::Cursor target(c.line() + offset, c.column()); QChar ch = doc()->characterAt(target); if (ch == QChar::Null) { return false; } // The cursor is at the end of the line: just append the char. if (c.column() == doc()->lineLength(c.line())) { return doc()->insertText(c, ch); } // We can replace the current one with the fetched character. KTextEditor::Cursor next(c.line(), c.column() + 1); QChar removed = doc()->line(c.line()).at(c.column()); if (doc()->replaceText(KTextEditor::Range(c, next), ch)) { overwrittenChar(removed); return true; } return false; } bool ReplaceViMode::commandMoveOneWordLeft() { KTextEditor::Cursor c(m_view->cursorPosition()); c = findPrevWordStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); } updateCursor(c); return true; } bool ReplaceViMode::commandMoveOneWordRight() { KTextEditor::Cursor c(m_view->cursorPosition()); c = findNextWordStart(c.line(), c.column()); if (!c.isValid()) { c = doc()->documentEnd(); } updateCursor(c); return true; } bool ReplaceViMode::handleKeypress(const QKeyEvent *e) { // backspace should work even if the shift key is down if (e->modifiers() != Qt::ControlModifier && e->key() == Qt::Key_Backspace) { backspace(); return true; } if (e->modifiers() == Qt::NoModifier) { switch (e->key()) { - case Qt::Key_Escape: - m_overwritten.clear(); - leaveReplaceMode(); - return true; - case Qt::Key_Left: - m_overwritten.clear(); - m_view->cursorLeft(); - return true; - case Qt::Key_Right: - m_overwritten.clear(); - m_view->cursorRight(); - return true; - case Qt::Key_Up: - m_overwritten.clear(); - m_view->up(); - return true; - case Qt::Key_Down: - m_overwritten.clear(); - m_view->down(); - return true; - case Qt::Key_Home: - m_overwritten.clear(); - m_view->home(); - return true; - case Qt::Key_End: - m_overwritten.clear(); - m_view->end(); - return true; - case Qt::Key_PageUp: - m_overwritten.clear(); - m_view->pageUp(); - return true; - case Qt::Key_PageDown: - m_overwritten.clear(); - m_view->pageDown(); - return true; - case Qt::Key_Delete: - m_view->keyDelete(); - return true; - case Qt::Key_Insert: - startInsertMode(); - return true; - default: - return false; + case Qt::Key_Escape: + m_overwritten.clear(); + leaveReplaceMode(); + return true; + case Qt::Key_Left: + m_overwritten.clear(); + m_view->cursorLeft(); + return true; + case Qt::Key_Right: + m_overwritten.clear(); + m_view->cursorRight(); + return true; + case Qt::Key_Up: + m_overwritten.clear(); + m_view->up(); + return true; + case Qt::Key_Down: + m_overwritten.clear(); + m_view->down(); + return true; + case Qt::Key_Home: + m_overwritten.clear(); + m_view->home(); + return true; + case Qt::Key_End: + m_overwritten.clear(); + m_view->end(); + return true; + case Qt::Key_PageUp: + m_overwritten.clear(); + m_view->pageUp(); + return true; + case Qt::Key_PageDown: + m_overwritten.clear(); + m_view->pageDown(); + return true; + case Qt::Key_Delete: + m_view->keyDelete(); + return true; + case Qt::Key_Insert: + startInsertMode(); + return true; + default: + return false; } } else if (e->modifiers() == Qt::ControlModifier) { switch (e->key()) { - case Qt::Key_BracketLeft: - case Qt::Key_C: - startNormalMode(); - return true; - case Qt::Key_E: - commandInsertFromLine(1); - return true; - case Qt::Key_Y: - commandInsertFromLine(-1); - return true; - case Qt::Key_W: - commandBackWord(); - return true; - case Qt::Key_U: - commandBackLine(); - return true; - case Qt::Key_Left: - m_overwritten.clear(); - commandMoveOneWordLeft(); - return true; - case Qt::Key_Right: - m_overwritten.clear(); - commandMoveOneWordRight(); - return true; - default: - return false; + case Qt::Key_BracketLeft: + case Qt::Key_C: + startNormalMode(); + return true; + case Qt::Key_E: + commandInsertFromLine(1); + return true; + case Qt::Key_Y: + commandInsertFromLine(-1); + return true; + case Qt::Key_W: + commandBackWord(); + return true; + case Qt::Key_U: + commandBackLine(); + return true; + case Qt::Key_Left: + m_overwritten.clear(); + commandMoveOneWordLeft(); + return true; + case Qt::Key_Right: + m_overwritten.clear(); + commandMoveOneWordRight(); + return true; + default: + return false; } } return false; } void ReplaceViMode::backspace() { KTextEditor::Cursor c1(m_view->cursorPosition()); KTextEditor::Cursor c2(c1.line(), c1.column() - 1); if (c1.column() > 0) { if (!m_overwritten.isEmpty()) { doc()->removeText(KTextEditor::Range(c1.line(), c1.column() - 1, c1.line(), c1.column())); doc()->insertText(c2, m_overwritten.right(1)); m_overwritten.remove(m_overwritten.length() - 1, 1); } updateCursor(c2); } } void ReplaceViMode::commandBackWord() { KTextEditor::Cursor current(m_view->cursorPosition()); KTextEditor::Cursor to(findPrevWordStart(current.line(), current.column())); if (!to.isValid()) { return; } while (current.isValid() && current != to) { backspace(); current = m_view->cursorPosition(); } } void ReplaceViMode::commandBackLine() { const int column = m_view->cursorPosition().column(); for (int i = column; i >= 0 && !m_overwritten.isEmpty(); i--) { backspace(); } } void ReplaceViMode::leaveReplaceMode() { // Redo replacement operation times m_view->abortCompletion(); if (m_count > 1) { // Look at added text so that we can repeat the addition const QString added = doc()->text(KTextEditor::Range(m_viInputModeManager->marks()->getStartEditYanked(), m_view->cursorPosition())); for (unsigned int i = 0; i < m_count - 1; i++) { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c2(c.line(), c.column() + added.length()); doc()->replaceText(KTextEditor::Range(c, c2), added); } } startNormalMode(); }