The editor now has context aware completions

This commit is contained in:
eelke 2026-10-22 17:52:37 +02:00
parent 1a1bbd0838
commit 960967c8a6
18 changed files with 1984 additions and 0 deletions

View file

@ -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);
}
}
}