// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "fakevimactions.h" #include "fakevimhandler.h" #include "fakevimtr.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 #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 using namespace TextEditor; using namespace Core; using namespace Utils; namespace FakeVim::Internal { const char INSTALL_HANDLER[] = "TextEditor.FakeVimHandler"; const char SETTINGS_CATEGORY[] = "D.FakeVim"; const char SETTINGS_ID[] = "A.FakeVim.General"; const char SETTINGS_EX_CMDS_ID[] = "B.FakeVim.ExCommands"; const char SETTINGS_USER_CMDS_ID[] = "C.FakeVim.UserCommands"; static class FakeVimPlugin *dd = nullptr; class MiniBuffer : public QStackedWidget { Q_OBJECT public: MiniBuffer() : m_label(new QLabel(this)) , m_edit(new QLineEdit(this)) { connect(m_edit, &QLineEdit::textEdited, this, &MiniBuffer::changed); connect(m_edit, &QLineEdit::cursorPositionChanged, this, &MiniBuffer::changed); connect(m_edit, &QLineEdit::selectionChanged, this, &MiniBuffer::changed); m_label->setTextInteractionFlags(Qt::TextSelectableByMouse); addWidget(m_label); addWidget(m_edit); m_hideTimer.setSingleShot(true); m_hideTimer.setInterval(8000); connect(&m_hideTimer, &QTimer::timeout, this, &QWidget::hide); } void setContents(const QString &contents, int cursorPos, int anchorPos, int messageLevel, FakeVimHandler *eventFilter) { if (cursorPos != -1) { { QSignalBlocker blocker(m_edit); m_label->clear(); m_edit->setText(contents); if (anchorPos != -1 && anchorPos != cursorPos) m_edit->setSelection(anchorPos, cursorPos - anchorPos); else m_edit->setCursorPosition(cursorPos); } setCurrentWidget(m_edit); m_edit->setFocus(); } else { if (contents.isEmpty()) { if (m_lastMessageLevel == MessageMode) hide(); else m_hideTimer.start(); } else { m_hideTimer.stop(); show(); m_label->setText(contents); QString css; if (messageLevel == MessageError) { css = "border:1px solid rgba(255,255,255,150);" "background-color:rgba(255,0,0,100);"; } else if (messageLevel == MessageWarning) { css = "border:1px solid rgba(255,255,255,120);" "background-color:rgba(255,255,0,20);"; } else if (messageLevel == MessageShowCmd) { css = "border:1px solid rgba(255,255,255,120);" "background-color:rgba(100,255,100,30);"; } m_label->setStyleSheet(QString::fromLatin1( "*{border-radius:2px;padding-left:4px;padding-right:4px;%1}").arg(css)); } if (m_edit->hasFocus()) emit edited(QString(), -1, -1); setCurrentWidget(m_label); } if (m_eventFilter != eventFilter) { if (m_eventFilter != nullptr) { m_edit->removeEventFilter(m_eventFilter); disconnect(this, &MiniBuffer::edited, nullptr, nullptr); } if (eventFilter != nullptr) { m_edit->installEventFilter(eventFilter); connect(this, &MiniBuffer::edited, eventFilter, &FakeVimHandler::miniBufferTextEdited); } m_eventFilter = eventFilter; } m_lastMessageLevel = messageLevel; } QSize sizeHint() const override { QSize size = QWidget::sizeHint(); // reserve maximal width for line edit widget return currentWidget() == m_edit ? QSize(maximumWidth(), size.height()) : size; } signals: void edited(const QString &text, int cursorPos, int anchorPos); private: void changed() { const int cursorPos = m_edit->cursorPosition(); int anchorPos = m_edit->selectionStart(); if (anchorPos == cursorPos) anchorPos = cursorPos + m_edit->selectedText().length(); emit edited(m_edit->text(), cursorPos, anchorPos); } QLabel *m_label; QLineEdit *m_edit; QObject *m_eventFilter = nullptr; QTimer m_hideTimer; int m_lastMessageLevel = MessageMode; }; class RelativeNumbersColumn : public QWidget { public: RelativeNumbersColumn(TextEditorWidget *baseTextEditor) : QWidget(baseTextEditor) , m_editor(baseTextEditor) { setAttribute(Qt::WA_TransparentForMouseEvents, true); m_timerUpdate.setSingleShot(true); m_timerUpdate.setInterval(0); connect(&m_timerUpdate, &QTimer::timeout, this, &RelativeNumbersColumn::followEditorLayout); auto start = QOverload<>::of(&QTimer::start); connect(m_editor, &QPlainTextEdit::cursorPositionChanged, &m_timerUpdate, start); connect(m_editor->verticalScrollBar(), &QAbstractSlider::valueChanged, &m_timerUpdate, start); connect(m_editor->document(), &QTextDocument::contentsChanged, &m_timerUpdate, start); connect(TextEditorSettings::instance(), &TextEditorSettings::displaySettingsChanged, &m_timerUpdate, start); m_editor->installEventFilter(this); followEditorLayout(); } protected: void paintEvent(QPaintEvent *event) override { QTextCursor firstVisibleCursor = m_editor->cursorForPosition(QPoint(0, 0)); QTextBlock firstVisibleBlock = firstVisibleCursor.block(); if (firstVisibleCursor.positionInBlock() > 0) { firstVisibleBlock = firstVisibleBlock.next(); firstVisibleCursor.setPosition(firstVisibleBlock.position()); } // Find relative number for the first visible line. QTextBlock block = m_editor->textCursor().block(); bool forward = firstVisibleBlock.blockNumber() > block.blockNumber(); int n = 0; while (block.isValid() && block != firstVisibleBlock) { block = forward ? block.next() : block.previous(); if (block.isVisible()) n += forward ? 1 : -1; } // Copy colors from extra area palette. QPainter p(this); QPalette pal = m_editor->extraArea()->palette(); const QColor fg = pal.color(QPalette::Dark); const QColor bg = pal.color(QPalette::Window); p.setPen(fg); // Draw relative line numbers. QRect rect(0, m_editor->cursorRect(firstVisibleCursor).y(), width(), m_lineSpacing); bool hideLineNumbers = m_editor->lineNumbersVisible(); while (block.isValid()) { if (block.isVisible()) { if (n != 0 && rect.intersects(event->rect())) { const int line = qAbs(n); const QString number = QString::number(line); if (hideLineNumbers) p.fillRect(rect, bg); if (hideLineNumbers || line < 100) p.drawText(rect, Qt::AlignRight | Qt::AlignVCenter, number); } rect.translate(0, m_lineSpacing * block.lineCount()); if (rect.y() > height()) break; ++n; } block = block.next(); } } bool eventFilter(QObject *, QEvent *event) override { if (event->type() == QEvent::Resize || event->type() == QEvent::Move) m_timerUpdate.start(); return false; } private: void followEditorLayout() { QTextCursor tc = m_editor->textCursor(); m_currentPos = tc.position(); m_lineSpacing = m_editor->document()->documentLayout()->blockBoundingRect(tc.block()).height(); setFont(m_editor->extraArea()->font()); // Follow geometry of normal line numbers if visible, // otherwise follow geometry of marks (breakpoints etc.). QRect rect = m_editor->extraArea()->geometry().adjusted(0, 0, -3, 0); bool marksVisible = m_editor->marksVisible(); bool lineNumbersVisible = m_editor->lineNumbersVisible(); bool foldMarksVisible = m_editor->codeFoldingVisible(); if (marksVisible && lineNumbersVisible) rect.setLeft(m_lineSpacing); if (foldMarksVisible && (marksVisible || lineNumbersVisible)) rect.setRight(rect.right() - (m_lineSpacing + m_lineSpacing % 2)); setGeometry(rect); update(); } int m_currentPos = 0; int m_lineSpacing = 0; TextEditorWidget *m_editor; QTimer m_timerUpdate; }; /////////////////////////////////////////////////////////////////////// // // FakeVimPlugin // /////////////////////////////////////////////////////////////////////// using ExCommandMap = QMap; using UserCommandMap = QMap; class FakeVimPlugin final : public ExtensionSystem::IPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "FakeVim.json") public: FakeVimPlugin(); ~FakeVimPlugin() final { dd = nullptr; } void initialize() final; void extensionsInitialized() final { m_miniBuffer = new MiniBuffer; StatusBarManager::addStatusBarWidget(m_miniBuffer, StatusBarManager::LastLeftAligned); } ExtensionSystem::IPlugin::ShutdownFlag aboutToShutdown() final { StatusBarManager::destroyStatusBarWidget(m_miniBuffer); m_miniBuffer = nullptr; return SynchronousShutdown; } void editorOpened(Core::IEditor *); void editorAboutToClose(Core::IEditor *); void currentEditorAboutToChange(Core::IEditor *); void allDocumentsRenamed(const FilePath &oldPath, const FilePath &newPath); void documentRenamed(Core::IDocument *document, const FilePath &oldPath, const FilePath &newPath); void renameFileNameInEditors(const FilePath &oldPath, const FilePath &newPath); void setUseFakeVim(bool on); void setUseFakeVimInternal(bool on); void quitFakeVim(); void fold(FakeVimHandler *handler, int depth, bool fold); void maybeReadVimRc(); void setShowRelativeLineNumbers(bool on); void setCursorBlinking(bool on); void resetCommandBuffer(); void showCommandBuffer(FakeVimHandler *handler, const QString &contents, int cursorPos, int anchorPos, int messageLevel); void handleExCommand(FakeVimHandler *handler, bool *handled, const ExCommand &cmd); void readSettings(); void handleDelayedQuitAll(bool forced); void handleDelayedQuit(bool forced, Core::IEditor *editor); void userActionTriggered(int key); void updateAllHightLights(); void switchToFile(int n); int currentFile() const; void createRelativeNumberWidget(IEditor *editor); signals: void delayedQuitRequested(bool forced, Core::IEditor *editor); void delayedQuitAllRequested(bool forced); public: struct HandlerAndData { #ifdef Q_OS_WIN // We need to declare a constructor here, otherwise the MSVC 17.5.x compiler fails to parse // the "{nullptr}" initializer in the definition below. // This seems to be a compiler bug, // see: https://developercommunity.visualstudio.com/t/10351118 HandlerAndData() : handler(nullptr) {} #endif FakeVimHandler *handler{nullptr}; TextEditorWidget::SuggestionBlocker suggestionBlocker; }; QHash m_editorToHandler; void setActionChecked(Id id, bool check); using DistFunction = int (*)(const QRect &, const QRect &); void moveSomewhere(FakeVimHandler *handler, DistFunction f, int count); void keepOnlyWindow(); // :only ExCommandMap m_exCommandMap; ExCommandMap m_defaultExCommandMap; UserCommandMap m_userCommandMap; UserCommandMap m_defaultUserCommandMap; MiniBuffer *m_miniBuffer = nullptr; QString m_lastHighlight; int m_savedCursorFlashTime = 0; }; /////////////////////////////////////////////////////////////////////// // // FakeVimExCommandsPage // /////////////////////////////////////////////////////////////////////// enum { CommandRole = Qt::UserRole }; const char exCommandMapGroup[] = "FakeVimExCommand"; const char userCommandMapGroup[] = "FakeVimUserCommand"; const char reKey[] = "RegEx"; const char cmdKey[] = "Cmd"; const char idKey[] = "Command"; class FakeVimExCommandsMappings : public CommandMappings { public: FakeVimExCommandsMappings(); void apply(); protected: ExCommandMap exCommandMapFromWidget(); void commandChanged(); void resetToDefault(); void defaultAction() override; void handleCurrentCommandChanged(QTreeWidgetItem *current); private: QGroupBox *m_commandBox; FancyLineEdit *m_commandEdit; }; FakeVimExCommandsMappings::FakeVimExCommandsMappings() { setPageTitle(Tr::tr("Ex Command Mapping")); setTargetHeader(Tr::tr("Ex Trigger Expression")); setImportExportEnabled(false); connect(this, &FakeVimExCommandsMappings::currentCommandChanged, this, &FakeVimExCommandsMappings::handleCurrentCommandChanged); m_commandBox = new QGroupBox(Tr::tr("Ex Command"), this); m_commandBox->setEnabled(false); auto commandBoxLayout = new QVBoxLayout(m_commandBox); auto boxLayout = new QHBoxLayout; commandBoxLayout->addLayout(boxLayout); m_commandEdit = new FancyLineEdit(m_commandBox); m_commandEdit->setFiltering(true); m_commandEdit->setPlaceholderText(QString()); connect(m_commandEdit, &FancyLineEdit::textChanged, this, &FakeVimExCommandsMappings::commandChanged); m_commandEdit->setValidationFunction([](FancyLineEdit *e, QString *){ return QRegularExpression(e->text()).isValid(); }); auto resetButton = new QPushButton(Tr::tr("Reset"), m_commandBox); resetButton->setToolTip(Tr::tr("Reset to default.")); connect(resetButton, &QPushButton::clicked, this, &FakeVimExCommandsMappings::resetToDefault); boxLayout->addWidget(new QLabel(Tr::tr("Regular expression:"))); boxLayout->addWidget(m_commandEdit); boxLayout->addWidget(resetButton); auto infoLabel = new InfoLabel(Tr::tr("Invalid regular expression."), InfoLabel::Error); infoLabel->setVisible(false); connect(m_commandEdit, &FancyLineEdit::validChanged, this, [infoLabel](bool valid){ infoLabel->setVisible(!valid); }); commandBoxLayout->addWidget(infoLabel); layout()->addWidget(m_commandBox); QMap sections; const QList commands = ActionManager::commands(); for (Command *c : commands) { if (c->action() && c->action()->isSeparator()) continue; auto item = new QTreeWidgetItem; const QString name = c->id().toString(); const int pos = name.indexOf('.'); const QString section = name.left(pos); const QString subId = name.mid(pos + 1); item->setData(0, CommandRole, name); if (!sections.contains(section)) { auto categoryItem = new QTreeWidgetItem(commandList(), { section }); QFont f = categoryItem->font(0); f.setBold(true); categoryItem->setFont(0, f); sections.insert(section, categoryItem); commandList()->expandItem(categoryItem); } sections[section]->addChild(item); item->setText(0, subId); item->setText(1, c->description()); QString regex; if (dd->m_exCommandMap.contains(name)) regex = dd->m_exCommandMap[name].pattern(); item->setText(2, regex); if (regex != dd->m_defaultExCommandMap[name].pattern()) setModified(item, true); } handleCurrentCommandChanged(nullptr); } ExCommandMap FakeVimExCommandsMappings::exCommandMapFromWidget() { ExCommandMap map; int n = commandList()->topLevelItemCount(); for (int i = 0; i != n; ++i) { QTreeWidgetItem *section = commandList()->topLevelItem(i); int m = section->childCount(); for (int j = 0; j != m; ++j) { QTreeWidgetItem *item = section->child(j); const QString name = item->data(0, CommandRole).toString(); const QString regex = item->data(2, Qt::DisplayRole).toString(); const QString pattern = dd->m_defaultExCommandMap.value(name).pattern(); if ((regex.isEmpty() && pattern.isEmpty()) || (!regex.isEmpty() && pattern == regex)) continue; const QRegularExpression expression(regex); if (expression.isValid()) map[name] = expression; } } return map; } void FakeVimExCommandsMappings::handleCurrentCommandChanged(QTreeWidgetItem *current) { if (current) { m_commandEdit->setText(current->text(2)); m_commandBox->setEnabled(true); } else { m_commandEdit->clear(); m_commandBox->setEnabled(false); } } void FakeVimExCommandsMappings::commandChanged() { QTreeWidgetItem *current = commandList()->currentItem(); if (!current) return; const QString name = current->data(0, CommandRole).toString(); const QString regex = m_commandEdit->text(); if (current->data(0, Qt::UserRole).isValid()) current->setText(2, regex); setModified(current, regex != dd->m_defaultExCommandMap[name].pattern()); } void FakeVimExCommandsMappings::resetToDefault() { QTreeWidgetItem *current = commandList()->currentItem(); if (!current) return; const QString name = current->data(0, CommandRole).toString(); QString regex; if (dd->m_defaultExCommandMap.contains(name)) regex = dd->m_defaultExCommandMap[name].pattern(); m_commandEdit->setText(regex); } void FakeVimExCommandsMappings::defaultAction() { const int n = commandList()->topLevelItemCount(); for (int i = 0; i != n; ++i) { QTreeWidgetItem *section = commandList()->topLevelItem(i); const int m = section->childCount(); for (int j = 0; j != m; ++j) { QTreeWidgetItem *item = section->child(j); const QString name = item->data(0, CommandRole).toString(); QString regex; if (dd->m_defaultExCommandMap.contains(name)) regex = dd->m_defaultExCommandMap[name].pattern(); setModified(item, false); item->setText(2, regex); if (item == commandList()->currentItem()) emit currentCommandChanged(item); } } } void FakeVimExCommandsMappings::apply() { // now save the mappings if necessary const ExCommandMap &newMapping = exCommandMapFromWidget(); ExCommandMap &globalCommandMapping = dd->m_exCommandMap; if (newMapping != globalCommandMapping) { const ExCommandMap &defaultMap = dd->m_defaultExCommandMap; QtcSettings *settings = ICore::settings(); settings->beginWriteArray(exCommandMapGroup); int count = 0; using Iterator = ExCommandMap::const_iterator; const Iterator end = newMapping.constEnd(); for (Iterator it = newMapping.constBegin(); it != end; ++it) { const QString id = it.key(); const QRegularExpression re = it.value(); if ((defaultMap.contains(id) && defaultMap[id] != re) || (!defaultMap.contains(id) && !re.pattern().isEmpty())) { settings->setArrayIndex(count); settings->setValue(idKey, id); settings->setValue(reKey, re.pattern()); ++count; } } settings->endArray(); globalCommandMapping.clear(); globalCommandMapping.insert(defaultMap); globalCommandMapping.insert(newMapping); } } class FakeVimExCommandsPageWidget : public IOptionsPageWidget { public: FakeVimExCommandsPageWidget() { auto exCommands = new FakeVimExCommandsMappings; setOnApply([exCommands] { exCommands->apply(); }); Layouting::Column { exCommands, Layouting::noMargin }.attachTo(this); } }; class FakeVimExCommandsPage : public IOptionsPage { public: FakeVimExCommandsPage() { setId(SETTINGS_EX_CMDS_ID); setDisplayName(Tr::tr("Ex Command Mapping")); setCategory(SETTINGS_CATEGORY); setWidgetCreator([] { return new FakeVimExCommandsPageWidget; }); } }; const FakeVimExCommandsPage exCommandPage; /////////////////////////////////////////////////////////////////////// // // FakeVimUserCommandsPage // /////////////////////////////////////////////////////////////////////// class FakeVimUserCommandsModel : public QAbstractTableModel { public: FakeVimUserCommandsModel() { m_commandMap = dd->m_userCommandMap; } UserCommandMap commandMap() const { return m_commandMap; } int rowCount(const QModelIndex &parent) const final { return parent.isValid() ? 0 : 9; } int columnCount(const QModelIndex &parent) const final { return parent.isValid() ? 0 : 2; } QVariant data(const QModelIndex &index, int role) const final; bool setData(const QModelIndex &index, const QVariant &data, int role) final; QVariant headerData(int section, Qt::Orientation orientation, int role) const final; Qt::ItemFlags flags(const QModelIndex &index) const final; private: UserCommandMap m_commandMap; }; QVariant FakeVimUserCommandsModel::headerData(int section, Qt::Orientation orient, int role) const { if (orient == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case 0: return Tr::tr("Action"); case 1: return Tr::tr("Command"); }; } return QVariant(); } Qt::ItemFlags FakeVimUserCommandsModel::flags(const QModelIndex &index) const { if (index.column() == 1) return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; return QAbstractTableModel::flags(index); } class FakeVimUserCommandsDelegate : public QItemDelegate { public: explicit FakeVimUserCommandsDelegate(QObject *parent) : QItemDelegate(parent) {} QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const override { auto lineEdit = new QLineEdit(parent); lineEdit->setFrame(false); return lineEdit; } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { auto lineEdit = qobject_cast(editor); QTC_ASSERT(lineEdit, return); model->setData(index, lineEdit->text(), Qt::EditRole); } }; class FakeVimUserCommandsPageWidget : public IOptionsPageWidget { public: FakeVimUserCommandsPageWidget() { auto widget = new QTreeView; widget->setModel(&m_model); widget->resizeColumnToContents(0); auto delegate = new FakeVimUserCommandsDelegate(widget); widget->setItemDelegateForColumn(1, delegate); auto layout = new QGridLayout(this); layout->addWidget(widget, 0, 0); setLayout(layout); } private: void apply() final { // now save the mappings if necessary const UserCommandMap ¤t = m_model.commandMap(); UserCommandMap &userMap = dd->m_userCommandMap; if (current != userMap) { QtcSettings *settings = ICore::settings(); settings->beginWriteArray(userCommandMapGroup); int count = 0; using Iterator = UserCommandMap::const_iterator; const Iterator end = current.constEnd(); for (Iterator it = current.constBegin(); it != end; ++it) { const int key = it.key(); const QString cmd = it.value(); if ((dd->m_defaultUserCommandMap.contains(key) && dd->m_defaultUserCommandMap[key] != cmd) || (!dd->m_defaultUserCommandMap.contains(key) && !cmd.isEmpty())) { settings->setArrayIndex(count); settings->setValue(idKey, key); settings->setValue(cmdKey, cmd); ++count; } } settings->endArray(); userMap.clear(); userMap.insert(dd->m_defaultUserCommandMap); userMap.insert(current); } } FakeVimUserCommandsModel m_model; }; class FakeVimUserCommandsPage : public IOptionsPage { public: FakeVimUserCommandsPage() { setId(SETTINGS_USER_CMDS_ID); setDisplayName(Tr::tr("User Command Mapping")); setCategory(SETTINGS_CATEGORY); setWidgetCreator([] { return new FakeVimUserCommandsPageWidget; }); } }; const FakeVimUserCommandsPage userCommandsPage; /////////////////////////////////////////////////////////////////////// // // WordCompletion // /////////////////////////////////////////////////////////////////////// class FakeVimCompletionAssistProvider : public CompletionAssistProvider { public: IAssistProcessor *createProcessor(const AssistInterface *) const override; void setActive(const QString &needle, bool forward, FakeVimHandler *handler) { Q_UNUSED(forward) m_handler = handler; if (!m_handler) return; auto editor = qobject_cast(handler->widget()); if (!editor) return; //qDebug() << "ACTIVATE: " << needle << forward; m_needle = needle; editor->invokeAssist(Completion, this); } void setInactive() { m_needle.clear(); m_handler = nullptr; } const QString &needle() const { return m_needle; } void appendNeedle(const QChar &c) { m_needle.append(c); } FakeVimHandler *handler() const { return m_handler; } private: FakeVimHandler *m_handler = nullptr; QString m_needle; }; static FakeVimCompletionAssistProvider theFakeVimCompletionAssistProvider; class FakeVimAssistProposalItem final : public AssistProposalItem { public: FakeVimAssistProposalItem(const FakeVimCompletionAssistProvider *provider) : m_provider(const_cast(provider)) {} bool implicitlyApplies() const override { return false; } bool prematurelyApplies(const QChar &c) const override { m_provider->appendNeedle(c); return text() == m_provider->needle(); } void applyContextualContent(TextDocumentManipulatorInterface &, int) const override { QTC_ASSERT(m_provider->handler(), return); m_provider->handler()->handleReplay(text().mid(m_provider->needle().size())); const_cast(m_provider)->setInactive(); } private: FakeVimCompletionAssistProvider *m_provider; }; class FakeVimAssistProposalModel : public GenericProposalModel { public: FakeVimAssistProposalModel(const QList &items) { loadContent(items); } bool supportsPrefixExpansion() const override { return false; } }; class FakeVimCompletionAssistProcessor : public AsyncProcessor { public: FakeVimCompletionAssistProcessor(const IAssistProvider *provider) : m_provider(static_cast(provider)) {} IAssistProposal *performAsync() override { const QString &needle = m_provider->needle(); const int basePosition = interface()->position() - needle.size(); QTextCursor tc(interface()->textDocument()); tc.setPosition(interface()->position()); tc.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); QList items; QSet seen; QTextDocument::FindFlags flags = QTextDocument::FindCaseSensitively; while (1) { tc = tc.document()->find(needle, tc.position(), flags); if (tc.isNull()) break; QTextCursor sel = tc; sel.select(QTextCursor::WordUnderCursor); QString found = sel.selectedText(); // Only add "real" completions. if (found.startsWith(needle) && sel.anchor() != basePosition && Utils::insert(seen, found)) { auto item = new FakeVimAssistProposalItem(m_provider); item->setText(found); items.append(item); } tc.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor); } //qDebug() << "COMPLETIONS" << completions->size(); return new GenericProposal(basePosition, GenericProposalModelPtr(new FakeVimAssistProposalModel(items))); } private: const FakeVimCompletionAssistProvider *m_provider; }; IAssistProcessor *FakeVimCompletionAssistProvider::createProcessor(const AssistInterface *) const { return new FakeVimCompletionAssistProcessor(this); } // FakeVimUserCommandsModel QVariant FakeVimUserCommandsModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (index.column()) { case 0: // Action return Tr::tr("User command #%1").arg(index.row() + 1); case 1: // Command return m_commandMap.value(index.row() + 1); } } return QVariant(); } bool FakeVimUserCommandsModel::setData(const QModelIndex &index, const QVariant &data, int role) { if (role == Qt::DisplayRole || role == Qt::EditRole) if (index.column() == 1) m_commandMap[index.row() + 1] = data.toString(); return true; } #ifdef WITH_TESTS static void setupTest(QString *title, FakeVimHandler **handler, QWidget **edit) { *title = QString::fromLatin1("test.cpp"); IEditor *iedit = EditorManager::openEditorWithContents(Id(), title); EditorManager::activateEditor(iedit); *edit = iedit->widget(); *handler = dd->m_editorToHandler.value(iedit, {}).handler; (*handler)->setupWidget(); (*handler)->handleCommand("set startofline"); // *handler = 0; // m_statusMessage.clear(); // m_statusData.clear(); // m_infoMessage.clear(); // if (m_textedit) { // m_textedit->setPlainText(lines); // QTextCursor tc = m_textedit->textCursor(); // tc.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); // m_textedit->setTextCursor(tc); // m_textedit->setPlainText(lines); // *handler = new FakeVimHandler(m_textedit); // } else { // m_plaintextedit->setPlainText(lines); // QTextCursor tc = m_plaintextedit->textCursor(); // tc.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); // m_plaintextedit->setTextCursor(tc); // m_plaintextedit->setPlainText(lines); // *handler = new FakeVimHandler(m_plaintextedit); // } // connect(*handler, &FakeVimHandler::commandBufferChanged, // this, &FakeVimPlugin::changeStatusMessage); // connect(*handler, &FakeVimHandler::extraInformationChanged, // this, &FakeVimPlugin::changeExtraInformation); // connect(*handler, &FakeVimHandler::statusDataChanged, // this, &FakeVimPlugin::changeStatusData); // QCOMPARE(EDITOR(toPlainText()), lines); (*handler)->handleCommand("set iskeyword=@,48-57,_,192-255,a-z,A-Z"); } QObject *createFakeVimTester( void (*setupTest)(QString *, FakeVimHandler **, QWidget **) ); // in fakevim_test.cpp #endif FakeVimPlugin::FakeVimPlugin() { dd = this; #ifdef WITH_TESTS addTestCreator([] { return createFakeVimTester(&setupTest); }); #endif m_defaultExCommandMap[CppEditor::Constants::SWITCH_HEADER_SOURCE] = QRegularExpression("^A$"); m_defaultExCommandMap["Coreplugin.OutputPane.previtem"] = QRegularExpression("^(cN(ext)?|cp(revious)?)!?( (.*))?$"); m_defaultExCommandMap["Coreplugin.OutputPane.nextitem"] = QRegularExpression("^cn(ext)?!?( (.*))?$"); m_defaultExCommandMap[TextEditor::Constants::FOLLOW_SYMBOL_UNDER_CURSOR] = QRegularExpression("^tag?$"); m_defaultExCommandMap[Core::Constants::GO_BACK] = QRegularExpression("^pop?$"); m_defaultExCommandMap["QtCreator.Locate"] = QRegularExpression("^e$"); for (int i = 1; i < 10; ++i) { QString cmd = QString::fromLatin1(":echo User command %1 executed."); m_defaultUserCommandMap.insert(i, cmd.arg(i)); } } void FakeVimPlugin::initialize() { /* // Set completion settings and keep them up to date. TextEditorSettings *textEditorSettings = TextEditorSettings::instance(); completion->setCompletionSettings(textEditorSettings->completionSettings()); connect(textEditorSettings, &TextEditorSettings::completionSettingsChanged, completion, &TextEditorWidget::setCompletionSettings); */ readSettings(); // Vimrc can break test so don't source it if running tests. if (!ExtensionSystem::PluginManager::testRunRequested()) maybeReadVimRc(); Command *cmd = nullptr; cmd = ActionManager::registerAction(settings().useFakeVim.action(), INSTALL_HANDLER, Context(Core::Constants::C_GLOBAL), true); cmd->setDefaultKeySequence(QKeySequence(useMacShortcuts ? Tr::tr("Meta+Shift+Y,Meta+Shift+Y") : Tr::tr("Alt+Y,Alt+Y"))); connect(cmd->action(), &QAction::triggered, [] { settings().writeSettings(); }); ActionContainer *advancedMenu = ActionManager::actionContainer(Core::Constants::M_EDIT_ADVANCED); advancedMenu->addAction(cmd, Core::Constants::G_EDIT_EDITOR); const Id base = "FakeVim.UserAction"; for (int i = 1; i < 10; ++i) { auto act = new QAction(this); act->setText(Tr::tr("Execute User Action #%1").arg(i)); cmd = ActionManager::registerAction(act, base.withSuffix(i)); cmd->setDefaultKeySequence(QKeySequence((useMacShortcuts ? Tr::tr("Meta+Shift+Y,%1") : Tr::tr("Alt+Y,%1")).arg(i))); connect(act, &QAction::triggered, this, [this, i] { userActionTriggered(i); }); } connect(ICore::instance(), &ICore::coreAboutToClose, this, [] { // Don't attach to editors anymore. disconnect(EditorManager::instance(), &EditorManager::editorOpened, dd, &FakeVimPlugin::editorOpened); }); // EditorManager connect(EditorManager::instance(), &EditorManager::editorAboutToClose, this, &FakeVimPlugin::editorAboutToClose); connect(EditorManager::instance(), &EditorManager::editorOpened, this, &FakeVimPlugin::editorOpened); connect(EditorManager::instance(), &EditorManager::currentEditorAboutToChange, this, &FakeVimPlugin::currentEditorAboutToChange); connect(DocumentManager::instance(), &DocumentManager::allDocumentsRenamed, this, &FakeVimPlugin::allDocumentsRenamed); connect(DocumentManager::instance(), &DocumentManager::documentRenamed, this, &FakeVimPlugin::documentRenamed); FakeVimSettings &s = settings(); connect(&s.useFakeVim, &FvBoolAspect::changed, this, [this, &s] { setUseFakeVim(s.useFakeVim()); }); connect(&s.readVimRc, &FvBaseAspect::changed, this, &FakeVimPlugin::maybeReadVimRc); connect(&s.vimRcPath, &FvBaseAspect::changed, this, &FakeVimPlugin::maybeReadVimRc); connect(&s.relativeNumber, &FvBoolAspect::changed, this, [this, &s] { setShowRelativeLineNumbers(s.relativeNumber()); }); connect(&s.blinkingCursor, &FvBoolAspect::changed, this, [this, &s] { setCursorBlinking(s.blinkingCursor()); }); // Delayed operations. connect(this, &FakeVimPlugin::delayedQuitRequested, this, &FakeVimPlugin::handleDelayedQuit, Qt::QueuedConnection); connect(this, &FakeVimPlugin::delayedQuitAllRequested, this, &FakeVimPlugin::handleDelayedQuitAll, Qt::QueuedConnection); setCursorBlinking(s.blinkingCursor()); } void FakeVimPlugin::userActionTriggered(int key) { IEditor *editor = EditorManager::currentEditor(); FakeVimHandler *handler = m_editorToHandler[editor].handler; if (handler) { // If disabled, enable FakeVim mode just for single user command. bool enableFakeVim = !settings().useFakeVim(); if (enableFakeVim) setUseFakeVimInternal(true); const QString cmd = m_userCommandMap.value(key); handler->handleInput(cmd); if (enableFakeVim) setUseFakeVimInternal(false); } } void FakeVimPlugin::updateAllHightLights() { const QList editors = EditorManager::visibleEditors(); for (IEditor *editor : editors) { QWidget *w = editor->widget(); if (auto find = Aggregation::query(w)) find->highlightAll(m_lastHighlight, FindRegularExpression | FindCaseSensitively); } } void FakeVimPlugin::createRelativeNumberWidget(IEditor *editor) { if (auto textEditor = TextEditorWidget::fromEditor(editor)) { auto relativeNumbers = new RelativeNumbersColumn(textEditor); connect(&settings().relativeNumber, &FvBaseAspect::changed, relativeNumbers, &QObject::deleteLater); connect(&settings().useFakeVim, &FvBaseAspect::changed, relativeNumbers, &QObject::deleteLater); relativeNumbers->show(); } } void FakeVimPlugin::readSettings() { QtcSettings *settings = ICore::settings(); m_exCommandMap = m_defaultExCommandMap; int size = settings->beginReadArray(exCommandMapGroup); for (int i = 0; i < size; ++i) { settings->setArrayIndex(i); const QString id = settings->value(idKey).toString(); const QString re = settings->value(reKey).toString(); const QRegularExpression regEx(re); if (regEx.isValid()) m_exCommandMap[id] = regEx; } settings->endArray(); m_userCommandMap = m_defaultUserCommandMap; size = settings->beginReadArray(userCommandMapGroup); for (int i = 0; i < size; ++i) { settings->setArrayIndex(i); const int id = settings->value(idKey).toInt(); const QString cmd = settings->value(cmdKey).toString(); m_userCommandMap[id] = cmd; } settings->endArray(); } void FakeVimPlugin::maybeReadVimRc() { //qDebug() << theFakeVimSetting(ConfigReadVimRc) // << theFakeVimSetting(ConfigReadVimRc)->value(); //qDebug() << theFakeVimSetting(ConfigShiftWidth)->value(); if (!settings().readVimRc()) return; QString fileName = settings().vimRcPath().path(); if (fileName.isEmpty()) { fileName = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QLatin1String(HostOsInfo::isWindowsHost() ? "/_vimrc" : "/.vimrc"); } //qDebug() << "READING VIMRC: " << fileName; // Read it into a temporary handler for effects modifying global state. QPlainTextEdit editor; FakeVimHandler handler(&editor); handler.handleCommand("source " + fileName); //qDebug() << theFakeVimSetting(ConfigShiftWidth)->value(); } static void triggerAction(Id id) { Command *cmd = ActionManager::command(id); QTC_ASSERT(cmd, qDebug() << "UNKNOWN CODE: " << id.name(); return); QAction *action = cmd->action(); QTC_ASSERT(action, return); action->trigger(); } void FakeVimPlugin::setActionChecked(Id id, bool check) { Command *cmd = ActionManager::command(id); QTC_ASSERT(cmd, return); QAction *action = cmd->action(); QTC_ASSERT(action, return); QTC_ASSERT(action->isCheckable(), return); action->setChecked(!check); // trigger negates the action's state action->trigger(); } static int moveRightWeight(const QRect &cursor, const QRect &other) { if (!cursor.adjusted(999999, 0, 0, 0).intersects(other)) return -1; const int dx = other.left() - cursor.right(); const int dy = qAbs(cursor.center().y() - other.center().y()); const int w = 10000 * dx + dy; return w; } static int moveLeftWeight(const QRect &cursor, const QRect &other) { if (!cursor.adjusted(-999999, 0, 0, 0).intersects(other)) return -1; const int dx = cursor.left() - other.right(); const int dy = qAbs(cursor.center().y() -other.center().y()); const int w = 10000 * dx + dy; return w; } static int moveUpWeight(const QRect &cursor, const QRect &other) { if (!cursor.adjusted(0, 0, 0, -999999).intersects(other)) return -1; const int dy = cursor.top() - other.bottom(); const int dx = qAbs(cursor.center().x() - other.center().x()); const int w = 10000 * dy + dx; return w; } static int moveDownWeight(const QRect &cursor, const QRect &other) { if (!cursor.adjusted(0, 0, 0, 999999).intersects(other)) return -1; const int dy = other.top() - cursor.bottom(); const int dx = qAbs(cursor.center().x() - other.center().x()); const int w = 10000 * dy + dx; return w; } void FakeVimPlugin::moveSomewhere(FakeVimHandler *handler, DistFunction f, int count) { QTC_ASSERT(handler, return); QWidget *w = handler->widget(); auto pe = qobject_cast(w); QTC_ASSERT(pe, return); QRect rc = pe->cursorRect(); QRect cursorRect(w->mapToGlobal(rc.topLeft()), w->mapToGlobal(rc.bottomRight())); //qDebug() << "\nCURSOR: " << cursorRect; IEditor *bestEditor = nullptr; int repeat = count; IEditor *currentEditor = EditorManager::currentEditor(); QList editors = EditorManager::visibleEditors(); while (repeat < 0 || repeat-- > 0) { editors.removeOne(currentEditor); int bestValue = -1; for (IEditor *editor : std::as_const(editors)) { QWidget *w = editor->widget(); QRect editorRect(w->mapToGlobal(w->geometry().topLeft()), w->mapToGlobal(w->geometry().bottomRight())); //qDebug() << " EDITOR: " << editorRect << editor; int value = f(cursorRect, editorRect); if (value != -1 && (bestValue == -1 || value < bestValue)) { bestValue = value; bestEditor = editor; //qDebug() << " BEST SO FAR: " << bestValue << bestEditor; } } if (bestValue == -1) break; currentEditor = bestEditor; //qDebug() << " BEST: " << bestValue << bestEditor; } // FIME: This is know to fail as the EditorManager will fall back to // the current editor's view. Needs additional public API there. if (bestEditor) EditorManager::activateEditor(bestEditor); } void FakeVimPlugin::keepOnlyWindow() { IEditor *currentEditor = EditorManager::currentEditor(); QList editors = EditorManager::visibleEditors(); editors.removeOne(currentEditor); for (IEditor *editor : std::as_const(editors)) { EditorManager::activateEditor(editor); triggerAction(Core::Constants::REMOVE_CURRENT_SPLIT); } } void FakeVimPlugin::fold(FakeVimHandler *handler, int depth, bool fold) { QTC_ASSERT(handler, return); QTextDocument *doc = handler->textCursor().document(); QTC_ASSERT(doc, return); auto documentLayout = qobject_cast(doc->documentLayout()); QTC_ASSERT(documentLayout, return); QTextBlock block = handler->textCursor().block(); int indent = TextDocumentLayout::foldingIndent(block); if (fold) { if (TextDocumentLayout::isFolded(block)) { while (block.isValid() && (TextDocumentLayout::foldingIndent(block) >= indent || !block.isVisible())) { block = block.previous(); } } if (TextDocumentLayout::canFold(block)) ++indent; while (depth != 0 && block.isValid()) { const int indent2 = TextDocumentLayout::foldingIndent(block); if (TextDocumentLayout::canFold(block) && indent2 < indent) { TextDocumentLayout::doFoldOrUnfold(block, false); if (depth > 0) --depth; indent = indent2; } block = block.previous(); } } else { if (TextDocumentLayout::isFolded(block)) { if (depth < 0) { // recursively open fold while (block.isValid() && TextDocumentLayout::foldingIndent(block) >= indent) { if (TextDocumentLayout::canFold(block)) TextDocumentLayout::doFoldOrUnfold(block, true); block = block.next(); } } else { if (TextDocumentLayout::canFold(block)) { TextDocumentLayout::doFoldOrUnfold(block, true); if (depth > 0) --depth; } } } } documentLayout->requestUpdate(); documentLayout->emitDocumentSizeChanged(); } // This class defers deletion of a child FakeVimHandler using deleteLater(). class DeferredDeleter : public QObject { Q_OBJECT FakeVimHandler *m_handler; public: DeferredDeleter(QObject *parent, FakeVimHandler *handler) : QObject(parent), m_handler(handler) {} ~DeferredDeleter() override { if (m_handler) { m_handler->disconnectFromEditor(); m_handler->deleteLater(); m_handler = nullptr; } } }; void FakeVimPlugin::editorOpened(IEditor *editor) { if (!editor) return; if (m_editorToHandler.contains(editor)) { // We get here via the call from the duplicated handler in case // it was triggered by triggerAction(Core::Constants::SPLIT). // On the other hand, we need the path from there to support the // case of manual calls to IEditor::duplicate(). return; } QWidget *widget = editor->widget(); if (!widget) return; // we can only handle QTextEdit and QPlainTextEdit if (auto edit = Aggregation::query(widget)) widget = edit; else if (auto edit = Aggregation::query(widget)) widget = edit; else return; // Duplicated editors are not signalled by the EditorManager. Track them nevertheless. connect(editor, &IEditor::editorDuplicated, this, [this](IEditor *duplicate) { editorOpened(duplicate); connect(duplicate, &QObject::destroyed, this, [this, duplicate] { m_editorToHandler.remove(duplicate); }); }); auto tew = TextEditorWidget::fromEditor(editor); //qDebug() << "OPENING: " << editor << editor->widget() // << "MODE: " << theFakeVimSetting(ConfigUseFakeVim)->value(); auto handler = new FakeVimHandler(widget, nullptr); // the handler might have triggered the deletion of the editor: // make sure that it can return before being deleted itself new DeferredDeleter(widget, handler); m_editorToHandler[editor].handler = handler; handler->extraInformationChanged.set([this](const QString &text) { EditorManager::splitSideBySide(); QString title = "stdout.txt"; IEditor *iedit = EditorManager::openEditorWithContents(Id(), &title, text.toUtf8()); EditorManager::activateEditor(iedit); FakeVimHandler *handler = m_editorToHandler.value(iedit, {}).handler; QTC_ASSERT(handler, return); handler->handleCommand("0"); }); handler->commandBufferChanged.set( [this, handler](const QString &contents, int cursorPos, int anchorPos, int messageLevel) { showCommandBuffer(handler, contents, cursorPos, anchorPos, messageLevel); }); handler->selectionChanged.set([tew](const QList &selection) { if (tew) tew->setExtraSelections(TextEditorWidget::FakeVimSelection, selection); }); handler->tabPressedInInsertMode.set([tew]() { auto suggestion = tew->currentSuggestion(); if (suggestion) { suggestion->apply(); return false; } return true; }); handler->modeChanged.set([tew, this, editor](bool insertMode) { HandlerAndData &handlerAndData = m_editorToHandler[editor]; if (!handlerAndData.handler->inFakeVimMode()) return; // We don't want to show suggestions unless we are in insert mode. if (insertMode != (handlerAndData.suggestionBlocker == nullptr)) handlerAndData.suggestionBlocker = insertMode ? nullptr : tew->blockSuggestions(); if (tew) tew->clearSuggestion(); }); handler->highlightMatches.set([this](const QString &needle) { m_lastHighlight = needle; for (IEditor *editor : EditorManager::visibleEditors()) { QWidget *w = editor->widget(); if (auto find = Aggregation::query(w)) find->highlightAll(needle, FindRegularExpression | FindCaseSensitively); } }); handler->moveToMatchingParenthesis.set([](bool *moved, bool *forward, QTextCursor *cursor) { *moved = false; bool undoFakeEOL = false; if (cursor->atBlockEnd() && cursor->block().length() > 1) { cursor->movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); undoFakeEOL = true; } TextBlockUserData::MatchType match = TextBlockUserData::matchCursorForward(cursor); if (match == TextBlockUserData::Match) { *moved = true; *forward = true; } else { if (undoFakeEOL) cursor->movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); if (match == TextBlockUserData::NoMatch) { // Backward matching is according to the character before the cursor. bool undoMove = false; if (!cursor->atBlockEnd()) { cursor->movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); undoMove = true; } match = TextBlockUserData::matchCursorBackward(cursor); if (match == TextBlockUserData::Match) { *moved = true; *forward = false; } else if (undoMove) { cursor->movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); } } } }); handler->indentRegion.set([tew](int beginBlock, int endBlock, QChar typedChar) { if (!tew) return; TabSettings tabSettings; tabSettings.m_indentSize = settings().shiftWidth(); tabSettings.m_tabSize = settings().tabStop(); tabSettings.m_tabPolicy = settings().expandTab() ? TabSettings::SpacesOnlyTabPolicy : TabSettings::TabsOnlyTabPolicy; tabSettings.m_continuationAlignBehavior = tew->textDocument()->tabSettings().m_continuationAlignBehavior; QTextDocument *doc = tew->document(); QTextBlock startBlock = doc->findBlockByNumber(beginBlock); // Record line lenghts for mark adjustments QVector lineLengths(endBlock - beginBlock + 1); QTextBlock block = startBlock; for (int i = beginBlock; i <= endBlock; ++i) { lineLengths[i - beginBlock] = block.text().length(); if (typedChar.unicode() == 0 && block.text().simplified().isEmpty()) { // clear empty lines QTextCursor cursor(block); while (!cursor.atBlockEnd()) cursor.deleteChar(); } else { tew->textDocument()->indenter()->indentBlock(block, typedChar, tabSettings); } block = block.next(); } }); handler->checkForElectricCharacter.set([tew](bool *result, QChar c) { if (tew) *result = tew->textDocument()->indenter()->isElectricCharacter(c); }); handler->requestDisableBlockSelection.set([tew] { if (tew) tew->setTextCursor(tew->textCursor()); }); handler->requestSetBlockSelection.set([tew](const QTextCursor &cursor) { if (tew) { const TabSettings &tabs = tew->textDocument()->tabSettings(); MultiTextCursor mtc; const bool forwardSelection = cursor.anchor() < cursor.position(); QTextBlock block = cursor.document()->findBlock(cursor.anchor()); const QTextBlock end = forwardSelection ? cursor.block().next() : cursor.block().previous(); const int anchor = tabs.columnAt(block.text(), cursor.anchor() - block.position()); const int pos = tabs.columnAt(cursor.block().text(), cursor.positionInBlock()); while (block.isValid() && block != end) { const int columns = tabs.columnCountForText(block.text()); if (columns >= anchor || columns >= pos) { QTextCursor c(block); c.setPosition(block.position() + tabs.positionAtColumn(block.text(), anchor)); c.setPosition(block.position() + tabs.positionAtColumn(block.text(), pos), QTextCursor::KeepAnchor); mtc.addCursor(c); } block = forwardSelection ? block.next() : block.previous(); } tew->setMultiTextCursor(mtc); } }); handler->requestBlockSelection.set([tew](QTextCursor *cursor) { if (tew && cursor) { MultiTextCursor mtc = tew->multiTextCursor(); *cursor = mtc.cursors().first(); cursor->setPosition(mtc.mainCursor().position(), QTextCursor::KeepAnchor); } }); handler->requestHasBlockSelection.set([tew](bool *on) { if (tew && on) *on = tew->multiTextCursor().hasMultipleCursors(); }); handler->simpleCompletionRequested.set([handler](const QString &needle, bool forward) { theFakeVimCompletionAssistProvider.setActive(needle, forward, handler); }); handler->windowCommandRequested.set([this, handler](const QString &map, int count) { // normalize mapping const QString key = map.toUpper(); if (key == "C" || key == "") triggerAction(Core::Constants::REMOVE_CURRENT_SPLIT); else if (key == "N" || key == "") triggerAction(Core::Constants::GOTO_NEXT_SPLIT); else if (key == "O" || key == "") keepOnlyWindow(); else if (key == "P" || key == "") triggerAction(Core::Constants::GOTO_PREV_SPLIT); else if (key == "S" || key == "") triggerAction(Core::Constants::SPLIT); else if (key == "V" || key == "") triggerAction(Core::Constants::SPLIT_SIDE_BY_SIDE); else if (key == "W" || key == "") triggerAction(Core::Constants::GOTO_NEXT_SPLIT); else if (key.contains("RIGHT") || key == "L" || key == "" || key == "") moveSomewhere(handler, &moveRightWeight, key == "" ? -1 : count); else if (key.contains("LEFT") || key == "H" || key == "" || key == "") moveSomewhere(handler, &moveLeftWeight, key == "" ? -1 : count); else if (key.contains("UP") || key == "K" || key == "" || key == "") moveSomewhere(handler, &moveUpWeight, key == "" ? -1 : count); else if (key.contains("DOWN") || key == "J" || key == "" || key == "") moveSomewhere(handler, &moveDownWeight, key == "" ? -1 : count); else qDebug() << "UNKNOWN WINDOW COMMAND: " << map; }); handler->findRequested.set([](bool reverse) { Find::setUseFakeVim(true); Find::openFindToolBar(reverse ? Find::FindBackwardDirection : Find::FindForwardDirection); }); handler->findNextRequested.set([](bool reverse) { triggerAction(reverse ? Core::Constants::FIND_PREVIOUS : Core::Constants::FIND_NEXT); }); handler->foldToggle.set([this, handler](int depth) { QTextBlock block = handler->textCursor().block(); fold(handler, depth, !TextDocumentLayout::isFolded(block)); }); handler->foldAll.set([handler](bool fold) { QTextDocument *document = handler->textCursor().document(); auto documentLayout = qobject_cast(document->documentLayout()); QTC_ASSERT(documentLayout, return); QTextBlock block = document->firstBlock(); while (block.isValid()) { TextDocumentLayout::doFoldOrUnfold(block, !fold); block = block.next(); } documentLayout->requestUpdate(); documentLayout->emitDocumentSizeChanged(); }); handler->fold.set([this, handler](int depth, bool dofold) { fold(handler, depth, dofold); }); handler->foldGoTo.set([handler](int count, bool current) { QTextCursor tc = handler->textCursor(); QTextBlock block = tc.block(); int pos = -1; if (count > 0) { int repeat = count; block = block.next(); QTextBlock prevBlock = block; int indent = TextDocumentLayout::foldingIndent(block); block = block.next(); while (block.isValid()) { int newIndent = TextDocumentLayout::foldingIndent(block); if (current ? indent > newIndent : indent < newIndent) { if (prevBlock.isVisible()) { pos = prevBlock.position(); if (--repeat <= 0) break; } else if (current) { indent = newIndent; } } if (!current) indent = newIndent; prevBlock = block; block = block.next(); } } else if (count < 0) { int repeat = -count; int indent = TextDocumentLayout::foldingIndent(block); block = block.previous(); while (block.isValid()) { int newIndent = TextDocumentLayout::foldingIndent(block); if (current ? indent > newIndent : indent < newIndent) { while (block.isValid() && !block.isVisible()) block = block.previous(); pos = block.position(); if (--repeat <= 0) break; } if (!current) indent = newIndent; block = block.previous(); } } if (pos != -1) { tc.setPosition(pos, QTextCursor::KeepAnchor); handler->setTextCursor(tc); } }); handler->requestJumpToGlobalMark.set( [this](QChar mark, bool backTickMode, const QString &fileName) { if (IEditor *iedit = EditorManager::openEditor(FilePath::fromString(fileName))) { if (FakeVimHandler *handler = m_editorToHandler.value(iedit, {}).handler) handler->jumpToLocalMark(mark, backTickMode); } }); handler->handleExCommandRequested.set([this, handler](bool *handled, const ExCommand &cmd) { handleExCommand(handler, handled, cmd); }); handler->tabNextRequested.set([] { triggerAction(Core::Constants::GOTONEXTINHISTORY); }); handler->tabPreviousRequested.set([] { triggerAction(Core::Constants::GOTOPREVINHISTORY); }); handler->completionRequested.set([tew] { if (tew) tew->invokeAssist(Completion, &theFakeVimCompletionAssistProvider); }); handler->processOutput.set([](const QString &command, const QString &input, QString *output) { Process proc; proc.setCommand(Utils::CommandLine::fromUserInput(command)); proc.setWriteData(input.toLocal8Bit()); proc.start(); // FIXME: Process should be interruptable by user. // Solution is to create a QObject for each process and emit finished state. proc.waitForFinished(); *output = proc.cleanedStdOut(); }); handler->setCurrentFileName(editor->document()->filePath().toString()); handler->installEventFilter(); // pop up the bar if (settings().useFakeVim()) { resetCommandBuffer(); handler->setupWidget(); if (settings().relativeNumber()) createRelativeNumberWidget(editor); } } void FakeVimPlugin::editorAboutToClose(IEditor *editor) { //qDebug() << "CLOSING: " << editor << editor->widget(); m_editorToHandler.remove(editor); } void FakeVimPlugin::currentEditorAboutToChange(IEditor *editor) { if (FakeVimHandler *handler = m_editorToHandler.value(editor, {}).handler) handler->enterCommandMode(); } void FakeVimPlugin::allDocumentsRenamed(const FilePath &oldPath, const FilePath &newPath) { renameFileNameInEditors(oldPath, newPath); FakeVimHandler::updateGlobalMarksFilenames(oldPath.toString(), newPath.toString()); } void FakeVimPlugin::documentRenamed( IDocument *, const FilePath &oldPath, const FilePath &newPath) { renameFileNameInEditors(oldPath, newPath); } void FakeVimPlugin::renameFileNameInEditors(const FilePath &oldPath, const FilePath &newPath) { for (const HandlerAndData &handlerAndData : m_editorToHandler) { if (handlerAndData.handler->currentFileName() == oldPath.toString()) handlerAndData.handler->setCurrentFileName(newPath.toString()); } } void FakeVimPlugin::setUseFakeVim(bool on) { //qDebug() << "SET USE FAKEVIM" << on; Find::setUseFakeVim(on); setUseFakeVimInternal(on); setShowRelativeLineNumbers(settings().relativeNumber()); setCursorBlinking(settings().blinkingCursor()); } void FakeVimPlugin::setUseFakeVimInternal(bool on) { if (on) { //ICore *core = ICore::instance(); //core->updateAdditionalContexts(Context(FAKEVIM_CONTEXT), // Context()); for (const HandlerAndData &handlerAndData : m_editorToHandler) handlerAndData.handler->setupWidget(); } else { //ICore *core = ICore::instance(); //core->updateAdditionalContexts(Context(), // Context(FAKEVIM_CONTEXT)); resetCommandBuffer(); for (auto it = m_editorToHandler.begin(); it != m_editorToHandler.end(); ++it) { if (auto textDocument = qobject_cast(it.key()->document())) { HandlerAndData &handlerAndData = it.value(); handlerAndData.handler->restoreWidget(textDocument->tabSettings().m_tabSize); handlerAndData.suggestionBlocker.reset(); } } } } void FakeVimPlugin::setShowRelativeLineNumbers(bool on) { if (on && settings().useFakeVim()) { for (auto it = m_editorToHandler.constBegin(); it != m_editorToHandler.constEnd(); ++it) createRelativeNumberWidget(it.key()); } } void FakeVimPlugin::setCursorBlinking(bool on) { if (m_savedCursorFlashTime == 0) m_savedCursorFlashTime = QGuiApplication::styleHints()->cursorFlashTime(); const bool blink = on || !settings().useFakeVim(); QGuiApplication::styleHints()->setCursorFlashTime(blink ? m_savedCursorFlashTime : 0); } void FakeVimPlugin::handleExCommand(FakeVimHandler *handler, bool *handled, const ExCommand &cmd) { QTC_ASSERT(handler, return); using namespace Core; //qDebug() << "PLUGIN HANDLE: " << cmd.cmd << cmd.count; *handled = false; // Focus editor first so actions can be executed in correct context. QWidget *editor = handler->widget(); if (editor) editor->setFocus(); auto editorFromHandler = [this, handler]() -> Core::IEditor * { auto itEditor = std::find_if(m_editorToHandler.cbegin(), m_editorToHandler.cend(), [handler](const HandlerAndData &handlerAndData) { return handlerAndData.handler == handler; }); if (itEditor != m_editorToHandler.cend()) return itEditor.key(); return nullptr; }; *handled = true; if ((cmd.matches("w", "write") || cmd.cmd == "wq") && cmd.args.isEmpty()) { // :w[rite] bool saved = false; IEditor *editor = editorFromHandler(); const QString fileName = handler->currentFileName(); if (editor && editor->document()->filePath().toString() == fileName) { triggerAction(Core::Constants::SAVE); saved = !editor->document()->isModified(); if (saved) { QFile file3(fileName); if (file3.open(QIODevice::ReadOnly)) { const QByteArray ba = file3.readAll(); handler->showMessage(MessageInfo, Tr::tr("\"%1\" %2 %3L, %4C written") .arg(fileName).arg(' ').arg(ba.count('\n')).arg(ba.size())); if (cmd.cmd == "wq") emit delayedQuitRequested(cmd.hasBang, editor); } } } if (!saved) handler->showMessage(MessageError, Tr::tr("File not saved")); } else if (cmd.matches("wa", "wall") || cmd.matches("wqa", "wqall")) { // :wa[ll] :wqa[ll] triggerAction(Core::Constants::SAVEALL); const QList failed = DocumentManager::modifiedDocuments(); if (failed.isEmpty()) handler->showMessage(MessageInfo, Tr::tr("Saving succeeded")); else handler->showMessage(MessageError, Tr::tr("%n files not saved", nullptr, failed.size())); if (cmd.matches("wqa", "wqall")) emit delayedQuitAllRequested(cmd.hasBang); } else if (cmd.matches("q", "quit")) { // :q[uit] emit delayedQuitRequested(cmd.hasBang, editorFromHandler()); } else if (cmd.matches("qa", "qall")) { // :qa[ll] emit delayedQuitAllRequested(cmd.hasBang); } else if (cmd.matches("sp", "split")) { // :sp[lit] triggerAction(Core::Constants::SPLIT); updateAllHightLights(); } else if (cmd.matches("vs", "vsplit")) { // :vs[plit] triggerAction(Core::Constants::SPLIT_SIDE_BY_SIDE); updateAllHightLights(); } else if (cmd.matches("mak", "make")) { // :mak[e][!] [arguments] triggerAction(ProjectExplorer::Constants::BUILD); } else if (cmd.matches("se", "set")) { if (cmd.args.isEmpty()) { // :se[t] ICore::showOptionsDialog(SETTINGS_ID); } else if (cmd.args == "ic" || cmd.args == "ignorecase") { // :set nc setActionChecked(Core::Constants::CASE_SENSITIVE, false); } else if (cmd.args == "noic" || cmd.args == "noignorecase") { // :set noic setActionChecked(Core::Constants::CASE_SENSITIVE, true); } *handled = false; // Let the handler see it as well. } else if (cmd.matches("n", "next")) { // :n[ext] switchToFile(currentFile() + cmd.count); } else if (cmd.matches("prev", "previous") || cmd.matches("N", "Next")) { // :prev[ious], :N[ext] switchToFile(currentFile() - cmd.count); } else if (cmd.matches("bn", "bnext")) { // :bn[ext] switchToFile(currentFile() + cmd.count); } else if (cmd.matches("bp", "bprevious") || cmd.matches("bN", "bNext")) { // :bp[revious], :bN[ext] switchToFile(currentFile() - cmd.count); } else if (cmd.matches("on", "only")) { // :on[ly] keepOnlyWindow(); } else if (cmd.cmd == "AS") { triggerAction(Core::Constants::SPLIT); triggerAction(CppEditor::Constants::SWITCH_HEADER_SOURCE); } else if (cmd.cmd == "AV") { triggerAction(Core::Constants::SPLIT_SIDE_BY_SIDE); triggerAction(CppEditor::Constants::SWITCH_HEADER_SOURCE); } else { // Check whether one of the configure commands matches. const auto end = m_exCommandMap.constEnd(); for (auto it = m_exCommandMap.constBegin(); it != end; ++it) { const QString &id = it.key(); QRegularExpression re = it.value(); if (!re.pattern().isEmpty() && re.match(cmd.cmd).hasMatch()) { triggerAction(Id::fromString(id)); return; } } *handled = false; } } void FakeVimPlugin::handleDelayedQuit(bool forced, IEditor *editor) { // This tries to simulate vim behaviour. But the models of vim and // Qt Creator core do not match well... if (EditorManager::hasSplitter()) triggerAction(Core::Constants::REMOVE_CURRENT_SPLIT); else EditorManager::closeEditors({editor}, !forced); } void FakeVimPlugin::handleDelayedQuitAll(bool forced) { triggerAction(Core::Constants::REMOVE_ALL_SPLITS); EditorManager::closeAllEditors(!forced); } void FakeVimPlugin::quitFakeVim() { settings().useFakeVim.setValue(false); } void FakeVimPlugin::resetCommandBuffer() { showCommandBuffer(nullptr, QString(), -1, -1, 0); } void FakeVimPlugin::showCommandBuffer(FakeVimHandler *handler, const QString &contents, int cursorPos, int anchorPos, int messageLevel) { //qDebug() << "SHOW COMMAND BUFFER" << contents; QTC_ASSERT(m_miniBuffer, return); m_miniBuffer->setContents(contents, cursorPos, anchorPos, messageLevel, handler); } int FakeVimPlugin::currentFile() const { IEditor *editor = EditorManager::currentEditor(); if (editor) { const std::optional index = DocumentModel::indexOfDocument(editor->document()); if (QTC_GUARD(index)) return index.value(); } return -1; } void FakeVimPlugin::switchToFile(int n) { int size = DocumentModel::entryCount(); QTC_ASSERT(size, return); n = n % size; if (n < 0) n += size; EditorManager::activateEditorForEntry(DocumentModel::entries().at(n)); } } // FakeVim::Internal #include "fakevimplugin.moc"