The editor now has context aware completions
This commit is contained in:
parent
1a1bbd0838
commit
960967c8a6
18 changed files with 1984 additions and 0 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "CodeEditor.h"
|
||||
#include "EditorGutter.h"
|
||||
#include <QKeyEvent>
|
||||
#include <QPainter>
|
||||
#include <QTextBlock>
|
||||
#include <QDebug>
|
||||
|
|
@ -124,9 +125,125 @@ bool CodeEditor::lineHasError(int blockNumber) const
|
|||
return errorLines.count(blockNumber) > 0;
|
||||
}
|
||||
|
||||
void CodeEditor::setCompletionCallback(EditorCompletionCallback callback)
|
||||
{
|
||||
m_completionCallback = std::move(callback);
|
||||
if (!m_completionCallback)
|
||||
hideCompletions();
|
||||
}
|
||||
|
||||
void CodeEditor::showCompletions()
|
||||
{
|
||||
updateCompletions(true);
|
||||
}
|
||||
|
||||
void CodeEditor::hideCompletions()
|
||||
{
|
||||
if (m_completionPopup)
|
||||
m_completionPopup->hide();
|
||||
}
|
||||
|
||||
bool CodeEditor::updateCompletions(bool explicitRequest)
|
||||
{
|
||||
if (!m_completionCallback) {
|
||||
hideCompletions();
|
||||
return false;
|
||||
}
|
||||
const int pos = textCursor().position();
|
||||
EditorCompletionResult result = m_completionCallback(toPlainText(), pos);
|
||||
bool show = !result.items.empty();
|
||||
if (show && !explicitRequest) {
|
||||
// do not bother the user while nothing identifier like has been typed
|
||||
if (result.replaceLen == 0 && !result.hasQualifier)
|
||||
show = false;
|
||||
// the only candidate is what has been typed already
|
||||
else if (result.items.size() == 1 && result.replaceLen > 0) {
|
||||
QTextCursor c = textCursor();
|
||||
c.setPosition(result.replaceStart);
|
||||
c.setPosition(result.replaceStart + result.replaceLen, QTextCursor::KeepAnchor);
|
||||
if (c.selectedText() == result.items[0].insertText)
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
if (!show) {
|
||||
hideCompletions();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_completionPopup) {
|
||||
m_completionPopup = new CompletionPopup(this);
|
||||
connect(m_completionPopup, &CompletionPopup::itemAccepted, this, &CodeEditor::acceptCompletion);
|
||||
}
|
||||
m_completionResult = std::move(result);
|
||||
m_completionPopup->setFont(font());
|
||||
m_completionPopup->setItems(m_completionResult.items);
|
||||
|
||||
QTextCursor c = textCursor();
|
||||
c.setPosition(m_completionResult.replaceStart);
|
||||
QRect r = cursorRect(c);
|
||||
m_completionPopup->showAt(viewport()->mapToGlobal(r.bottomLeft()));
|
||||
return true;
|
||||
}
|
||||
|
||||
void CodeEditor::acceptCompletion(int row)
|
||||
{
|
||||
if (row < 0 || row >= static_cast<int>(m_completionResult.items.size()))
|
||||
return;
|
||||
const QString text = m_completionResult.items[row].insertText;
|
||||
QTextCursor c = textCursor();
|
||||
c.beginEditBlock();
|
||||
c.setPosition(m_completionResult.replaceStart);
|
||||
c.setPosition(m_completionResult.replaceStart + m_completionResult.replaceLen, QTextCursor::KeepAnchor);
|
||||
c.insertText(text);
|
||||
c.endEditBlock();
|
||||
setTextCursor(c);
|
||||
hideCompletions();
|
||||
}
|
||||
|
||||
void CodeEditor::focusOutEvent(QFocusEvent *e)
|
||||
{
|
||||
hideCompletions();
|
||||
QPlainTextEdit::focusOutEvent(e);
|
||||
}
|
||||
|
||||
void CodeEditor::mousePressEvent(QMouseEvent *e)
|
||||
{
|
||||
hideCompletions();
|
||||
QPlainTextEdit::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void CodeEditor::keyPressEvent(QKeyEvent *e)
|
||||
{
|
||||
auto k = e->key();
|
||||
const bool popupVisible = m_completionPopup && m_completionPopup->isVisible();
|
||||
if (popupVisible) {
|
||||
switch (k) {
|
||||
case Qt::Key_Down:
|
||||
m_completionPopup->moveSelection(1);
|
||||
return;
|
||||
case Qt::Key_Up:
|
||||
m_completionPopup->moveSelection(-1);
|
||||
return;
|
||||
case Qt::Key_PageDown:
|
||||
m_completionPopup->moveSelection(10);
|
||||
return;
|
||||
case Qt::Key_PageUp:
|
||||
m_completionPopup->moveSelection(-10);
|
||||
return;
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
case Qt::Key_Tab:
|
||||
acceptCompletion(m_completionPopup->currentRow());
|
||||
return;
|
||||
case Qt::Key_Escape:
|
||||
hideCompletions();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (k == Qt::Key_Space && e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
showCompletions();
|
||||
return;
|
||||
}
|
||||
switch (k) {
|
||||
case Qt::Key_Tab:
|
||||
if (indentSelection(true))
|
||||
|
|
@ -155,6 +272,20 @@ void CodeEditor::keyPressEvent(QKeyEvent *e)
|
|||
break;
|
||||
}
|
||||
QPlainTextEdit::keyPressEvent(e);
|
||||
|
||||
if (m_completionCallback) {
|
||||
if (popupVisible) {
|
||||
if (k == Qt::Key_Backspace || k == Qt::Key_Delete || !e->text().isEmpty())
|
||||
updateCompletions(false);
|
||||
else if (k == Qt::Key_Left || k == Qt::Key_Right || k == Qt::Key_Home || k == Qt::Key_End)
|
||||
hideCompletions();
|
||||
}
|
||||
else if (!e->text().isEmpty() && !e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
const QChar ch = e->text()[0];
|
||||
if (ch.isLetterOrNumber() || ch == QLatin1Char('_') || ch == QLatin1Char('.') || ch == QLatin1Char('"'))
|
||||
updateCompletions(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QPlainTextEdit>
|
||||
#include <set>
|
||||
#include "util/Colors.h"
|
||||
#include "CompletionPopup.h"
|
||||
|
||||
/** This class adds some capabilities to QPlainTextEdit that are useful
|
||||
* in code editor scenarios.
|
||||
|
|
@ -20,9 +21,16 @@ public:
|
|||
void clearErrorMarkers();
|
||||
void setFont(const QFont &);
|
||||
void setTabSize(int chars);
|
||||
|
||||
/// Install the function that supplies completions, an empty function disables completion
|
||||
void setCompletionCallback(EditorCompletionCallback callback);
|
||||
/// Show completions at the cursor, also when nothing has been typed yet
|
||||
void showCompletions();
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void focusOutEvent(QFocusEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
signals:
|
||||
|
||||
|
|
@ -44,6 +52,14 @@ private:
|
|||
|
||||
std::set<int> errorLines;
|
||||
|
||||
EditorCompletionCallback m_completionCallback;
|
||||
CompletionPopup *m_completionPopup = nullptr;
|
||||
EditorCompletionResult m_completionResult;
|
||||
|
||||
bool updateCompletions(bool explicitRequest);
|
||||
void hideCompletions();
|
||||
void acceptCompletion(int row);
|
||||
|
||||
// Settings
|
||||
int m_tabSize = 0; // tabSize in characters
|
||||
bool m_useTab = false;
|
||||
|
|
|
|||
112
pglab/codeeditor/CompletionPopup.cpp
Normal file
112
pglab/codeeditor/CompletionPopup.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#include "CompletionPopup.h"
|
||||
#include <QApplication>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int DetailRole = Qt::UserRole + 1;
|
||||
constexpr int AmbiguousRole = Qt::UserRole + 2;
|
||||
|
||||
class CompletionDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
|
||||
{
|
||||
QStyleOptionViewItem opt = option;
|
||||
initStyleOption(&opt, index);
|
||||
const QString display = opt.text;
|
||||
opt.text.clear();
|
||||
const QWidget *w = opt.widget;
|
||||
(w ? w->style() : QApplication::style())->drawControl(QStyle::CE_ItemViewItem, &opt, painter, w);
|
||||
|
||||
const bool selected = opt.state & QStyle::State_Selected;
|
||||
const QPalette &pal = opt.palette;
|
||||
const QString detail = index.data(DetailRole).toString();
|
||||
const bool ambiguous = index.data(AmbiguousRole).toBool();
|
||||
|
||||
painter->save();
|
||||
painter->setPen(selected ? pal.highlightedText().color() : pal.text().color());
|
||||
QRect r = opt.rect.adjusted(4, 0, -4, 0);
|
||||
painter->drawText(r, Qt::AlignVCenter | Qt::AlignLeft, display);
|
||||
if (!detail.isEmpty()) {
|
||||
const int x = painter->fontMetrics().horizontalAdvance(display) + 12;
|
||||
QColor dim = selected ? pal.highlightedText().color() : pal.color(QPalette::Disabled, QPalette::Text);
|
||||
painter->setPen(dim);
|
||||
if (ambiguous) {
|
||||
QFont f = painter->font();
|
||||
f.setBold(true);
|
||||
painter->setFont(f);
|
||||
}
|
||||
painter->drawText(r.adjusted(x, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, QStringLiteral("\u2014 ") + detail);
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
|
||||
{
|
||||
QSize s = QStyledItemDelegate::sizeHint(option, index);
|
||||
const QFontMetrics fm(option.font);
|
||||
s.setWidth(fm.horizontalAdvance(index.data(Qt::DisplayRole).toString())
|
||||
+ fm.horizontalAdvance(index.data(DetailRole).toString()) + 40);
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
CompletionPopup::CompletionPopup(QWidget *parent)
|
||||
: QListWidget(parent)
|
||||
{
|
||||
setWindowFlags(Qt::ToolTip);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setUniformItemSizes(true);
|
||||
setItemDelegate(new CompletionDelegate(this));
|
||||
connect(this, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) {
|
||||
emit itemAccepted(row(item));
|
||||
});
|
||||
}
|
||||
|
||||
void CompletionPopup::setItems(const std::vector<EditorCompletionItem> &items)
|
||||
{
|
||||
clear();
|
||||
int width = 150;
|
||||
const QFontMetrics fm(font());
|
||||
for (const auto &i : items) {
|
||||
auto *item = new QListWidgetItem(i.display, this);
|
||||
item->setData(DetailRole, i.detail);
|
||||
item->setData(AmbiguousRole, i.ambiguous);
|
||||
width = std::max(width, fm.horizontalAdvance(i.display) + fm.horizontalAdvance(i.detail) + 50);
|
||||
}
|
||||
if (!items.empty())
|
||||
setCurrentRow(0);
|
||||
const int rows = static_cast<int>(std::min<size_t>(items.size(), 10));
|
||||
const int rowHeight = fm.height() + 6;
|
||||
resize(std::min(width, 600), rows * rowHeight + 2 * frameWidth() + 2);
|
||||
}
|
||||
|
||||
void CompletionPopup::moveSelection(int delta)
|
||||
{
|
||||
const int n = count();
|
||||
if (n == 0)
|
||||
return;
|
||||
int r = QListWidget::currentRow() + delta;
|
||||
r = std::clamp(r, 0, n - 1);
|
||||
setCurrentRow(r);
|
||||
}
|
||||
|
||||
void CompletionPopup::showAt(const QPoint &globalPos)
|
||||
{
|
||||
QPoint p = globalPos;
|
||||
if (auto *screen = QApplication::screenAt(globalPos)) {
|
||||
const QRect avail = screen->availableGeometry();
|
||||
if (p.x() + width() > avail.right())
|
||||
p.setX(std::max(avail.left(), avail.right() - width()));
|
||||
}
|
||||
move(p);
|
||||
show();
|
||||
}
|
||||
40
pglab/codeeditor/CompletionPopup.h
Normal file
40
pglab/codeeditor/CompletionPopup.h
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#ifndef COMPLETIONPOPUP_H
|
||||
#define COMPLETIONPOPUP_H
|
||||
|
||||
#include <QListWidget>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
struct EditorCompletionItem {
|
||||
QString display;
|
||||
QString detail;
|
||||
QString insertText;
|
||||
bool ambiguous = false;
|
||||
};
|
||||
|
||||
struct EditorCompletionResult {
|
||||
int replaceStart = 0;
|
||||
int replaceLen = 0;
|
||||
bool hasQualifier = false; // completing behind "name."
|
||||
std::vector<EditorCompletionItem> items;
|
||||
};
|
||||
|
||||
/// Callback that determines the completions for a cursor position in the text
|
||||
using EditorCompletionCallback = std::function<EditorCompletionResult(const QString &text, int cursorPos)>;
|
||||
|
||||
/// List shown below the cursor, it never takes keyboard focus so typing continues in the editor.
|
||||
class CompletionPopup : public QListWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CompletionPopup(QWidget *parent);
|
||||
|
||||
void setItems(const std::vector<EditorCompletionItem> &items);
|
||||
void moveSelection(int delta);
|
||||
int currentRow() const { return QListWidget::currentRow(); }
|
||||
void showAt(const QPoint &globalPos);
|
||||
|
||||
signals:
|
||||
void itemAccepted(int row);
|
||||
};
|
||||
|
||||
#endif // COMPLETIONPOPUP_H
|
||||
Loading…
Add table
Add a link
Reference in a new issue