-
Notifications
You must be signed in to change notification settings - Fork 8
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
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin <[email protected]> | ||
// 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 { | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
// SPDX-FileCopyrightText: 2023 Mikhail Zolotukhin <[email protected]> | ||
// 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
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?