Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new cmdline (local to editor view) #57

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ add_subdirectory(external)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Expand Down
5 changes: 5 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ add_qtc_plugin(QNVim
qnvimplugin.h
qnvimcore.cpp
qnvimcore.h
cmdline.cpp
cmdline.h
automap.h
textediteventfilter.cpp
textediteventfilter.h
)
95 changes: 95 additions & 0 deletions src/automap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin <mail@gikari.com>
// SPDX-License-Identifier: MIT
#pragma once

#include <QObject>

#include <unordered_map>
#include <concepts>

namespace QNVim::Internal {

class AutoMapBase : public QObject {
Q_OBJECT
protected:
explicit AutoMapBase(QObject *parent = nullptr) : QObject(parent){};
};

template<typename T>
concept QObjectBasedPointer =
std::is_pointer_v<T> && std::is_base_of_v<QObject, std::remove_pointer_t<T>>;

/**
* Map, that automatically deletes pairs, when key or value is deleted,
* given that either a key or a value is a pointer to QObject.
*/
template <typename K, typename V>
requires (QObjectBasedPointer<K> || QObjectBasedPointer<V>)
class AutoMap : public AutoMapBase {
Copy link
Owner

@sassanh sassanh Apr 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to learn a bit about its mission and the rational behind implementing it.

It seems to me that this data-structure has two missions:

  1. Act as a map.
  2. Help with garbage collection.

If that's correct then it may be an anti-pattern regarding Single Responsibility Principle and may make the maintenance of the code hard over time.
An alternative would be to implement a list of pairs for this purpose instead of a map and keep the lookup feature for a simple Qt or std map?

Also I'm under the impression that it will delete the key if the value is deleted and also deletes the value if the key is deleted. So if the cmdline is somehow deleted it deletes the editor and the editorview, right? Is it intentional?

Also in this project we have avoided standard library and sticked with Qt data structures (like using QMap instead of std::map). What do you think about keep this pattern for the sake of consistency?

public:
explicit AutoMap(QObject *parent = nullptr) : AutoMapBase(parent){};

V &at(const K &key) {
return m_map.at(key);
}

auto begin() { return m_map.begin(); };
auto end() { return m_map.end(); };
auto cbegin() const { return m_map.cbegin(); };
auto cend() const { return m_map.cend(); };

auto contains(const K& key) const {
return m_map.contains(key);
}

auto find(const K &key) {
return m_map.find(key);
}

auto find(const K &key) const {
return m_map.find(key);
}

auto insert(const std::pair<K, V> &v) {
auto result = m_map.insert(v);

if (!result.second)
return result;

if constexpr (std::is_base_of_v<QObject, std::remove_pointer<K>>)
connect(v.first, &QObject::destroyed, this, [=]() {
m_map.erase(v.first);
});

if constexpr (std::is_base_of_v<QObject, std::remove_pointer<V>>)
connect(v.second, &QObject::destroyed, this, [=]() {
m_map.erase(v.first);
});

return result;
}

auto insert_or_assign(const K &k, V &&v) {
auto it = m_map.find(k);

if (it == m_map.end()) {
auto [it, _] = this->insert({k, v});
return std::make_pair(it, true);
} else {
if constexpr (std::is_base_of_v<QObject, std::remove_pointer<V>>) {
it->second->disconnect(this);
connect(v, &QObject::destroyed, this, [=]() {
m_map.erase(k);
});
}

it->second = v;
return std::make_pair(it, false);
}
}

private:
std::unordered_map<K, V> m_map;
};

} // namespace QNVim::Internal
243 changes: 243 additions & 0 deletions src/cmdline.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin <mail@gikari.com>
// SPDX-License-Identifier: MIT
#include "cmdline.h"

#include "log.h"
#include "qnvimcore.h"
#include "textediteventfilter.h"

#include <QHBoxLayout>
#include <QPlainTextEdit>
#include <QDebug>
#include <QFuture>
#include <QRegularExpression>

#include <texteditor/fontsettings.h>
#include <texteditor/texteditorsettings.h>
#include <coreplugin/editormanager/editormanager.h>

#include <neovimconnector.h>

namespace QNVim::Internal {

CmdLine::CmdLine(QObject *parent) : QObject(parent) {
// HACK:
// We need the parent of an editor, so that we can inject CmdLine widget below it.
// Parent widget might be absent when EditorManager::editorOpened is emitted.
// Because of that we detect when the parent changes (from nothing to something)
// and only then call our editorOpened callback
connect(
Core::EditorManager::instance(), &Core::EditorManager::editorOpened, this,
[this](Core::IEditor *editor) {
if (!editor)
return;

auto editorWidget = editor->widget();
auto eventFilter = new ParentChangedFilter(editorWidget);
editorWidget->installEventFilter(eventFilter);

connect(
eventFilter, &ParentChangedFilter::parentChanged, this,
[this, editor](QObject *parent) {
this->editorOpened(*editor);
},
Qt::SingleShotConnection);
});

m_core = qobject_cast<QNVimCore*>(parent);
}

void CmdLine::onCmdLineShow(QStringView content,
int pos, QChar firstc, QStringView prompt, int indent) {
// Save params for other requests for cmdline
m_firstChar = firstc;
m_prompt = prompt.toString();
m_indent = indent;

// Hide all cmds, that could possibly be in other splits
for (auto &[_, cmdWidget] : m_uniqueWidgets)
cmdWidget->hide();

auto currentCmdWidget = currentWidget();

if (!currentCmdWidget)
return;

QString text = firstc + prompt + QString(indent, ' ') + content;

currentCmdWidget->setText(text);
currentCmdWidget->setReadOnly(false);
currentCmdWidget->show();

currentCmdWidget->focus();

// Update cursor position
auto cursor = currentCmdWidget->textCursor();
auto cursorPositionFromNvim = firstc.isPrint() + prompt.length() + indent + pos;
cursor.setPosition(cursorPositionFromNvim);
currentCmdWidget->setTextCursor(cursor);
}

void CmdLine::onCmdLineHide()
{
auto currentCmd = currentWidget();

currentCmd->clear();
currentCmd->hide();

// Focus editor, since we are done
auto currentEditor = Core::EditorManager::currentEditor();
if (currentEditor && currentEditor->widget())
currentEditor->widget()->setFocus();
}

void CmdLine::onCmdLinePos(int pos)
{
auto currentCmd = currentWidget();

// Update cursor position
auto cursor = currentCmd->textCursor();
auto cursorPositionFromNvim = m_firstChar.isPrint() + m_prompt.length() + m_indent + pos;
cursor.setPosition(cursorPositionFromNvim);
currentCmd->setTextCursor(cursor);
}

void CmdLine::showMessage(QStringView message)
{
auto currentCmd = currentWidget();

currentCmd->clear();
currentCmd->setReadOnly(true);
currentCmd->setText(message.toString());

currentCmd->show();
}

void CmdLine::clear()
{
auto currentCmd = currentWidget();

currentCmd->clear();
}

void CmdLine::editorOpened(Core::IEditor &editor) {
qDebug(Main) << "CmdLine::editorOpened" << &editor;
if (!m_widgets.contains(&editor)) {
auto editorWidget = editor.widget();
auto stackLayout = editorWidget->parentWidget();
auto editorView = stackLayout->parentWidget();

CmdLineWidget* widgetToAdd = nullptr;

if (auto it = m_uniqueWidgets.find(editorView); it != m_uniqueWidgets.end())
widgetToAdd = it->second; // We already have a widget for that editor
else
widgetToAdd = new CmdLineWidget(m_core, editorView);

m_widgets.insert({&editor, widgetToAdd});
m_uniqueWidgets.insert_or_assign(editorView, std::move(widgetToAdd));
}
}

CmdLineWidget *CmdLine::currentWidget() const {
auto currentEditor = Core::EditorManager::currentEditor();

if (!currentEditor)
return nullptr;

auto it = m_widgets.find(currentEditor);

if (it == m_widgets.cend())
return nullptr;

return it->second;
}

CmdLineWidget::CmdLineWidget(QNVimCore *core, QWidget *parent) : QWidget(parent) {
auto parentLayout = parent->layout();
parentLayout->addWidget(this);

auto pLayout = new QHBoxLayout(this);
pLayout->setContentsMargins(0, 0, 0, 0);

m_pTextWidget = new QPlainTextEdit(this);
m_pTextWidget->document()->setDocumentMargin(0);
m_pTextWidget->setFrameStyle(QFrame::Shape::NoFrame);
m_pTextWidget->setObjectName(QStringLiteral("cmdline"));
m_pTextWidget->setStyleSheet(QStringLiteral("#cmdline { border-top: 1px solid palette(dark) }"));

auto editorFont = TextEditor::TextEditorSettings::instance()->fontSettings().font();
m_pTextWidget->setFont(editorFont);

auto textEditEventFilter = new TextEditEventFilter(core->nvimConnector(), this);
m_pTextWidget->installEventFilter(textEditEventFilter);

connect(m_pTextWidget->document()->documentLayout(),
&QAbstractTextDocumentLayout::documentSizeChanged,
this, &CmdLineWidget::adjustSize);

connect(TextEditor::TextEditorSettings::instance(),
&TextEditor::TextEditorSettings::fontSettingsChanged,
this, [this](const TextEditor::FontSettings &settings) {
m_pTextWidget->setFont(settings.font());
});

adjustSize(m_pTextWidget->document()->size());
// Do not show by default
hide();

pLayout->addWidget(m_pTextWidget);
}

void CmdLineWidget::setText(const QString &text)
{
m_pTextWidget->setPlainText(text);
}

QString CmdLineWidget::text() const
{
return m_pTextWidget->toPlainText();
}

void CmdLineWidget::clear()
{
m_pTextWidget->clear();
}

void CmdLineWidget::focus() const
{
m_pTextWidget->setFocus();
}

void CmdLineWidget::setTextCursor(const QTextCursor &cursor)
{
m_pTextWidget->setTextCursor(cursor);
}

QTextCursor CmdLineWidget::textCursor() const
{
return m_pTextWidget->textCursor();
}

void CmdLineWidget::setReadOnly(bool value)
{
m_pTextWidget->setReadOnly(value);
}

void CmdLineWidget::adjustSize(const QSizeF &newTextDocumentSize)
{
auto fontHeight = m_pTextWidget->fontMetrics().height();
auto newHeight = newTextDocumentSize.height() * fontHeight + m_pTextWidget->frameWidth() * 2;

m_pTextWidget->setMaximumHeight(newHeight);
m_pTextWidget->parentWidget()->setMaximumHeight(newHeight);
}

bool ParentChangedFilter::eventFilter(QObject *watched, QEvent *event) {
if (event->type() == QEvent::ParentChange)
emit parentChanged(watched->parent());

return QObject::eventFilter(watched, event);
}

} // namespace QNVim::Internal
Loading