From 29047824247bd416b4706f20b75556793aaa811c Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 16 Dec 2023 20:41:44 +0530 Subject: [PATCH 1/8] add: FakeVim submodule to project --- .gitmodules | 3 +++ third_party/FakeVim | 1 + 2 files changed, 4 insertions(+) create mode 160000 third_party/FakeVim diff --git a/.gitmodules b/.gitmodules index 0017dbd02..d5606ae2b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "third_party/syntax-highlighting"] path = third_party/syntax-highlighting url = https://github.com/KDE/syntax-highlighting.git +[submodule "third_party/FakeVim"] + path = third_party/FakeVim + url = https://github.com/cpeditor/FakeVim diff --git a/third_party/FakeVim b/third_party/FakeVim new file mode 160000 index 000000000..dce7b0841 --- /dev/null +++ b/third_party/FakeVim @@ -0,0 +1 @@ +Subproject commit dce7b08412a19fb53d97b679d28270c0ca41725d From 3f9418d86c4e92e0abe69eb7f654611db9b1ea25 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 16 Dec 2023 20:44:21 +0530 Subject: [PATCH 2/8] add: FakeVim targets to cmake --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index fabee76db..19fad53f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,8 @@ add_subdirectory(third_party/lsp-cpp) add_subdirectory(third_party/qhttp) +add_subdirectory(third_party/FakeVim) + add_subdirectory(third_party/diff_match_patch) option(PORTABLE_VERSION "Build the portable version" Off) @@ -279,6 +281,7 @@ target_link_libraries(cpeditor PRIVATE Qt5::Network) target_link_libraries(cpeditor PRIVATE Qt5::Widgets) target_link_libraries(cpeditor PRIVATE QtFindReplaceDialog) target_link_libraries(cpeditor PRIVATE SingleApplication) +target_link_libraries(cpeditor PRIVATE fakevim) target_link_libraries(cpeditor PRIVATE QHttp) target_link_libraries(cpeditor PRIVATE diff_match_patch) target_link_libraries(cpeditor PRIVATE KF5::SyntaxHighlighting) From 620f87a878969608c3ac8bf29390237db4aa73d7 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 16 Dec 2023 20:49:30 +0530 Subject: [PATCH 3/8] add: FakeVim for git aur PKGBUILD --- dist/aur/git/PKGBUILD | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dist/aur/git/PKGBUILD b/dist/aur/git/PKGBUILD index 108a0b110..a98dfd475 100644 --- a/dist/aur/git/PKGBUILD +++ b/dist/aur/git/PKGBUILD @@ -29,12 +29,13 @@ conflicts=("cpeditor") source=('git+https://github.com/cpeditor/cpeditor.git' 'git+https://github.com/cpeditor/QtFindReplaceDialog.git' + 'git+https://github.com/cpeditor/FakeVim.git' 'git+https://github.com/cpeditor/lsp-cpp.git' 'git+https://github.com/itay-grudev/singleapplication.git' 'git+https://github.com/MikeMirzayanov/testlib.git' 'git+https://github.com/cpeditor/qhttp.git') -md5sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP') +md5sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP') pkgver() { cd "$_pkgname" @@ -46,6 +47,7 @@ prepare() { git submodule init git config submodule.third_party/QtFindReplaceDialog.url "$srcdir/QtFindReplaceDialog" + git config submodule.third_party/FakeVim.url "$srcdir/FakeVim" git config submodule.third_party/lsp-cpp.url "$srcdir/lsp-cpp" git config submodule.third_party/singleapplication.url "$srcdir/singleapplication" git config submodule.third_party/testlib.url "$srcdir/testlib" From e7b339c7c608e7154122f71a6ec7f34d237a6e06 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 16 Dec 2023 22:55:32 +0530 Subject: [PATCH 4/8] port: Changes from PR 516 --- CMakeLists.txt | 4 + src/Editor/FakeVimCommands.cpp | 234 ++++++++++++ src/Editor/FakeVimCommands.hpp | 92 +++++ src/Editor/FakeVimProxy.cpp | 579 +++++++++++++++++++++++++++++ src/Editor/FakeVimProxy.hpp | 105 ++++++ src/Settings/PreferencesWindow.cpp | 2 +- src/Settings/settings.json | 55 +++ src/appwindow.cpp | 99 +++-- src/appwindow.hpp | 19 +- src/mainwindow.cpp | 28 ++ src/mainwindow.hpp | 13 +- 11 files changed, 1185 insertions(+), 45 deletions(-) create mode 100644 src/Editor/FakeVimCommands.cpp create mode 100644 src/Editor/FakeVimCommands.hpp create mode 100644 src/Editor/FakeVimProxy.cpp create mode 100644 src/Editor/FakeVimProxy.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 19fad53f0..6078cde8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,10 @@ add_executable(cpeditor src/Editor/CodeEditor.hpp src/Editor/CodeEditorSideBar.cpp src/Editor/CodeEditorSideBar.hpp + src/Editor/FakeVimProxy.cpp + src/Editor/FakeVimProxy.hpp + src/Editor/FakeVimCommands.cpp + src/Editor/FakeVimCommands.hpp src/Editor/HighLighter.cpp src/Editor/HighLighter.hpp src/Editor/KSHRepository.cpp diff --git a/src/Editor/FakeVimCommands.cpp b/src/Editor/FakeVimCommands.cpp new file mode 100644 index 000000000..6a9bfe06c --- /dev/null +++ b/src/Editor/FakeVimCommands.cpp @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2019-2023 Ashar Khan + * + * This file is part of CP Editor. + * + * CP Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * I will not be responsible if CP Editor behaves in unexpected way and + * causes your ratings to go down and or lose any important contest. + * + * Believe Software is "Software" and it isn't immune to bugs. + * + */ + +#include "Editor/FakeVimCommands.hpp" +#include "Util/FileUtil.hpp" +#include "Widgets/TestCases.hpp" +#include "appwindow.hpp" +#include "generated/SettingsHelper.hpp" +#include "third_party/FakeVim/fakevim/fakevimhandler.h" +#include +#include +#include +#include + +namespace Editor +{ + +FakeVimCommands::FakeVimCommands(AppWindow *window, QObject *parent) : QObject(parent), appwin(window) +{ +} + +FakeVimCommands::CommandTypes FakeVimCommands::customCommandType(FakeVim::Internal::ExCommand const &ex) +{ + if (ex.cmd == "new") + return CommandTypes::New; + + if (ex.cmd == "open" || ex.cmd == "opn") + return CommandTypes::Open; + + if (ex.cmd == "compile" || ex.cmd == "cmp") + return CommandTypes::Compile; + + if (ex.cmd == "crun" || ex.cmd == "crn") + return CommandTypes::CompileRun; + + if (ex.cmd == "run") + return CommandTypes::Run; + + if (ex.cmd == "drun" || ex.cmd == "drn") + return CommandTypes::DetachedRun; + + if (ex.cmd == "killall" || ex.cmd == "kap") + return CommandTypes::KillProcess; + + if (ex.cmd == "format" || ex.cmd == "fmt") + return CommandTypes::FormatCode; + + if (ex.cmd == "snippet" || ex.cmd == "snp") + return CommandTypes::Snippets; + + if (ex.cmd == "vmode" || ex.cmd == "vmd") + return CommandTypes::Vmode; + + if (ex.cmd == "preference" || ex.cmd == "prf") + return CommandTypes::Preference; + + if (ex.cmd == "chlang" || ex.cmd == "chl") + return CommandTypes::Chlang; + + if (ex.cmd == "clear" || ex.cmd == "clr") + return CommandTypes::Clear; + + if (ex.cmd == "exit" || ex.cmd == "ext") + return CommandTypes::Exit; + + return CommandTypes::Unknown; +} + +bool FakeVimCommands::handleCustomCommand(CommandTypes type, QString const &args, bool hasbang) +{ + if (type == CommandTypes::Unknown) + return false; + + switch (type) + { + case CommandTypes::New: { + const QString lang = args.isEmpty() ? SettingsHelper::getDefaultLanguage() : language(args); + + if (!lang.isEmpty()) + appwin->openTab("", lang); + else + showError(tr("`new` requires no argument or one of 'cpp', 'java' and 'python', got [%1]").arg(args)); + break; + } + case CommandTypes::Open: { + QString path = args; + + if (path.isEmpty()) + { + appwin->on_actionOpen_triggered(); + break; + } + + if (path.startsWith('~')) + path = QDir::home().filePath(args.mid(1)); + + QFileInfo file(path); + + if (!Util::cppSuffix.contains(file.suffix()) && !Util::javaSuffix.contains(file.suffix()) && + !Util::javaSuffix.contains(file.suffix())) + { + showError(tr("[%1] is not C++, Python or Java source file").arg(file.absoluteFilePath())); + break; + } + + if (!file.exists() && !hasbang) + { + showError(tr("[%1] does not exist. To open a tab with a non-existing file, use `open!` instead").arg(path)); + break; + } + + appwin->openTab(file.absoluteFilePath()); + break; + } + case CommandTypes::Compile: { + appwin->on_actionCompile_triggered(); + break; + } + case CommandTypes::CompileRun: { + appwin->on_actionCompileRun_triggered(); + break; + } + case CommandTypes::Run: { + + if (args.isEmpty()) + appwin->on_actionRun_triggered(); + else // args is not empty + { + bool ok = false; + int caseNum = args.toInt(&ok); + + if (!ok) + showError(tr("[%1] is not a number").arg(args)); + else // args is a number + { + if (appwin->currentWindow() && caseNum > 0 && + caseNum <= appwin->currentWindow()->testcases->count()) // args is valid + appwin->currentWindow()->runTestCase(caseNum - 1); + else + showError( + tr("%1 is out of range [1, %2]").arg(args).arg(appwin->currentWindow()->testcases->count())); + } + } + + break; + } + case CommandTypes::DetachedRun: { + appwin->on_actionRunDetached_triggered(); + } + break; + case CommandTypes::KillProcess: { + appwin->on_actionKillProcesses_triggered(); + break; + } + case CommandTypes::FormatCode: { + appwin->on_actionFormatCode_triggered(); + break; + } + case CommandTypes::Snippets: { + appwin->on_actionUseSnippets_triggered(); + break; + } + case CommandTypes::Vmode: { + if (args == "edit") + appwin->on_actionEditorMode_triggered(); + else if (args == "split") + appwin->on_actionSplitMode_triggered(); + else + showError(tr("[%1] is not a valid view mode. It should be one of 'split' and 'edit'").arg(args)); + break; + } + case CommandTypes::Preference: { + appwin->on_actionSettings_triggered(); + break; + } + case CommandTypes::Chlang: { + const QString lang = language(args); + + if (!lang.isEmpty() && appwin->currentWindow()) + appwin->currentWindow()->setLanguage(lang); + else + showError(tr("%1 is not a valid language name. It should be one of 'cpp', 'java' and 'python'").arg(args)); + break; + } + case CommandTypes::Clear: { + appwin->currentWindow()->on_clearMessagesButton_clicked(); + break; + } + case CommandTypes::Exit: { + appwin->on_actionQuit_triggered(); + break; + } + case CommandTypes::Unknown: { + Q_UNREACHABLE(); + } + }; + return true; +} + +void FakeVimCommands::showError(QString const &message) +{ + if (auto *handler = qobject_cast(parent())) + { + handler->showMessage(FakeVim::Internal::MessageInfo, message); + } +} + +QString FakeVimCommands::language(const QString &langCode) +{ + const auto code = langCode.toLower().trimmed(); + if (code == "cpp" || code == "c++") + return "C++"; + if (code == "java") + return "Java"; + if (code == "py" || code == "python") + return "Python"; + return QString(); +} + +} // namespace Editor diff --git a/src/Editor/FakeVimCommands.hpp b/src/Editor/FakeVimCommands.hpp new file mode 100644 index 000000000..c0997c0fa --- /dev/null +++ b/src/Editor/FakeVimCommands.hpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019-2023 Ashar Khan + * + * This file is part of CP Editor. + * + * CP Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * I will not be responsible if CP Editor behaves in unexpected way and + * causes your ratings to go down and or lose any important contest. + * + * Believe Software is "Software" and it isn't immune to bugs. + * + */ + +#ifndef FAKE_VIM_COMMAND_HPP +#define FAKE_VIM_COMMAND_HPP + +#include + +namespace FakeVim +{ +namespace Internal +{ +class ExCommand; +} +} // namespace FakeVim + +class AppWindow; +template struct QPair; + +namespace Editor +{ +class FakeVimCommands : public QObject +{ + Q_OBJECT + + public: + enum class CommandTypes + { + New, + Open, + Compile, + CompileRun, + Run, + DetachedRun, + KillProcess, + FormatCode, + Snippets, + Vmode, + Preference, + Chlang, + Clear, + Exit, + + Unknown + }; + + /** + * @brief Constructs a new handler which performs actions using/on the provided appwindow + */ + explicit FakeVimCommands(AppWindow *window, QObject *parent = nullptr); + + /** + * @brief retrieve the type of custom command. + * @returns enum CommandTypes that represents the command type. + * @note if not a custom command or invalid command, the CommandTypes::UNKNOWN is returned + */ + static CommandTypes customCommandType(FakeVim::Internal::ExCommand const &ex); + + /** + * @brief Tries to handle passed command type with commandArgs and bang. + * @returns true if the command was handled otherwise false + * @note Custom commands are handled before Vim Commands + */ + bool handleCustomCommand(CommandTypes type, QString const &commandArgs, bool hasBang = false); + + private: + void showError(QString const &message); + + /** + * @brief get the language of a language code + */ + static QString language(const QString &langCode); + + AppWindow *appwin; +}; +} // namespace Editor + +#endif // FAKE_VIM_COMMAND_HPP diff --git a/src/Editor/FakeVimProxy.cpp b/src/Editor/FakeVimProxy.cpp new file mode 100644 index 000000000..ed256bf73 --- /dev/null +++ b/src/Editor/FakeVimProxy.cpp @@ -0,0 +1,579 @@ +/* + * Copyright (C) 2019-2023 Ashar Khan + * + * This file is part of CP Editor. + * + * CP Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * I will not be responsible if CP Editor behaves in unexpected way and + * causes your ratings to go down and or lose any important contest. + * + * Believe Software is "Software" and it isn't immune to bugs. + * + */ + +/* Most of the code in this file is under the following copyright + Copyright (c) 2017, Lukas Holecek + This file is part of CopyQ. + CopyQ is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + CopyQ 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 General Public License for more details. + You should have received a copy of the GNU General Public License + along with CopyQ. If not, see . +*/ +#include "Editor/FakeVimProxy.hpp" +#include "Core/EventLogger.hpp" +#include "Editor/CodeEditor.hpp" +#include "appwindow.hpp" +#include "generated/SettingsHelper.hpp" +#include "mainwindow.hpp" +#include "third_party/FakeVim/fakevim/fakevimactions.h" +#include "third_party/FakeVim/fakevim/fakevimhandler.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Editor +{ + +FakeVimProxy::FakeVimProxy(QWidget *widget, MainWindow *mw, AppWindow *aw, QObject *parent) + : QObject(parent), m_widget(widget), m_mainWindow(mw), m_appWindow(aw), + m_commandHandler(new FakeVimCommands(aw, parent)) +{ + m_statusData = new QLabel(m_mainWindow); + m_statusMessage = new QLabel(m_mainWindow); + setStatusBar(); +} + +FakeVimProxy::~FakeVimProxy() +{ + delete m_commandHandler; +} + +void FakeVimProxy::changeStatusData(QString const &info) +{ + m_statusData->setText(info); +} + +void FakeVimProxy::highlightMatches(QString const &pattern) +{ + QTextDocument *doc = nullptr; + + { // in a block so we don't inadvertently use one of them later + auto *plainEditor = qobject_cast(m_widget); + auto *editor = qobject_cast(m_widget); + if (editor) + { + doc = editor->document(); + } + else if (plainEditor) + { + doc = plainEditor->document(); + } + else + { + return; + } + } + Q_ASSERT(doc); + + QTextEdit::ExtraSelection selection; + selection.format.setBackground(Qt::yellow); + selection.format.setForeground(Qt::black); + + // Highlight matches. + QRegExp re(pattern); + QTextCursor cur = doc->find(re); + + m_searchSelection.clear(); + + int a = cur.position(); + while (!cur.isNull()) + { + if (cur.hasSelection()) + { + selection.cursor = cur; + m_searchSelection.append(selection); + } + else + { + cur.movePosition(QTextCursor::NextCharacter); + } + cur = doc->find(re, cur); + int b = cur.position(); + if (a == b) + { + cur.movePosition(QTextCursor::NextCharacter); + cur = doc->find(re, cur); + b = cur.position(); + if (a == b) + break; + } + a = b; + } + + updateExtraSelections(); +} + +void FakeVimProxy::changeStatusMessage(QString const &contents, int cursorPos) +{ + m_statusMessage->setText(cursorPos == -1 ? contents + : contents.left(cursorPos) + QChar(10073) + contents.mid(cursorPos)); +} + +void FakeVimProxy::setStatusBar() +{ + m_mainWindow->statusBar()->addPermanentWidget(m_statusData); + m_mainWindow->statusBar()->addWidget(m_statusMessage); + m_mainWindow->statusBar()->setFont(SettingsHelper::getEditorFont()); +} + +void FakeVimProxy::handleExCommand(bool *handled, FakeVim::Internal::ExCommand const &cmd) +{ + auto customCommandType = FakeVimCommands::customCommandType(cmd); + if (customCommandType != FakeVimCommands::CommandTypes::Unknown) + { + *handled = m_commandHandler->handleCustomCommand(customCommandType, cmd.args, cmd.hasBang); + return; + } + if (wantSaveAndQuit(cmd)) + { + // :wq + if (save()) + quit(); + } + else if (wantSave(cmd)) + { + save(); // :w + } + else if (wantQuit(cmd)) + { + if (cmd.hasBang) + forceQuit(); // :q! + else + quit(); // :q + } + else + { + *handled = false; + return; + } + + *handled = true; +} + +void FakeVimProxy::requestSetBlockSelection(const QTextCursor &tc) +{ + auto *editor = qobject_cast(m_widget); + auto *plainEditor = qobject_cast(m_widget); + if (!editor && !plainEditor) + { + return; + } + + QPalette pal = m_widget->parentWidget() != nullptr ? m_widget->parentWidget()->palette() : QApplication::palette(); + + m_blockSelection.clear(); + m_clearSelection.clear(); + + QTextCursor cur = tc; + + QTextEdit::ExtraSelection selection; + selection.format.setBackground(pal.color(QPalette::Base)); + selection.format.setForeground(pal.color(QPalette::Text)); + selection.cursor = cur; + m_clearSelection.append(selection); + + selection.format.setBackground(pal.color(QPalette::Highlight)); + selection.format.setForeground(pal.color(QPalette::HighlightedText)); + + int from = cur.positionInBlock(); + int to = cur.anchor() - cur.document()->findBlock(cur.anchor()).position(); + const int min = qMin(cur.position(), cur.anchor()); + const int max = qMax(cur.position(), cur.anchor()); + for (QTextBlock block = cur.document()->findBlock(min); block.isValid() && block.position() < max; + block = block.next()) + { + cur.setPosition(block.position() + qMin(from, block.length())); + cur.setPosition(block.position() + qMin(to, block.length()), QTextCursor::KeepAnchor); + selection.cursor = cur; + m_blockSelection.append(selection); + } + + if (editor) + { + disconnect(editor, &QTextEdit::selectionChanged, this, &FakeVimProxy::updateBlockSelection); + editor->setTextCursor(tc); + connect(editor, &QTextEdit::selectionChanged, this, &FakeVimProxy::updateBlockSelection); + } + else + { + disconnect(plainEditor, &QPlainTextEdit::selectionChanged, this, &FakeVimProxy::updateBlockSelection); + plainEditor->setTextCursor(tc); + connect(plainEditor, &QPlainTextEdit::selectionChanged, this, &FakeVimProxy::updateBlockSelection); + } + + QPalette pal2 = m_widget->palette(); + pal2.setColor(QPalette::Highlight, Qt::transparent); + pal2.setColor(QPalette::HighlightedText, Qt::transparent); + m_widget->setPalette(pal2); + + updateExtraSelections(); +} + +void FakeVimProxy::requestDisableBlockSelection() +{ + auto *editor = qobject_cast(m_widget); + auto *plainEditor = qobject_cast(m_widget); + if (!editor && !plainEditor) + { + return; + } + + QPalette pal = m_widget->parentWidget() != nullptr ? m_widget->parentWidget()->palette() : QApplication::palette(); + + m_blockSelection.clear(); + m_clearSelection.clear(); + + m_widget->setPalette(pal); + + if (editor) + { + disconnect(editor, &QTextEdit::selectionChanged, this, &FakeVimProxy::updateBlockSelection); + } + else + { + disconnect(plainEditor, &QPlainTextEdit::selectionChanged, this, &FakeVimProxy::updateBlockSelection); + } + + updateExtraSelections(); +} + +void FakeVimProxy::updateBlockSelection() +{ + auto *editor = qobject_cast(m_widget); + auto *plainEditor = qobject_cast(m_widget); + if (!editor && !plainEditor) + { + return; + } + + requestSetBlockSelection(editor ? editor->textCursor() : plainEditor->textCursor()); +} + +void FakeVimProxy::requestHasBlockSelection(bool *on) +{ + *on = !m_blockSelection.isEmpty(); +} + +void FakeVimProxy::indentRegion(int beginBlock, int endBlock, QChar typedChar) +{ + QTextDocument *doc = nullptr; + { // in a block so we don't inadvertently use one of them later + auto *plainEditor = qobject_cast(m_widget); + auto *editor = qobject_cast(m_widget); + if (editor) + { + doc = editor->document(); + } + else if (plainEditor) + { + doc = plainEditor->document(); + } + else + { + return; + } + } + Q_ASSERT(doc); + + const int indentSize = FakeVim::Internal::theFakeVimSetting(FakeVim::Internal::ConfigShiftWidth)->value().toInt(); + + QTextBlock startBlock = doc->findBlockByNumber(beginBlock); + + // Record line lengths for mark adjustments + QVector lineLengths(endBlock - beginBlock + 1); + QTextBlock block = startBlock; + + for (int i = beginBlock; i <= endBlock; ++i) + { + const auto line = block.text(); + lineLengths[i - beginBlock] = line.length(); + if (typedChar.unicode() == 0 && line.simplified().isEmpty()) + { + // clear empty lines + QTextCursor cursor(block); + while (!cursor.atBlockEnd()) + cursor.deleteChar(); + } + else + { + const auto previousBlock = block.previous(); + const auto previousLine = previousBlock.isValid() ? previousBlock.text() : QString(); + + int indent = firstNonSpace(previousLine); + if (typedChar == '}') + indent = std::max(0, indent - indentSize); + else if (previousLine.endsWith("{")) + indent += indentSize; + const auto indentString = QString(" ").repeated(indent); + + QTextCursor cursor(block); + cursor.beginEditBlock(); + cursor.movePosition(QTextCursor::StartOfBlock); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, firstNonSpace(line)); + cursor.removeSelectedText(); + cursor.insertText(indentString); + cursor.endEditBlock(); + } + block = block.next(); + } +} + +void FakeVimProxy::moveToMatchingParenthesis(bool *moved, bool *forward, QTextCursor *cursor) +{ + auto isClosingParenthesis = [this](QChar symbol) { + return std::any_of(parenthesisList.begin(), parenthesisList.end(), + [&](auto const &e) { return symbol == e.second; }); + }; + + QChar underCursor = charUnderCursor(cursor, 0); + QChar counterSymbol = getCounterParenthesis(underCursor); + if (counterSymbol == QChar()) + return; + + int direction = -1; + int position = cursor->position(); + if (isClosingParenthesis(counterSymbol)) // if counter symbol is closing, search for it in forward direction + { + direction = 1; + } + + int counter = 1; + int singleCounter = 0; + int doubleCounter = 0; + + while (counter != 0 && position > 0 && position < (document()->characterCount() - 1)) + { + position += direction; + auto character = document()->characterAt(position); + if (character == "\"") + doubleCounter++; + else if (character == "'") + singleCounter++; + else if (character == underCursor && singleCounter % 2 == 0 && doubleCounter % 2 == 0) + ++counter; + else if (character == counterSymbol && singleCounter % 2 == 0 && doubleCounter % 2 == 0) + --counter; + } + + // position stopped at matching parenthesis + if (!counter) + { + *moved = true; + cursor->setPosition(position); + } +} + +void FakeVimProxy::checkForElectricCharacter(bool *result, QChar c) +{ + *result = c == '{' || c == '}'; +} + +QChar FakeVimProxy::charUnderCursor(QTextCursor *cursor, int offset) +{ + + QChar underCursor; + auto text = document()->findBlockByNumber(cursor->blockNumber()).text(); + int index = cursor->positionInBlock(); + index += offset; + if (index < text.size() && index >= 0) + underCursor = text[index]; + return underCursor; +} + +QChar FakeVimProxy::getCounterParenthesis(QChar symbol) +{ + for (auto const &e : parenthesisList) + { + if (symbol == e.first) + return e.second; + if (symbol == e.second) + return e.first; + } + return {}; +} + +int FakeVimProxy::firstNonSpace(const QString &text) +{ + int indent = 0; + while (indent < text.length() && text.at(indent) == ' ') + ++indent; + return indent; +} + +void FakeVimProxy::updateExtraSelections() +{ + auto *editor = qobject_cast(m_widget); + auto *plainEditor = qobject_cast(m_widget); + if (editor) + { + editor->setExtraSelections(m_clearSelection + m_searchSelection + m_blockSelection); + } + else if (plainEditor) + { + plainEditor->setExtraSelections(m_clearSelection + m_searchSelection + m_blockSelection); + } +} + +bool FakeVimProxy::wantSaveAndQuit(const FakeVim::Internal::ExCommand &cmd) +{ + return cmd.cmd == "wq"; +} + +bool FakeVimProxy::wantSave(const FakeVim::Internal::ExCommand &cmd) +{ + return cmd.matches("w", "write") || cmd.matches("wa", "wall"); +} + +bool FakeVimProxy::wantQuit(const FakeVim::Internal::ExCommand &cmd) +{ + return cmd.matches("q", "quit") || cmd.matches("qa", "qall"); +} + +bool FakeVimProxy::save() +{ + return m_mainWindow->save(true, "Vim Save"); +} + +void FakeVimProxy::quit() +{ + m_appWindow->closeWindow(m_mainWindow); +} + +void FakeVimProxy::forceQuit() +{ + m_appWindow->closeWindow(m_mainWindow, true); +} + +bool FakeVimProxy::hasChanges() +{ + return m_mainWindow->isTextChanged(); +} + +QTextDocument *FakeVimProxy::document() const +{ + QTextDocument *doc = nullptr; + if (auto *ed = qobject_cast(m_widget)) + doc = ed->document(); + else if (auto *ed = qobject_cast(m_widget)) + doc = ed->document(); + return doc; +} + +QString FakeVimProxy::content() const +{ + return document()->toPlainText(); +} + +void FakeVimProxy::initHandler(FakeVim::Internal::FakeVimHandler *handler) +{ + handler->handleCommand(QLatin1String("set nopasskeys")); + handler->handleCommand(QLatin1String("set nopasscontrolkey")); + handler->installEventFilter(); + handler->setupWidget(); +} + +void FakeVimProxy::sourceVimRc(FakeVim::Internal::FakeVimHandler *handler) +{ + QTemporaryFile file; + if (file.open()) + { + file.write(SettingsHelper::getFakeVimRC().toLocal8Bit()); + file.close(); + handler->handleCommand("source " + file.fileName()); + } + else + { + LOG_WARN("Failed to open a temporary file to source vimrc."); + handler->handleCommand(QLatin1String("set noexpandtab")); + handler->handleCommand(QLatin1String("set shiftwidth=4")); + handler->handleCommand(QLatin1String("set tabstop=4")); + handler->handleCommand(QLatin1String("set autoindent")); + handler->handleCommand(QLatin1String("set smartindent")); + } +} + +void FakeVimProxy::clearUndoRedo(QWidget *editor) +{ + if (auto *ed = qobject_cast(editor)) + { + ed->setUndoRedoEnabled(false); + ed->setUndoRedoEnabled(true); + } + else if (auto *ed = qobject_cast(editor)) + { + ed->setUndoRedoEnabled(false); + ed->setUndoRedoEnabled(true); + } +} + +void FakeVimProxy::connectSignals(FakeVim::Internal::FakeVimHandler *handler, QWidget *editor, MainWindow *mainWindow, + AppWindow *appWindow) +{ + auto *proxy = new FakeVimProxy(editor, mainWindow, appWindow, handler); + + handler->commandBufferChanged.connect( + [proxy](const QString &contents, int cursorPos, int /*anchorPos*/, int /*messageLevel*/) { + proxy->changeStatusMessage(contents, cursorPos); + }); + handler->statusDataChanged.connect([proxy](const QString &text) { proxy->changeStatusData(text); }); + handler->highlightMatches.connect([proxy](const QString &needle) { proxy->highlightMatches(needle); }); + handler->handleExCommandRequested.connect( + [proxy](bool *handled, const FakeVim::Internal::ExCommand &cmd) { proxy->handleExCommand(handled, cmd); }); + handler->requestSetBlockSelection.connect( + [proxy](const QTextCursor &cursor) { proxy->requestSetBlockSelection(cursor); }); + handler->requestDisableBlockSelection.connect([proxy] { proxy->requestDisableBlockSelection(); }); + handler->requestHasBlockSelection.connect([proxy](bool *on) { proxy->requestHasBlockSelection(on); }); + + handler->indentRegion.connect([proxy](int beginBlock, int endBlock, QChar typedChar) { + proxy->indentRegion(beginBlock, endBlock, typedChar); + }); + handler->checkForElectricCharacter.connect( + [proxy](bool *result, QChar c) { proxy->checkForElectricCharacter(result, c); }); + + handler->moveToMatchingParenthesis.connect([proxy](bool *moved, bool *forward, QTextCursor *cursor) { + proxy->moveToMatchingParenthesis(moved, forward, cursor); + }); + + handler->tabNextRequested.connect([appWindow, mainWindow] { + auto total = appWindow->tabCount(); + auto curr = appWindow->indexOfWindow(mainWindow); + int next = (curr + 1) % total; + if (next != curr) + appWindow->setTabAt(next); + }); + handler->tabPreviousRequested.connect([appWindow, mainWindow] { + auto total = appWindow->tabCount(); + auto curr = appWindow->indexOfWindow(mainWindow); + int last = curr ? curr - 1 : total - 1; + if (last != curr) + appWindow->setTabAt(last); + }); + QObject::connect(proxy, &FakeVimProxy::handleInput, handler, + [handler](const QString &text) { handler->handleInput(text); }); +} + +} // namespace Editor diff --git a/src/Editor/FakeVimProxy.hpp b/src/Editor/FakeVimProxy.hpp new file mode 100644 index 000000000..662ab4df2 --- /dev/null +++ b/src/Editor/FakeVimProxy.hpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2019-2023 Ashar Khan + * + * This file is part of CP Editor. + * + * CP Editor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * I will not be responsible if CP Editor behaves in unexpected way and + * causes your ratings to go down and or lose any important contest. + * + * Believe Software is "Software" and it isn't immune to bugs. + * + */ + +/* Most of the code in this file is under the following copyright + Copyright (c) 2017, Lukas Holecek + This file is part of CopyQ. + CopyQ is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + CopyQ 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 General Public License for more details. + You should have received a copy of the GNU General Public License + along with CopyQ. If not, see . +*/ + +#ifndef FAKE_VIM_PROXY_HPP +#define FAKE_VIM_PROXY_HPP + +#include "Editor/FakeVimCommands.hpp" +#include "third_party/FakeVim/fakevim/fakevimhandler.h" +#include + +class AppWindow; +class MainWindow; +class QLabel; + +namespace Editor +{ +class FakeVimProxy : public QObject +{ + Q_OBJECT + + public: + FakeVimProxy(QWidget *widget, MainWindow *mw, AppWindow *aw, QObject *parent = nullptr); + ~FakeVimProxy() override; + static void sourceVimRc(FakeVim::Internal::FakeVimHandler *handler); + static void initHandler(FakeVim::Internal::FakeVimHandler *handler); + static void clearUndoRedo(QWidget *); + static void connectSignals(FakeVim::Internal::FakeVimHandler *handler, QWidget *editor, MainWindow *mainwindow, + AppWindow *appwin); + + signals: + void handleInput(QString const &key); + + public slots: + void changeStatusData(QString const &); + void highlightMatches(QString const &); + void changeStatusMessage(QString const &, int); + void handleExCommand(bool *, const FakeVim::Internal::ExCommand &); + void requestSetBlockSelection(QTextCursor const &); + void requestDisableBlockSelection(); + void updateBlockSelection(); + void requestHasBlockSelection(bool *); + void indentRegion(int, int, QChar); + void moveToMatchingParenthesis(bool *, bool *, QTextCursor *); + static void checkForElectricCharacter(bool *, QChar); + + private: + static int firstNonSpace(QString const &); + QChar charUnderCursor(QTextCursor *, int); + QChar getCounterParenthesis(QChar); + void updateExtraSelections(); + static bool wantSaveAndQuit(FakeVim::Internal::ExCommand const &); + static bool wantSave(FakeVim::Internal::ExCommand const &); + static bool wantQuit(FakeVim::Internal::ExCommand const &); + bool save(); + void quit(); + void forceQuit(); + void setStatusBar(); + bool hasChanges(); + QTextDocument *document() const; + QString content() const; + + QWidget *m_widget; + MainWindow *m_mainWindow; + AppWindow *m_appWindow; + FakeVimCommands *m_commandHandler; + QLabel *m_statusMessage = nullptr; + QLabel *m_statusData = nullptr; + + QVector> parenthesisList = {{'{', '}'}, {'(', ')'}, {'[', ']'}}; + QList m_searchSelection; + QList m_clearSelection; + QList m_blockSelection; +}; +} // namespace Editor + +#endif // FAKE_VIM_PROXY_HPP diff --git a/src/Settings/PreferencesWindow.cpp b/src/Settings/PreferencesWindow.cpp index 15300fb14..7409c9013 100644 --- a/src/Settings/PreferencesWindow.cpp +++ b/src/Settings/PreferencesWindow.cpp @@ -190,7 +190,7 @@ PreferencesWindow::PreferencesWindow(QWidget *parent) : QMainWindow(parent) AddPageHelper(this) .page(TRKEY("Code Edit"), {"Tab Width", "Cursor Width", "Auto Indent", "Wrap Text", "Auto Complete Parentheses", "Auto Remove Parentheses", - "Tab Jump Out Parentheses", "Replace Tabs", "Highlight Error Line"}) + "Tab Jump Out Parentheses", "Replace Tabs", "Highlight Error Line", "FakeVim/Enable", "FakeVim/RC"}) .dir(TRKEY("Language")) .page(TRKEY("General"), {"Default Language"}) .dir(TRKEY("C++")) diff --git a/src/Settings/settings.json b/src/Settings/settings.json index a67fe623d..02f6175e1 100644 --- a/src/Settings/settings.json +++ b/src/Settings/settings.json @@ -5,6 +5,12 @@ "default": 4, "param": "QVariantList {1,16}", "tip": "The width of the tab character, or the number of spaces of an indent", + "depends": [ + { + "name": "FakeVim/Enable", + "check":"return !var.toBool();" + } + ], "old": ["tab_stop"] }, { @@ -368,24 +374,48 @@ "type": "bool", "default": true, "tip": "Automatically complete a pair of parentheses when typing the left element of it,\nand move out of it when typing the right element of it.\nThis can be overridden for each parenthesis in each language.", + "depends": [ + { + "name": "FakeVim/Enable", + "check":"return !var.toBool();" + } + ], "old": ["auto_parenthesis"] }, { "name": "Auto Remove Parentheses", "type": "bool", "default": true, + "depends": [ + { + "name": "FakeVim/Enable", + "check":"return !var.toBool();" + } + ], "tip": "Automatically delete the whole pair of parentheses when deleting\nthe left element of it if the two elements are adjacent.\nThis can be overridden for each parenthesis in each language." }, { "name": "Tab Jump Out Parentheses", "desc": "Jump out of a parenthesis by pressing Tab", "type": "bool", + "depends": [ + { + "name": "FakeVim/Enable", + "check":"return !var.toBool();" + } + ], "tip": "When this is enabled, you can use Tab instead of the\nclosing parenthesis to jump out of a parenthesis.\nThis can be overridden for each parenthesis in each language." }, { "name": "Auto Indent", "type": "bool", "default": true, + "depends": [ + { + "name": "FakeVim/Enable", + "check":"return !var.toBool();" + } + ], "tip": "Add an indent when entering a new line after a \"{\"." }, { @@ -436,6 +466,12 @@ "name": "Replace Tabs", "desc": "Replace tabs by spaces", "type": "bool", + "depends": [ + { + "name": "FakeVim/Enable", + "check":"return !var.toBool();" + } + ], "tip": "Use spaces instead of a tab character." }, { @@ -1172,6 +1208,25 @@ "default": false, "notr": true }, + { + "name": "FakeVim/Enable", + "desc": "Enable Vim Emulation", + "type": "bool", + "tip": "Enable vim emulation in Code Editor" + }, + { + "name": "FakeVim/RC", + "desc": "Vim Configuration", + "type": "QString", + "default": "set noexpandtab\nset autoindent\nset smartindent\nset tabstop=4\nset shiftwidth=4", + "ui": "QPlainTextEdit", + "tip": "The contents of Vim RC. It is loaded everytime vim emulation starts. \nNot all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands", + "depends": [ + { + "name": "FakeVim/Enable" + } + ] + }, { "name": "WakaTime/Path", "desc": "Path", diff --git a/src/appwindow.cpp b/src/appwindow.cpp index 46a6db078..c7cf6d087 100644 --- a/src/appwindow.cpp +++ b/src/appwindow.cpp @@ -161,7 +161,7 @@ AppWindow::AppWindow(bool cpp, bool java, bool python, bool noHotExit, int numbe void AppWindow::finishConstruction() { - if (ui->tabWidget->count() == 0) + if (tabCount() == 0) openTab(""); #ifdef Q_OS_WIN @@ -198,7 +198,7 @@ AppWindow::~AppWindow() { LOG_INFO("Destruction started"); saveSettings(); - while (ui->tabWidget->count()) + while (tabCount()) { auto *tmp = ui->tabWidget->widget(0); ui->tabWidget->removeTab(0); @@ -268,7 +268,7 @@ void AppWindow::changeEvent(QEvent *event) /******************** PRIVATE METHODS ********************/ void AppWindow::setConnections() { - connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, &AppWindow::onTabCloseRequested); + connect(ui->tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) { closeTab(index); }); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppWindow::onTabChanged); ui->tabWidget->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->tabWidget->tabBar(), &QTabBar::customContextMenuRequested, this, &AppWindow::onTabContextMenuRequested); @@ -396,11 +396,16 @@ void AppWindow::maybeSetHotkeys() } } -bool AppWindow::closeTab(int index) +bool AppWindow::closeTab(int index, bool noConfirmQuit) { LOG_INFO(INFO_OF(index)); auto *tmp = windowAt(index); - if (tmp->closeConfirm()) + if (!tmp) + { + LOG_WARN("Current window is nullptr"); + return false; + } + if (noConfirmQuit || tmp->closeConfirm()) { ui->tabWidget->removeTab(index); onEditorFileChanged(); @@ -410,6 +415,11 @@ bool AppWindow::closeTab(int index) return false; } +bool AppWindow::closeWindow(MainWindow *window, bool noConfirmQuit) +{ + return closeTab(indexOfWindow(window), noConfirmQuit); +} + void AppWindow::saveSettings() { if (!this->isMaximized()) @@ -431,9 +441,8 @@ void AppWindow::openTab(MainWindow *window, MainWindow *after) connect(window, &MainWindow::compileOrRunTriggered, this, &AppWindow::onCompileOrRunTriggered); connect(window, &MainWindow::fileSaved, this, &AppWindow::onFileSaved); - ui->tabWidget->setCurrentIndex( - ui->tabWidget->insertTab(after ? ui->tabWidget->indexOf(after) + 1 : ui->tabWidget->currentIndex() + 1, window, - window->getTabTitle(false, true))); + setTabAt(ui->tabWidget->insertTab(after ? indexOfWindow(after) + 1 : ui->tabWidget->currentIndex() + 1, window, + window->getTabTitle(false, true))); window->getEditor()->setFocus(); onEditorFileChanged(); @@ -550,7 +559,7 @@ bool AppWindow::quit() { LOG_INFO("quit() called without hotExit"); on_actionCloseAll_triggered(); - if (ui->tabWidget->count() >= 1) + if (tabCount() >= 1) { LOG_INFO("Closing is cancelled"); return false; @@ -568,7 +577,7 @@ int AppWindow::getNewUntitledIndex() { int index = 0; QSet vis; - for (int t = 0; t < ui->tabWidget->count(); ++t) + for (int t = 0; t < tabCount(); ++t) { auto *tmp = windowAt(t); if (tmp->isUntitled() && tmp->getProblemURL().isEmpty()) @@ -692,7 +701,7 @@ void AppWindow::on_actionSaveAs_triggered() void AppWindow::on_actionSaveAll_triggered() { - for (int t = 0; t < ui->tabWidget->count(); ++t) + for (int t = 0; t < tabCount(); ++t) { auto *tmp = windowAt(t); if (!tmp->save(true, tr("Save All"))) @@ -709,7 +718,7 @@ void AppWindow::on_actionCloseCurrent_triggered() void AppWindow::on_actionCloseAll_triggered() { - for (int t = 0; t < ui->tabWidget->count(); t++) + for (int t = 0; t < tabCount(); t++) { if (closeTab(t)) --t; @@ -720,7 +729,7 @@ void AppWindow::on_actionCloseAll_triggered() void AppWindow::on_actionCloseSaved_triggered() { - for (int t = 0; t < ui->tabWidget->count(); t++) + for (int t = 0; t < tabCount(); t++) if (!windowAt(t)->isTextChanged() && closeTab(t)) --t; } @@ -848,11 +857,6 @@ bool AppWindow::forceClose() return close(); } -void AppWindow::onTabCloseRequested(int index) -{ - closeTab(index); -} - void AppWindow::onTabChanged(int index) { LOG_INFO(INFO_OF(index)); @@ -902,6 +906,7 @@ void AppWindow::onTabChanged(int index) connect(tmp->getRightSplitter(), &QSplitter::splitterMoved, this, &AppWindow::onRightSplitterMoved); triggerWakaTime(tmp); + tmp->getEditor()->setFocus(); } void AppWindow::onEditorFileChanged() @@ -910,7 +915,7 @@ void AppWindow::onEditorFileChanged() { QMap> tabsByName; - for (int t = 0; t < ui->tabWidget->count(); ++t) + for (int t = 0; t < tabCount(); ++t) { tabsByName[windowAt(t)->getTabTitle(false, false)].push_back(t); } @@ -948,7 +953,7 @@ void AppWindow::onEditorFileChanged() void AppWindow::onEditorTextChanged(MainWindow *window) { - int index = ui->tabWidget->indexOf(window); + int index = indexOfWindow(window); if (index != -1) { auto title = ui->tabWidget->tabText(index); @@ -1033,7 +1038,7 @@ void AppWindow::onSettingsApplied(const QString &pagePath) { LOG_INFO("Apply settings for " << INFO_OF(pagePath)); - for (int i = 0; i < ui->tabWidget->count(); ++i) + for (int i = 0; i < tabCount(); ++i) { windowAt(i)->applySettings(pagePath); onEditorTextChanged(windowAt(i)); @@ -1106,11 +1111,11 @@ void AppWindow::onIncomingCompanionRequest(const Extensions::CompanionData &data { LOG_INFO("Request from competitive companion arrived"); - for (int i = 0; i < ui->tabWidget->count(); ++i) + for (int i = 0; i < tabCount(); ++i) { if (windowAt(i)->getProblemURL() == data.url) { - ui->tabWidget->setCurrentIndex(i); + setTabAt(i); currentWindow()->applyCompanion(data); return; } @@ -1166,18 +1171,18 @@ void AppWindow::onRightSplitterMoved() SettingsHelper::setRightSplitterSize(splitter->saveState()); } -void AppWindow::openTab(const QString &path, MainWindow *after) +void AppWindow::openTab(const QString &path, QString lang, MainWindow *after) { LOG_INFO("OpenTab Path is " << path); if (!path.isEmpty()) { auto fileInfo = QFileInfo(path); - for (int t = 0; t < ui->tabWidget->count(); t++) + for (int t = 0; t < tabCount(); t++) { auto tPath = qobject_cast(ui->tabWidget->widget(t))->getFilePath(); if (path == tPath || (fileInfo.exists() && fileInfo == QFileInfo(tPath))) { - ui->tabWidget->setCurrentIndex(t); + setTabAt(t); return; } } @@ -1185,16 +1190,18 @@ void AppWindow::openTab(const QString &path, MainWindow *after) auto *newWindow = new MainWindow(path, getNewUntitledIndex(), this); - QString lang = SettingsHelper::getDefaultLanguage(); + if (lang.isEmpty()) + { - auto suffix = QFileInfo(path).suffix(); + auto suffix = QFileInfo(path).suffix(); - if (Util::cppSuffix.contains(suffix)) - lang = "C++"; - else if (Util::javaSuffix.contains(suffix)) - lang = "Java"; - else if (Util::pythonSuffix.contains(suffix)) - lang = "Python"; + if (Util::cppSuffix.contains(suffix)) + lang = "C++"; + else if (Util::javaSuffix.contains(suffix)) + lang = "Java"; + else if (Util::pythonSuffix.contains(suffix)) + lang = "Python"; + } newWindow->setLanguage(lang); @@ -1405,9 +1412,9 @@ void AppWindow::on_actionToggleBlockComment_triggered() void AppWindow::onConfirmTriggered(MainWindow *widget) { - int index = ui->tabWidget->indexOf(widget); + int index = indexOfWindow(widget); if (index != -1) - ui->tabWidget->setCurrentIndex(index); + setTabAt(index); } void AppWindow::onTabContextMenuRequested(const QPoint &pos) @@ -1425,19 +1432,19 @@ void AppWindow::onTabContextMenuRequested(const QPoint &pos) tabMenu->addAction(tr("Close"), [index, this] { closeTab(index); }); tabMenu->addAction(tr("Close Others"), [window, this] { - for (int i = 0; i < ui->tabWidget->count(); ++i) + for (int i = 0; i < tabCount(); ++i) if (windowAt(i) != window && closeTab(i)) --i; }); tabMenu->addAction(tr("Close to the Left"), [window, this] { - for (int i = 0; i < ui->tabWidget->count() && windowAt(i) != window; ++i) + for (int i = 0; i < tabCount() && windowAt(i) != window; ++i) if (closeTab(i)) --i; }); tabMenu->addAction(tr("Close to the Right"), [index, this] { - for (int i = index + 1; i < ui->tabWidget->count(); ++i) + for (int i = index + 1; i < tabCount(); ++i) if (closeTab(i)) --i; }); @@ -1518,6 +1525,20 @@ void AppWindow::onTabContextMenuRequested(const QPoint &pos) } } +void AppWindow::setTabAt(int index) +{ + ui->tabWidget->setCurrentIndex(index); +} + +int AppWindow::indexOfWindow(MainWindow *window) +{ + return ui->tabWidget->indexOf(window); +} + +int AppWindow::tabCount() const +{ + return ui->tabWidget->count(); +} MainWindow *AppWindow::currentWindow() { int current = ui->tabWidget->currentIndex(); diff --git a/src/appwindow.hpp b/src/appwindow.hpp index cd412b336..2ba88a0d8 100644 --- a/src/appwindow.hpp +++ b/src/appwindow.hpp @@ -35,6 +35,11 @@ namespace Ui { class AppWindow; } +namespace Editor +{ +class FakeVimCommands; +class FakeVimProxy; +} // namespace Editor namespace Extensions { @@ -84,6 +89,10 @@ class AppWindow : public QMainWindow void showOnTop(); + bool closeTab(int index, bool noConfirmQuit = false); + + bool closeWindow(MainWindow *window, bool noConfirmQuit = false); + private slots: // UI Slots void on_actionSupportUs_triggered(); @@ -194,8 +203,6 @@ class AppWindow : public QMainWindow void onEditorLanguageChanged(MainWindow *window); - void onTabCloseRequested(int); - void onTabChanged(int); void onLSPTimerElapsedCpp(); @@ -214,7 +221,7 @@ class AppWindow : public QMainWindow void onViewModeToggle(); - void openTab(const QString &path, MainWindow *after = nullptr); + void openTab(const QString &path, QString lang = "", MainWindow *after = nullptr); void onFileSaved(MainWindow *window); @@ -258,7 +265,6 @@ class AppWindow : public QMainWindow void saveSettings(); QVector hotkeyObjects; void maybeSetHotkeys(); - bool closeTab(int index); void openTab(MainWindow *window, MainWindow *after = nullptr); void openTab(const MainWindow::EditorStatus &status, bool duplicate = false, MainWindow *after = nullptr); void openTabs(const QStringList &paths); @@ -266,6 +272,9 @@ class AppWindow : public QMainWindow QStringList openFolder(const QString &path, bool cpp, bool java, bool python, int depth); void openContest(Widgets::ContestDialog::ContestData const &data); bool quit(); + void setTabAt(int index); + int indexOfWindow(MainWindow *window); + int tabCount() const; int getNewUntitledIndex(); void reAttachLanguageServer(MainWindow *window); void triggerWakaTime(MainWindow *window, bool isWrite = false); @@ -274,6 +283,8 @@ class AppWindow : public QMainWindow MainWindow *windowAt(int index); friend class Core::SessionManager; + friend class Editor::FakeVimCommands; + friend class Editor::FakeVimProxy; }; #endif // APPWINDOW_HPP diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 41f85013b..7fd1bd2fc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -23,6 +23,7 @@ #include "Core/MessageLogger.hpp" #include "Core/Runner.hpp" #include "Editor/CodeEditor.hpp" +#include "Editor/FakeVimProxy.hpp" #include "Extensions/CFTool.hpp" #include "Extensions/ClangFormatter.hpp" #include "Extensions/CompanionServer.hpp" @@ -47,6 +48,7 @@ #include #include "../ui/ui_mainwindow.h" +#include "third_party/FakeVim/fakevim/fakevimhandler.h" static const int MAX_NUMBER_OF_RECENT_FILES = 20; @@ -108,6 +110,13 @@ MainWindow::~MainWindow() delete editor; delete log; delete stopwatch; + + if (fakevimHandler) + { + fakevimHandler->disconnectFromEditor(); + fakevimHandler->deleteLater(); + fakevimHandler = nullptr; + } } void MainWindow::setEditor() @@ -630,7 +639,26 @@ void MainWindow::applySettings(const QString &pagePath) if (pageChanged("Code Edit") || pagePath.startsWith("Appearance/") || pageChanged(QString("Language/%1/%1 Parentheses").arg(language))) + { + if (pageChanged("Code Edit")) + { + editor->setVimCursor(SettingsHelper::isFakeVimEnable()); + ui->cursorInfo->setVisible(!SettingsHelper::isFakeVimEnable()); + delete fakevimHandler; + fakevimHandler = nullptr; + setStatusBar(nullptr); + if (SettingsHelper::isFakeVimEnable()) + { + fakevimHandler = new FakeVim::Internal::FakeVimHandler(editor, nullptr); + + Editor::FakeVimProxy::connectSignals(fakevimHandler, editor, this, appWindow); + Editor::FakeVimProxy::initHandler(fakevimHandler); + Editor::FakeVimProxy::sourceVimRc(fakevimHandler); + } + Editor::FakeVimProxy::clearUndoRedo(editor); + } editor->applySettings(language); + } if (!isLanguageSet && pageChanged("Language/General")) { diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index 5c1f8c2a5..d0be80ab8 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -25,7 +25,8 @@ class MessageLogger; namespace Editor { class CodeEditor; -} +class FakeVimCommands; +} // namespace Editor class QFileSystemWatcher; class QPushButton; class QSplitter; @@ -50,6 +51,13 @@ class CFTool; struct CompanionData; } // namespace Extensions +namespace FakeVim +{ +namespace Internal +{ +class FakeVimHandler; +} +} // namespace FakeVim namespace Widgets { class TestCases; @@ -237,6 +245,8 @@ class MainWindow : public QMainWindow int customTimeLimit = -1; // the custom time limit for this tab, -1 represents for the same as settings QString customCompileCommand; // the custom compile command for this tab, empty represents for the same as settings + FakeVim::Internal::FakeVimHandler *fakevimHandler = nullptr; + void setEditor(); void compile(); void run(); @@ -258,5 +268,6 @@ class MainWindow : public QMainWindow virtual void hideEvent(QHideEvent *event) override; virtual void showEvent(QShowEvent *event) override; + friend class Editor::FakeVimCommands; }; #endif // MAINWINDOW_HPP From 636e280b855db622dd7316b0d45a88792375d5d3 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 16 Dec 2023 22:56:19 +0530 Subject: [PATCH 5/8] Update translations --- translations/el_GR.ts | 48 +++++++++++++++++++++++++++++++++++++++ translations/es_MX.ts | 48 +++++++++++++++++++++++++++++++++++++++ translations/pt_BR.ts | 48 +++++++++++++++++++++++++++++++++++++++ translations/ru_RU.ts | 48 +++++++++++++++++++++++++++++++++++++++ translations/zh_CN.ts | 52 +++++++++++++++++++++++++++++++++++++++++-- translations/zh_TW.ts | 48 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 290 insertions(+), 2 deletions(-) diff --git a/translations/el_GR.ts b/translations/el_GR.ts index da7b3d828..3ad397bf4 100644 --- a/translations/el_GR.ts +++ b/translations/el_GR.ts @@ -849,6 +849,37 @@ Press any key to exit Ένα άγνωστο σφάλμα συνέβη στην διεργασία του LSP + + FakeVimCommands + + `new` requires no argument or one of 'cpp', 'java' and 'python', got [%1] + + + + [%1] is not C++, Python or Java source file + + + + [%1] does not exist. To open a tab with a non-existing file, use `open!` instead + + + + [%1] is not a number + + + + %1 is out of range [1, %2] + + + + [%1] is not a valid view mode. It should be one of 'split' and 'edit' + + + + %1 is not a valid language name. It should be one of 'cpp', 'java' and 'python' + + + FindReplaceDialog @@ -2696,6 +2727,23 @@ This may reduce distractions caused by stopwatch updates. Highlight lines containing diagnostics + + Enable Vim Emulation + + + + Enable vim emulation in Code Editor + + + + Vim Configuration + + + + The contents of Vim RC. It is loaded everytime vim emulation starts. +Not all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands + + Color of error messages diff --git a/translations/es_MX.ts b/translations/es_MX.ts index c1a04aead..5ee5895b9 100644 --- a/translations/es_MX.ts +++ b/translations/es_MX.ts @@ -849,6 +849,37 @@ Presione cualquier tecla para salir Ha ocurrido un error desconocido en el proceso LSP + + FakeVimCommands + + `new` requires no argument or one of 'cpp', 'java' and 'python', got [%1] + + + + [%1] is not C++, Python or Java source file + + + + [%1] does not exist. To open a tab with a non-existing file, use `open!` instead + + + + [%1] is not a number + + + + %1 is out of range [1, %2] + + + + [%1] is not a valid view mode. It should be one of 'split' and 'edit' + + + + %1 is not a valid language name. It should be one of 'cpp', 'java' and 'python' + + + FindReplaceDialog @@ -2701,6 +2732,23 @@ Es una lista de <nombre de ruta predeterminada>, separadas por comas, y pu Highlight lines containing diagnostics + + Enable Vim Emulation + + + + Enable vim emulation in Code Editor + + + + Vim Configuration + + + + The contents of Vim RC. It is loaded everytime vim emulation starts. +Not all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands + + Color of error messages diff --git a/translations/pt_BR.ts b/translations/pt_BR.ts index 7d2c92718..177bf96e4 100644 --- a/translations/pt_BR.ts +++ b/translations/pt_BR.ts @@ -849,6 +849,37 @@ Pressionar qualquer tecla para sair Ocorrência de erro desconhecido no processo LSP + + FakeVimCommands + + `new` requires no argument or one of 'cpp', 'java' and 'python', got [%1] + + + + [%1] is not C++, Python or Java source file + + + + [%1] does not exist. To open a tab with a non-existing file, use `open!` instead + + + + [%1] is not a number + + + + %1 is out of range [1, %2] + + + + [%1] is not a valid view mode. It should be one of 'split' and 'edit' + + + + %1 is not a valid language name. It should be one of 'cpp', 'java' and 'python' + + + FindReplaceDialog @@ -2700,6 +2731,23 @@ Isso pode reduzir distrações causadas pelas atualizações do cronômetro.Highlight lines containing diagnostics + + Enable Vim Emulation + + + + Enable vim emulation in Code Editor + + + + Vim Configuration + + + + The contents of Vim RC. It is loaded everytime vim emulation starts. +Not all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands + + Color of error messages diff --git a/translations/ru_RU.ts b/translations/ru_RU.ts index c1ffacba0..f2d764904 100644 --- a/translations/ru_RU.ts +++ b/translations/ru_RU.ts @@ -849,6 +849,37 @@ Press any key to exit Произошла неизвестная ошибка в Процессе LSP + + FakeVimCommands + + `new` requires no argument or one of 'cpp', 'java' and 'python', got [%1] + + + + [%1] is not C++, Python or Java source file + + + + [%1] does not exist. To open a tab with a non-existing file, use `open!` instead + + + + [%1] is not a number + + + + %1 is out of range [1, %2] + + + + [%1] is not a valid view mode. It should be one of 'split' and 'edit' + + + + %1 is not a valid language name. It should be one of 'cpp', 'java' and 'python' + + + FindReplaceDialog @@ -2698,6 +2729,23 @@ This may reduce distractions caused by stopwatch updates. Highlight lines containing diagnostics + + Enable Vim Emulation + + + + Enable vim emulation in Code Editor + + + + Vim Configuration + + + + The contents of Vim RC. It is loaded everytime vim emulation starts. +Not all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands + + Color of error messages diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts index 15baa6e0c..206e487b8 100755 --- a/translations/zh_CN.ts +++ b/translations/zh_CN.ts @@ -849,6 +849,37 @@ Press any key to exit Language server 发生未知错误 + + FakeVimCommands + + `new` requires no argument or one of 'cpp', 'java' and 'python', got [%1] + + + + [%1] is not C++, Python or Java source file + + + + [%1] does not exist. To open a tab with a non-existing file, use `open!` instead + + + + [%1] is not a number + + + + %1 is out of range [1, %2] + + + + [%1] is not a valid view mode. It should be one of 'split' and 'edit' + + + + %1 is not a valid language name. It should be one of 'cpp', 'java' and 'python' + + + FindReplaceDialog @@ -2691,13 +2722,30 @@ This may reduce distractions caused by stopwatch updates. Highlight lines containing diagnostics 高亮包含代码提示的行 + + Enable Vim Emulation + + + + Enable vim emulation in Code Editor + + + + Vim Configuration + + + + The contents of Vim RC. It is loaded everytime vim emulation starts. +Not all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands + + Color of error messages - 错误信息的颜色 + Color of warning messages - 警告信息的颜色 + diff --git a/translations/zh_TW.ts b/translations/zh_TW.ts index f28247602..7467d9404 100644 --- a/translations/zh_TW.ts +++ b/translations/zh_TW.ts @@ -855,6 +855,37 @@ Press any key to exit LSP 處理程序發生了未知的錯誤 + + FakeVimCommands + + `new` requires no argument or one of 'cpp', 'java' and 'python', got [%1] + + + + [%1] is not C++, Python or Java source file + + + + [%1] does not exist. To open a tab with a non-existing file, use `open!` instead + + + + [%1] is not a number + + + + %1 is out of range [1, %2] + + + + [%1] is not a valid view mode. It should be one of 'split' and 'edit' + + + + %1 is not a valid language name. It should be one of 'cpp', 'java' and 'python' + + + FindReplaceDialog @@ -2715,6 +2746,23 @@ This may reduce distractions caused by stopwatch updates. Highlight lines containing diagnostics + + Enable Vim Emulation + + + + Enable vim emulation in Code Editor + + + + Vim Configuration + + + + The contents of Vim RC. It is loaded everytime vim emulation starts. +Not all vim commands are supported, please check https://github.com/cpeditor/FakeVim for list of supported commands + + Color of error messages From 801649d4d688b85f471db031612f2295b24f80c2 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 16 Dec 2023 23:10:50 +0530 Subject: [PATCH 6/8] update: changelog file --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 096b177b8..ddff84b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Use [Kate's Syntax highlighter](https://api.kde.org/frameworks/syntax-highlighting/html/) engine for Code Highlighting. (#1101) - Add options to select error/warning messages colors for message logger. (#521 and #1247) +- Add vim emulation with [custom commands](to be updated after docs PR is ready). (#220 and #1270) ### Fixied From da3392af6d41fa06ebb154fdcc4923149629508f Mon Sep 17 00:00:00 2001 From: Ashar Date: Sat, 16 Dec 2023 23:55:58 +0530 Subject: [PATCH 7/8] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddff84b4c..644e7ac03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Use [Kate's Syntax highlighter](https://api.kde.org/frameworks/syntax-highlighting/html/) engine for Code Highlighting. (#1101) - Add options to select error/warning messages colors for message logger. (#521 and #1247) -- Add vim emulation with [custom commands](to be updated after docs PR is ready). (#220 and #1270) +- Add vim emulation with [custom commands](https://cpeditor.org/docs/preferences/code-edit/#custom-vim-commands). (#220 and #1270) ### Fixied From 04305b0f06fd69dd93f8ac8198779144f4b161dc Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 17 Feb 2024 20:51:44 +0530 Subject: [PATCH 8/8] fix: Disable cursor blinking in vim mode --- src/Editor/CodeEditor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Editor/CodeEditor.cpp b/src/Editor/CodeEditor.cpp index c2a331516..7275f6e8b 100644 --- a/src/Editor/CodeEditor.cpp +++ b/src/Editor/CodeEditor.cpp @@ -410,6 +410,8 @@ void CodeEditor::setHighlightCurrentLine(bool enabled) void CodeEditor::setVimCursor(bool value) { m_vimCursor = value; + // Do not flash the cursor in vim mode + QApplication::setCursorFlashTime(m_vimCursor ? 0 : 1000); setOverwriteMode(false); updateCursorWidth();