1306 lines
38 KiB
C++
1306 lines
38 KiB
C++
#include "codeeditorwidget.h"
|
||
|
||
#include <QPlainTextEdit>
|
||
#include <QVBoxLayout>
|
||
#include <QFile>
|
||
#include <QTextStream>
|
||
#include <QFontDatabase>
|
||
#include <QPainter>
|
||
#include <QTextBlock>
|
||
#include <QMouseEvent>
|
||
#include <QSyntaxHighlighter>
|
||
#include <QRegularExpression>
|
||
#include <QStringConverter>
|
||
#include <QTextCharFormat>
|
||
#include <QtGlobal>
|
||
|
||
#include <QListView>
|
||
#include <QStringListModel>
|
||
#include <QStyledItemDelegate>
|
||
#include <QTextDocument>
|
||
#include <QApplication>
|
||
|
||
#include <QLineEdit>
|
||
#include <QLabel>
|
||
#include <QToolButton>
|
||
#include <QPushButton>
|
||
#include <QHBoxLayout>
|
||
#include <QShortcut>
|
||
#include <QKeySequence>
|
||
#include <QPalette>
|
||
|
||
|
||
|
||
// ==================== 辅助类:行号区域 ====================
|
||
|
||
class LineNumberArea : public QWidget
|
||
{
|
||
public:
|
||
explicit LineNumberArea(CodeEditor *editor)
|
||
: QWidget(editor)
|
||
, m_codeEditor(editor)
|
||
{}
|
||
|
||
QSize sizeHint() const override
|
||
{
|
||
return QSize(m_codeEditor->lineNumberAreaWidth(), 0);
|
||
}
|
||
|
||
protected:
|
||
void paintEvent(QPaintEvent *event) override
|
||
{
|
||
m_codeEditor->lineNumberAreaPaintEvent(event);
|
||
}
|
||
|
||
void mousePressEvent(QMouseEvent *event) override
|
||
{
|
||
m_codeEditor->lineNumberAreaMousePressEvent(event);
|
||
}
|
||
|
||
private:
|
||
CodeEditor *m_codeEditor;
|
||
};
|
||
|
||
// ==================== 辅助类:C 语言风格高亮 ====================
|
||
|
||
class CSyntaxHighlighter : public QSyntaxHighlighter
|
||
{
|
||
public:
|
||
explicit CSyntaxHighlighter(QTextDocument *parent = nullptr)
|
||
: QSyntaxHighlighter(parent)
|
||
{
|
||
initRules();
|
||
}
|
||
|
||
protected:
|
||
void highlightBlock(const QString &text) override
|
||
{
|
||
// 普通规则
|
||
for (const HighlightingRule &rule : m_highlightingRules) {
|
||
QRegularExpressionMatchIterator i = rule.pattern.globalMatch(text);
|
||
while (i.hasNext()) {
|
||
QRegularExpressionMatch match = i.next();
|
||
setFormat(match.capturedStart(), match.capturedLength(), rule.format);
|
||
}
|
||
}
|
||
|
||
// 多行注释
|
||
setCurrentBlockState(0);
|
||
|
||
int startIndex = 0;
|
||
if (previousBlockState() != 1)
|
||
startIndex = text.indexOf(m_commentStartExpression);
|
||
|
||
while (startIndex >= 0) {
|
||
QRegularExpressionMatch match = m_commentEndExpression.match(text, startIndex);
|
||
int endIndex = match.capturedStart();
|
||
int commentLength;
|
||
if (endIndex == -1) {
|
||
setCurrentBlockState(1);
|
||
commentLength = text.length() - startIndex;
|
||
} else {
|
||
commentLength = endIndex - startIndex + match.capturedLength();
|
||
}
|
||
setFormat(startIndex, commentLength, m_multiLineCommentFormat);
|
||
startIndex = text.indexOf(m_commentStartExpression, startIndex + commentLength);
|
||
}
|
||
}
|
||
|
||
private:
|
||
struct HighlightingRule
|
||
{
|
||
QRegularExpression pattern;
|
||
QTextCharFormat format;
|
||
};
|
||
|
||
void initRules()
|
||
{
|
||
HighlightingRule rule;
|
||
|
||
// 关键字
|
||
QTextCharFormat keywordFormat;
|
||
keywordFormat.setForeground(Qt::blue);
|
||
keywordFormat.setFontWeight(QFont::Bold);
|
||
|
||
const QStringList keywordPatterns = {
|
||
QStringLiteral("\\bauto\\b"),
|
||
QStringLiteral("\\bbreak\\b"),
|
||
QStringLiteral("\\bcase\\b"),
|
||
QStringLiteral("\\bchar\\b"),
|
||
QStringLiteral("\\bconst\\b"),
|
||
QStringLiteral("\\bcontinue\\b"),
|
||
QStringLiteral("\\bdefault\\b"),
|
||
QStringLiteral("\\bdo\\b"),
|
||
QStringLiteral("\\bdouble\\b"),
|
||
QStringLiteral("\\belse\\b"),
|
||
QStringLiteral("\\benum\\b"),
|
||
QStringLiteral("\\bextern\\b"),
|
||
QStringLiteral("\\bfloat\\b"),
|
||
QStringLiteral("\\bfor\\b"),
|
||
QStringLiteral("\\bgoto\\b"),
|
||
QStringLiteral("\\bif\\b"),
|
||
QStringLiteral("\\binline\\b"),
|
||
QStringLiteral("\\bint\\b"),
|
||
QStringLiteral("\\blong\\b"),
|
||
QStringLiteral("\\bregister\\b"),
|
||
QStringLiteral("\\brestrict\\b"),
|
||
QStringLiteral("\\breturn\\b"),
|
||
QStringLiteral("\\bshort\\b"),
|
||
QStringLiteral("\\bsigned\\b"),
|
||
QStringLiteral("\\bsizeof\\b"),
|
||
QStringLiteral("\\bstatic\\b"),
|
||
QStringLiteral("\\bstruct\\b"),
|
||
QStringLiteral("\\bswitch\\b"),
|
||
QStringLiteral("\\btypedef\\b"),
|
||
QStringLiteral("\\bunion\\b"),
|
||
QStringLiteral("\\bunsigned\\b"),
|
||
QStringLiteral("\\bvoid\\b"),
|
||
QStringLiteral("\\bvolatile\\b"),
|
||
QStringLiteral("\\bwhile\\b"),
|
||
|
||
// C++ / 常用扩展
|
||
QStringLiteral("\\bclass\\b"),
|
||
QStringLiteral("\\btemplate\\b"),
|
||
QStringLiteral("\\btypename\\b"),
|
||
QStringLiteral("\\bnamespace\\b"),
|
||
QStringLiteral("\\busing\\b"),
|
||
QStringLiteral("\\bbool\\b"),
|
||
QStringLiteral("\\btrue\\b"),
|
||
QStringLiteral("\\bfalse\\b"),
|
||
};
|
||
|
||
for (const QString &pattern : keywordPatterns) {
|
||
rule.pattern = QRegularExpression(pattern);
|
||
rule.format = keywordFormat;
|
||
m_highlightingRules.append(rule);
|
||
}
|
||
|
||
// 单行注释 //
|
||
QTextCharFormat singleLineCommentFormat;
|
||
singleLineCommentFormat.setForeground(Qt::darkGreen);
|
||
rule.pattern = QRegularExpression(QStringLiteral("//[^\n]*"));
|
||
rule.format = singleLineCommentFormat;
|
||
m_highlightingRules.append(rule);
|
||
|
||
// 字符串 "xxx"
|
||
QTextCharFormat quotationFormat;
|
||
quotationFormat.setForeground(Qt::darkRed);
|
||
rule.pattern = QRegularExpression(QStringLiteral("\".*\""));
|
||
rule.format = quotationFormat;
|
||
m_highlightingRules.append(rule);
|
||
|
||
// 字符常量 'x'
|
||
QTextCharFormat charFormat;
|
||
charFormat.setForeground(Qt::darkRed);
|
||
rule.pattern = QRegularExpression(QStringLiteral("'[^']*'"));
|
||
rule.format = charFormat;
|
||
m_highlightingRules.append(rule);
|
||
|
||
// 函数名 foo(...)
|
||
QTextCharFormat functionFormat;
|
||
functionFormat.setForeground(Qt::darkMagenta);
|
||
rule.pattern = QRegularExpression(QStringLiteral("\\b[A-Za-z_][A-Za-z0-9_]*(?=\\()"));
|
||
rule.format = functionFormat;
|
||
m_highlightingRules.append(rule);
|
||
|
||
// 多行注释 /* ... */
|
||
m_commentStartExpression = QRegularExpression(QStringLiteral("/\\*"));
|
||
m_commentEndExpression = QRegularExpression(QStringLiteral("\\*/"));
|
||
|
||
m_multiLineCommentFormat.setForeground(Qt::darkGreen);
|
||
m_multiLineCommentFormat.setFontItalic(true);
|
||
}
|
||
|
||
private:
|
||
QVector<HighlightingRule> m_highlightingRules;
|
||
QRegularExpression m_commentStartExpression;
|
||
QRegularExpression m_commentEndExpression;
|
||
QTextCharFormat m_multiLineCommentFormat;
|
||
};
|
||
|
||
// ==================== CodeEditor 实现 ====================
|
||
|
||
class CompletionItemDelegate : public QStyledItemDelegate
|
||
{
|
||
public:
|
||
explicit CompletionItemDelegate(QObject *parent = nullptr)
|
||
: QStyledItemDelegate(parent) {}
|
||
|
||
void setPrefix(const QString &prefix) { m_prefix = prefix; }
|
||
|
||
void paint(QPainter *painter,
|
||
const QStyleOptionViewItem &option,
|
||
const QModelIndex &index) const override
|
||
{
|
||
QStyleOptionViewItem opt(option);
|
||
initStyleOption(&opt, index);
|
||
|
||
QString text = opt.text;
|
||
opt.text.clear(); // 不让默认的绘制文本
|
||
|
||
QStyle *style = opt.widget ? opt.widget->style()
|
||
: QApplication::style();
|
||
|
||
// 背景:选中时淡蓝色
|
||
if (opt.state & QStyle::State_Selected) {
|
||
painter->fillRect(opt.rect, QColor(173, 216, 230, 200)); // light blue
|
||
opt.state &= ~QStyle::State_Selected;
|
||
} else if (opt.state & QStyle::State_MouseOver) {
|
||
painter->fillRect(opt.rect, QColor(230, 240, 250));
|
||
}
|
||
|
||
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
|
||
|
||
// 构造 HTML:匹配前缀加粗
|
||
QString escaped = text.toHtmlEscaped();
|
||
QString html;
|
||
if (!m_prefix.isEmpty() &&
|
||
text.startsWith(m_prefix, Qt::CaseInsensitive)) {
|
||
int len = m_prefix.length();
|
||
QString head = escaped.left(len);
|
||
QString tail = escaped.mid(len);
|
||
html = QString("<b>%1</b>%2").arg(head, tail);
|
||
} else {
|
||
html = escaped;
|
||
}
|
||
|
||
QTextDocument doc;
|
||
doc.setDefaultFont(opt.font);
|
||
doc.setHtml(html);
|
||
|
||
painter->save();
|
||
painter->setClipRect(opt.rect.adjusted(2, 0, -2, 0));
|
||
painter->translate(opt.rect.left() + 4, opt.rect.top());
|
||
QRectF clip(0, 0, opt.rect.width() - 8, opt.rect.height());
|
||
doc.drawContents(painter, clip);
|
||
painter->restore();
|
||
}
|
||
|
||
private:
|
||
QString m_prefix;
|
||
};
|
||
|
||
class CompletionPopup : public QFrame
|
||
{
|
||
public:
|
||
explicit CompletionPopup(QWidget *parent = nullptr)
|
||
: QFrame(parent)
|
||
{
|
||
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
|
||
setFrameStyle(QFrame::Box | QFrame::Plain);
|
||
|
||
m_view = new QListView(this);
|
||
m_model = new QStringListModel(this);
|
||
m_delegate = new CompletionItemDelegate(this);
|
||
|
||
m_view->setModel(m_model);
|
||
m_view->setItemDelegate(m_delegate);
|
||
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
m_view->setSelectionMode(QAbstractItemView::SingleSelection);
|
||
m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
|
||
auto layout = new QVBoxLayout(this);
|
||
layout->setContentsMargins(0, 0, 0, 0);
|
||
layout->addWidget(m_view);
|
||
}
|
||
|
||
void setWords(const QStringList &words, const QString &prefix)
|
||
{
|
||
m_model->setStringList(words);
|
||
m_delegate->setPrefix(prefix);
|
||
|
||
if (words.isEmpty()) {
|
||
hide();
|
||
return;
|
||
}
|
||
|
||
m_view->setCurrentIndex(m_model->index(0, 0));
|
||
resizeToContent();
|
||
}
|
||
|
||
bool hasItems() const
|
||
{
|
||
return m_model->rowCount() > 0;
|
||
}
|
||
|
||
void selectNext()
|
||
{
|
||
if (!hasItems()) return;
|
||
QModelIndex idx = m_view->currentIndex();
|
||
int row = idx.isValid() ? idx.row() : 0;
|
||
row = (row + 1) % m_model->rowCount();
|
||
m_view->setCurrentIndex(m_model->index(row, 0));
|
||
}
|
||
|
||
void selectPrevious()
|
||
{
|
||
if (!hasItems()) return;
|
||
QModelIndex idx = m_view->currentIndex();
|
||
int row = idx.isValid() ? idx.row() : 0;
|
||
row = (row - 1 + m_model->rowCount()) % m_model->rowCount();
|
||
m_view->setCurrentIndex(m_model->index(row, 0));
|
||
}
|
||
|
||
QString currentText() const
|
||
{
|
||
QModelIndex idx = m_view->currentIndex();
|
||
if (!idx.isValid()) return QString();
|
||
return idx.data(Qt::DisplayRole).toString();
|
||
}
|
||
|
||
QListView *view() const { return m_view; }
|
||
|
||
protected:
|
||
void focusOutEvent(QFocusEvent *event) override
|
||
{
|
||
QFrame::focusOutEvent(event);
|
||
hide();
|
||
}
|
||
|
||
private:
|
||
void resizeToContent()
|
||
{
|
||
QFontMetrics fm(font());
|
||
int w = 0;
|
||
int h = 0;
|
||
int count = m_model->rowCount();
|
||
|
||
for (int i = 0; i < count; ++i) {
|
||
QString text = m_model->data(m_model->index(i, 0),
|
||
Qt::DisplayRole).toString();
|
||
int width = fm.horizontalAdvance(text) + 16;
|
||
w = qMax(w, width);
|
||
h += fm.height() + 4;
|
||
}
|
||
|
||
const int maxVisible = 8;
|
||
if (count > maxVisible) {
|
||
h = (fm.height() + 4) * maxVisible;
|
||
}
|
||
|
||
resize(w, h);
|
||
}
|
||
|
||
private:
|
||
QListView *m_view = nullptr;
|
||
QStringListModel *m_model = nullptr;
|
||
CompletionItemDelegate *m_delegate = nullptr;
|
||
};
|
||
|
||
|
||
// ==================== 查找/替换条(位于编辑器上方) ====================
|
||
|
||
class FindReplaceBar : public QWidget
|
||
{
|
||
public:
|
||
explicit FindReplaceBar(CodeEditor *editor, QWidget *parent = nullptr)
|
||
: QWidget(parent)
|
||
, m_editor(editor)
|
||
{
|
||
setAutoFillBackground(true);
|
||
QPalette pal = palette();
|
||
pal.setColor(QPalette::Window, QColor(230, 230, 230));
|
||
setPalette(pal);
|
||
|
||
m_searchEdit = new QLineEdit(this);
|
||
m_searchEdit->setPlaceholderText(QStringLiteral("查找..."));
|
||
|
||
m_replaceEdit = new QLineEdit(this);
|
||
m_replaceEdit->setPlaceholderText(QStringLiteral("替换为..."));
|
||
|
||
m_replaceToggle = new QToolButton(this);
|
||
m_replaceToggle->setText(QStringLiteral("替换"));
|
||
m_replaceToggle->setCheckable(true);
|
||
|
||
m_closeButton = new QToolButton(this);
|
||
m_closeButton->setText(QStringLiteral("×"));
|
||
|
||
auto *replaceLayout = new QHBoxLayout;
|
||
replaceLayout->setContentsMargins(0, 0, 0, 0);
|
||
replaceLayout->addWidget(new QLabel(QStringLiteral("替换为:"), this));
|
||
replaceLayout->addWidget(m_replaceEdit);
|
||
|
||
m_replacePanel = new QWidget(this);
|
||
m_replacePanel->setLayout(replaceLayout);
|
||
m_replacePanel->setVisible(false); // 默认隐藏,只有点“替换”才展开
|
||
|
||
auto *layout = new QHBoxLayout(this);
|
||
layout->setContentsMargins(4, 2, 4, 2);
|
||
layout->addWidget(new QLabel(QStringLiteral("查找:"), this));
|
||
layout->addWidget(m_searchEdit, 1);
|
||
layout->addWidget(m_replaceToggle);
|
||
layout->addWidget(m_replacePanel, 2);
|
||
layout->addWidget(m_closeButton);
|
||
|
||
// 查找:在查找框按回车 -> searchNext
|
||
connect(m_searchEdit, &QLineEdit::returnPressed,
|
||
this, [this]() {
|
||
if (!m_editor) return;
|
||
m_editor->searchNext(m_searchEdit->text());
|
||
});
|
||
|
||
// 切换“替换模式”
|
||
connect(m_replaceToggle, &QToolButton::toggled,
|
||
this, [this](bool on) {
|
||
m_replacePanel->setVisible(on);
|
||
});
|
||
|
||
// 关闭条:同时清除高亮
|
||
connect(m_closeButton, &QToolButton::clicked,
|
||
this, [this]() {
|
||
this->hide();
|
||
if (m_editor) {
|
||
m_editor->searchNext(QString()); // 空字符串 -> 清除匹配
|
||
}
|
||
});
|
||
|
||
// 在“替换为”输入框按回车 -> 替换当前匹配并跳到下一处
|
||
connect(m_replaceEdit, &QLineEdit::returnPressed,
|
||
this, [this]() {
|
||
if (!m_editor) return;
|
||
const QString searchText = m_searchEdit->text();
|
||
const QString replaceText = m_replaceEdit->text();
|
||
if (searchText.isEmpty()) return;
|
||
|
||
QTextCursor cursor = m_editor->textCursor();
|
||
QString selected = cursor.selectedText();
|
||
if (!selected.isEmpty() &&
|
||
selected.compare(searchText, Qt::CaseInsensitive) == 0) {
|
||
cursor.insertText(replaceText);
|
||
m_editor->setTextCursor(cursor);
|
||
}
|
||
|
||
// 替换完后继续查找下一处
|
||
m_editor->searchNext(searchText);
|
||
});
|
||
}
|
||
|
||
void focusSearch()
|
||
{
|
||
m_searchEdit->setFocus();
|
||
m_searchEdit->selectAll();
|
||
}
|
||
|
||
void setSearchText(const QString &text)
|
||
{
|
||
if (!text.isEmpty())
|
||
m_searchEdit->setText(text);
|
||
}
|
||
|
||
private:
|
||
CodeEditor *m_editor = nullptr;
|
||
QLineEdit *m_searchEdit = nullptr;
|
||
QLineEdit *m_replaceEdit = nullptr;
|
||
QWidget *m_replacePanel = nullptr;
|
||
QToolButton *m_replaceToggle = nullptr;
|
||
QToolButton *m_closeButton = nullptr;
|
||
};
|
||
|
||
|
||
|
||
|
||
CodeEditor::CodeEditor(QWidget *parent)
|
||
: QPlainTextEdit(parent)
|
||
{
|
||
m_lineNumberArea = new LineNumberArea(this);
|
||
|
||
connect(this, &QPlainTextEdit::blockCountChanged,
|
||
this, &CodeEditor::updateLineNumberAreaWidth);
|
||
connect(this, &QPlainTextEdit::updateRequest,
|
||
this, &CodeEditor::updateLineNumberArea);
|
||
connect(this, &QPlainTextEdit::cursorPositionChanged,
|
||
this, &CodeEditor::highlightCurrentLine);
|
||
|
||
// 默认等宽字体
|
||
QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||
font.setPointSize(12);
|
||
setFont(font);
|
||
|
||
// 自动换行,但行号以逻辑行(回车分隔)为准
|
||
setLineWrapMode(QPlainTextEdit::WidgetWidth);
|
||
|
||
updateLineNumberAreaWidth(0);
|
||
|
||
// C 语言高亮
|
||
m_highlighter = new CSyntaxHighlighter(document());
|
||
|
||
highlightCurrentLine();
|
||
|
||
initCompletion();
|
||
}
|
||
|
||
void CodeEditor::initCompletion()
|
||
{
|
||
// 简单放一批 C/C++ 关键字和常用单词
|
||
m_completionWords = QStringList{
|
||
"auto","break","case","char","const","continue","default","do","double","else",
|
||
"enum","extern","float","for","goto","if","inline","int","long","register",
|
||
"restrict","return","short","signed","sizeof","static","struct","switch",
|
||
"typedef","union","unsigned","void","volatile","while",
|
||
"bool","class","template","typename","namespace","using",
|
||
"printf","scanf","main"
|
||
};
|
||
|
||
m_completionPopup = new CompletionPopup(this);
|
||
m_completionPopup->hide();
|
||
|
||
// 点击候选项时插入补全
|
||
connect(m_completionPopup->view(), &QListView::clicked,
|
||
this, [this](const QModelIndex &index) {
|
||
const QString text = index.data(Qt::DisplayRole).toString();
|
||
insertCompletion(text);
|
||
});
|
||
}
|
||
|
||
QString CodeEditor::wordUnderCursor() const
|
||
{
|
||
QTextCursor cursor = textCursor();
|
||
QTextBlock block = cursor.block();
|
||
QString text = block.text();
|
||
int posInBlock = cursor.position() - block.position();
|
||
|
||
int start = posInBlock;
|
||
while (start > 0) {
|
||
QChar ch = text.at(start - 1);
|
||
if (ch.isLetterOrNumber() || ch == QLatin1Char('_'))
|
||
--start;
|
||
else
|
||
break;
|
||
}
|
||
|
||
return text.mid(start, posInBlock - start);
|
||
}
|
||
|
||
void CodeEditor::showCompletion(const QString &prefix)
|
||
{
|
||
if (!m_completionPopup)
|
||
return;
|
||
|
||
QString p = prefix.trimmed();
|
||
if (p.isEmpty()) {
|
||
m_completionPopup->hide();
|
||
return;
|
||
}
|
||
|
||
QStringList matches;
|
||
for (const QString &w : m_completionWords) {
|
||
if (w.startsWith(p, Qt::CaseInsensitive))
|
||
matches << w;
|
||
}
|
||
|
||
if (matches.isEmpty()) {
|
||
m_completionPopup->hide();
|
||
return;
|
||
}
|
||
|
||
m_completionPopup->setWords(matches, p);
|
||
|
||
// 计算弹窗位置:光标下方一点
|
||
QRect cr = cursorRect();
|
||
QPoint pos = cr.bottomLeft();
|
||
pos = mapToGlobal(pos);
|
||
pos.setY(pos.y() + 2);
|
||
|
||
m_completionPopup->move(pos);
|
||
m_completionPopup->show();
|
||
}
|
||
|
||
void CodeEditor::hideCompletion()
|
||
{
|
||
if (m_completionPopup)
|
||
m_completionPopup->hide();
|
||
}
|
||
|
||
void CodeEditor::insertCompletion(const QString &completion)
|
||
{
|
||
if (completion.isEmpty())
|
||
return;
|
||
|
||
hideCompletion();
|
||
|
||
QTextCursor cursor = textCursor();
|
||
QTextBlock block = cursor.block();
|
||
QString text = block.text();
|
||
int posInBlock = cursor.position() - block.position();
|
||
|
||
int start = posInBlock;
|
||
while (start > 0) {
|
||
QChar ch = text.at(start - 1);
|
||
if (ch.isLetterOrNumber() || ch == QLatin1Char('_'))
|
||
--start;
|
||
else
|
||
break;
|
||
}
|
||
|
||
int prefixLen = posInBlock - start;
|
||
|
||
cursor.beginEditBlock();
|
||
cursor.setPosition(block.position() + start);
|
||
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, prefixLen);
|
||
cursor.removeSelectedText();
|
||
cursor.insertText(completion);
|
||
cursor.endEditBlock();
|
||
|
||
setTextCursor(cursor);
|
||
}
|
||
|
||
|
||
int CodeEditor::lineNumberAreaWidth() const
|
||
{
|
||
int digits = 1;
|
||
int max = qMax(1, blockCount());
|
||
while (max >= 10) {
|
||
max /= 10;
|
||
++digits;
|
||
}
|
||
|
||
int space = 4 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
|
||
return space + 6; // 再多一点留白
|
||
}
|
||
|
||
void CodeEditor::updateLineNumberAreaWidth(int)
|
||
{
|
||
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
|
||
}
|
||
|
||
void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
|
||
{
|
||
if (dy != 0) {
|
||
m_lineNumberArea->scroll(0, dy);
|
||
} else {
|
||
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
|
||
}
|
||
|
||
if (rect.contains(viewport()->rect())) {
|
||
updateLineNumberAreaWidth(0);
|
||
}
|
||
}
|
||
|
||
void CodeEditor::resizeEvent(QResizeEvent *event)
|
||
{
|
||
QPlainTextEdit::resizeEvent(event);
|
||
|
||
QRect cr = contentsRect();
|
||
m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(),
|
||
lineNumberAreaWidth(), cr.height()));
|
||
}
|
||
|
||
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
|
||
{
|
||
QPainter painter(m_lineNumberArea);
|
||
painter.fillRect(event->rect(), QColor(245, 245, 245));
|
||
|
||
QTextBlock block = firstVisibleBlock();
|
||
int blockNumber = block.blockNumber();
|
||
int top = static_cast<int>(blockBoundingGeometry(block)
|
||
.translated(contentOffset()).top());
|
||
int bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||
|
||
const int width = m_lineNumberArea->width();
|
||
const QFontMetrics fm(font());
|
||
|
||
while (block.isValid() && top <= event->rect().bottom()) {
|
||
if (block.isVisible() && bottom >= event->rect().top()) {
|
||
int lineNumber = blockNumber + 1;
|
||
|
||
QRect lineRect(0, top, width, fm.height());
|
||
|
||
// 断点背景(淡蓝色)
|
||
if (m_breakpoints.contains(lineNumber)) {
|
||
QColor bpColor(173, 216, 230, 180); // light blue
|
||
painter.fillRect(lineRect, bpColor);
|
||
}
|
||
|
||
painter.setPen(Qt::gray);
|
||
painter.drawText(lineRect.adjusted(0, 0, -4, 0),
|
||
Qt::AlignRight | Qt::AlignVCenter,
|
||
QString::number(lineNumber));
|
||
}
|
||
|
||
block = block.next();
|
||
top = bottom;
|
||
bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||
++blockNumber;
|
||
}
|
||
}
|
||
|
||
void CodeEditor::lineNumberAreaMousePressEvent(QMouseEvent *event)
|
||
{
|
||
int y = event->pos().y();
|
||
|
||
QTextBlock block = firstVisibleBlock();
|
||
int blockNumber = block.blockNumber();
|
||
int top = static_cast<int>(blockBoundingGeometry(block)
|
||
.translated(contentOffset()).top());
|
||
int bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||
|
||
while (block.isValid() && top <= y) {
|
||
if (block.isVisible() && y <= bottom) {
|
||
int line = blockNumber + 1;
|
||
toggleBreakpoint(line);
|
||
break;
|
||
}
|
||
|
||
block = block.next();
|
||
top = bottom;
|
||
bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||
++blockNumber;
|
||
}
|
||
}
|
||
|
||
void CodeEditor::toggleBreakpoint(int line)
|
||
{
|
||
bool enabled;
|
||
|
||
if (m_breakpoints.contains(line)) {
|
||
m_breakpoints.remove(line);
|
||
enabled = false;
|
||
} else {
|
||
m_breakpoints.insert(line);
|
||
enabled = true;
|
||
}
|
||
|
||
// 只需要刷新行号区域
|
||
m_lineNumberArea->update();
|
||
emit breakpointToggled(line, enabled);
|
||
}
|
||
|
||
void CodeEditor::highlightCurrentLine()
|
||
{
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
// 将:当前行高亮 + 自定义行/字符背景,一起统一设置
|
||
void CodeEditor::refreshExtraSelections()
|
||
{
|
||
QList<QTextEdit::ExtraSelection> selections;
|
||
|
||
// 当前行高亮
|
||
if (!isReadOnly()) {
|
||
QTextEdit::ExtraSelection selection;
|
||
QColor lineColor(232, 242, 254); // 很浅的蓝色
|
||
selection.format.setBackground(lineColor);
|
||
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
||
selection.cursor = textCursor();
|
||
selection.cursor.clearSelection();
|
||
selections.append(selection);
|
||
}
|
||
|
||
// 行背景
|
||
for (auto it = m_lineBackgrounds.constBegin(); it != m_lineBackgrounds.constEnd(); ++it) {
|
||
int line = it.key();
|
||
const LineBackgroundInfo &info = it.value();
|
||
|
||
QTextBlock block = document()->findBlockByNumber(line - 1);
|
||
if (!block.isValid())
|
||
continue;
|
||
|
||
QTextCursor cursor(block);
|
||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||
|
||
QTextEdit::ExtraSelection selection;
|
||
QColor c = info.color;
|
||
c.setAlphaF(qBound(0.0, info.alpha, 1.0));
|
||
selection.format.setBackground(c);
|
||
selection.cursor = cursor;
|
||
selections.append(selection);
|
||
}
|
||
|
||
// 字符背景
|
||
for (auto it = m_charBackgrounds.constBegin(); it != m_charBackgrounds.constEnd(); ++it) {
|
||
int line = it.key();
|
||
const QVector<CharBackgroundInfo> &infos = it.value();
|
||
|
||
QTextBlock block = document()->findBlockByNumber(line - 1);
|
||
if (!block.isValid())
|
||
continue;
|
||
|
||
const QString text = block.text();
|
||
int blockStartPos = block.position();
|
||
|
||
for (const CharBackgroundInfo &info : infos) {
|
||
if (info.column < 0 || info.column >= text.size())
|
||
continue;
|
||
|
||
QTextCursor cursor(document());
|
||
cursor.setPosition(blockStartPos + info.column);
|
||
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||
|
||
QTextEdit::ExtraSelection selection;
|
||
QColor c = info.color;
|
||
c.setAlphaF(qBound(0.0, info.alpha, 1.0));
|
||
selection.format.setBackground(c);
|
||
selection.cursor = cursor;
|
||
selections.append(selection);
|
||
}
|
||
}
|
||
|
||
setExtraSelections(selections);
|
||
}
|
||
|
||
|
||
// =============== 行/字符背景接口实现 ===============
|
||
|
||
void CodeEditor::setLineBackground(int line, const QColor &color, qreal alpha)
|
||
{
|
||
if (line <= 0)
|
||
return;
|
||
|
||
LineBackgroundInfo info;
|
||
info.color = color;
|
||
info.alpha = alpha;
|
||
m_lineBackgrounds.insert(line, info);
|
||
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
void CodeEditor::clearLineBackground(int line)
|
||
{
|
||
m_lineBackgrounds.remove(line);
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
void CodeEditor::clearAllLineBackgrounds()
|
||
{
|
||
m_lineBackgrounds.clear();
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
void CodeEditor::setCharBackground(int line, int column, const QColor &color, qreal alpha)
|
||
{
|
||
if (line <= 0 || column < 0)
|
||
return;
|
||
|
||
CharBackgroundInfo info;
|
||
info.column = column;
|
||
info.color = color;
|
||
info.alpha = alpha;
|
||
|
||
auto &vec = m_charBackgrounds[line];
|
||
vec.append(info);
|
||
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
void CodeEditor::clearCharBackground(int line, int column)
|
||
{
|
||
if (!m_charBackgrounds.contains(line))
|
||
return;
|
||
|
||
auto &vec = m_charBackgrounds[line];
|
||
for (int i = 0; i < vec.size(); ++i) {
|
||
if (vec[i].column == column) {
|
||
vec.removeAt(i);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (vec.isEmpty())
|
||
m_charBackgrounds.remove(line);
|
||
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
void CodeEditor::clearAllCharBackgrounds()
|
||
{
|
||
m_charBackgrounds.clear();
|
||
refreshExtraSelections();
|
||
}
|
||
|
||
// =============== 键盘处理:括号补全 & { } 缩进 ===============
|
||
|
||
void CodeEditor::keyPressEvent(QKeyEvent *event)
|
||
{
|
||
bool completionVisible = (m_completionPopup && m_completionPopup->isVisible());
|
||
|
||
// 如果联想窗口是打开的,先拦截上下键、回车、ESC
|
||
if (completionVisible) {
|
||
if (event->key() == Qt::Key_Down) {
|
||
m_completionPopup->selectNext();
|
||
return;
|
||
} else if (event->key() == Qt::Key_Up) {
|
||
m_completionPopup->selectPrevious();
|
||
return;
|
||
} else if (event->key() == Qt::Key_Return ||
|
||
event->key() == Qt::Key_Enter ||
|
||
event->key() == Qt::Key_Tab) {
|
||
QString text = m_completionPopup->currentText();
|
||
if (!text.isEmpty())
|
||
insertCompletion(text);
|
||
else
|
||
hideCompletion();
|
||
return;
|
||
} else if (event->key() == Qt::Key_Escape) {
|
||
hideCompletion();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 回车:用我们的缩进逻辑,完成后关闭联想
|
||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
|
||
handleReturnKey(event);
|
||
hideCompletion();
|
||
return;
|
||
}
|
||
|
||
// 括号自动补全:(), [], {}
|
||
const QString text = event->text();
|
||
if (text == "(" || text == "[" || text == "{") {
|
||
QChar closing;
|
||
if (text == "(") closing = ')';
|
||
else if (text == "[") closing = ']';
|
||
else closing = '}';
|
||
|
||
QPlainTextEdit::keyPressEvent(event); // 先插入左括号
|
||
|
||
QTextCursor cursor = textCursor();
|
||
cursor.insertText(QString(closing));
|
||
cursor.movePosition(QTextCursor::Left); // 光标移动到中间
|
||
setTextCursor(cursor);
|
||
|
||
// 更新联想
|
||
QString prefix = wordUnderCursor();
|
||
if (!prefix.isEmpty())
|
||
showCompletion(prefix);
|
||
else
|
||
hideCompletion();
|
||
return;
|
||
}
|
||
|
||
// 默认行为:让基类先处理输入
|
||
QPlainTextEdit::keyPressEvent(event);
|
||
|
||
// 根据刚刚输入的字符决定是否弹出联想
|
||
bool isWordChar = !text.isEmpty()
|
||
&& (text[0].isLetterOrNumber() || text[0] == QLatin1Char('_'));
|
||
|
||
if (isWordChar) {
|
||
QString prefix = wordUnderCursor();
|
||
if (!prefix.isEmpty())
|
||
showCompletion(prefix);
|
||
else
|
||
hideCompletion();
|
||
} else {
|
||
// 非单词字符:关闭联想
|
||
hideCompletion();
|
||
}
|
||
}
|
||
|
||
|
||
// 处理回车键:
|
||
// - 普通行:复制当前行缩进
|
||
// - 光标在 '{' 后:
|
||
// {<cursor>}
|
||
// 变为:
|
||
// {
|
||
// <cursor>
|
||
// }
|
||
void CodeEditor::handleReturnKey(QKeyEvent *event)
|
||
{
|
||
QTextCursor cursor = textCursor();
|
||
QTextBlock block = cursor.block();
|
||
QString blockText = block.text();
|
||
int posInBlock = cursor.position() - block.position();
|
||
|
||
// 当前行的基础缩进(行首空白)
|
||
int leadingCount = 0;
|
||
while (leadingCount < blockText.size() &&
|
||
blockText.at(leadingCount).isSpace()) {
|
||
++leadingCount;
|
||
}
|
||
const QString baseIndent = blockText.left(leadingCount);
|
||
|
||
// 特殊情况:光标在 {█} 之间(上一字符是 '{',当前字符是 '}')
|
||
bool beforeRightBrace = (posInBlock < blockText.size() &&
|
||
blockText.at(posInBlock) == QLatin1Char('}'));
|
||
bool afterLeftBraceInSameLine = (posInBlock > 0 &&
|
||
blockText.at(posInBlock - 1) == QLatin1Char('{'));
|
||
|
||
if (beforeRightBrace && afterLeftBraceInSameLine) {
|
||
// 原行内容拆成三行:
|
||
// line1: 原来光标之前的内容(包括 '{')
|
||
// line2: baseIndent + 一个额外缩进(块内缩进)
|
||
// line3: baseIndent + 剩余的 '}' 和后续内容
|
||
QString before = blockText.left(posInBlock); // ...{
|
||
QString after = blockText.mid(posInBlock); // }...
|
||
|
||
QString innerIndent = baseIndent + QStringLiteral(" "); // 块内缩进 4 空格
|
||
|
||
QTextCursor editCursor(block);
|
||
editCursor.beginEditBlock();
|
||
editCursor.select(QTextCursor::BlockUnderCursor);
|
||
editCursor.insertText(before + "\n" +
|
||
innerIndent + "\n" +
|
||
baseIndent + after);
|
||
editCursor.endEditBlock();
|
||
|
||
// 光标放到中间那一行的末尾(空行、缩进之后)
|
||
QTextBlock innerBlock = document()->findBlockByNumber(block.blockNumber() + 1);
|
||
QTextCursor newCursor(innerBlock);
|
||
newCursor.movePosition(QTextCursor::EndOfLine);
|
||
setTextCursor(newCursor);
|
||
return;
|
||
}
|
||
|
||
// ---------------- 其它普通情况:保持安全策略 ----------------
|
||
// 1)先让基类正常插入换行(不会改上一行/已有内容)
|
||
QPlainTextEdit::keyPressEvent(event);
|
||
|
||
// 2)在新行开头插入合适缩进
|
||
cursor = textCursor();
|
||
QTextBlock currentBlock = cursor.block();
|
||
QTextBlock prevBlock = currentBlock.previous();
|
||
|
||
if (!prevBlock.isValid()) {
|
||
// 没有上一行(例如第一行),直接结束
|
||
return;
|
||
}
|
||
|
||
QString prevText = prevBlock.text();
|
||
|
||
int leadingPrev = 0;
|
||
while (leadingPrev < prevText.size() &&
|
||
prevText.at(leadingPrev).isSpace()) {
|
||
++leadingPrev;
|
||
}
|
||
QString prevIndent = prevText.left(leadingPrev);
|
||
|
||
// 判断上一行是否以 '{' 结尾
|
||
int lastNonSpace = prevText.size() - 1;
|
||
while (lastNonSpace >= 0 && prevText.at(lastNonSpace).isSpace()) {
|
||
--lastNonSpace;
|
||
}
|
||
bool prevEndsWithLeftBrace = (lastNonSpace >= 0 &&
|
||
prevText.at(lastNonSpace) == QLatin1Char('{'));
|
||
|
||
QString indent = prevIndent;
|
||
if (prevEndsWithLeftBrace) {
|
||
indent += QStringLiteral(" "); // 多缩进一层
|
||
}
|
||
|
||
cursor.insertText(indent);
|
||
setTextCursor(cursor);
|
||
}
|
||
|
||
|
||
// ==================== CodeEditorWidget 外壳 ====================
|
||
|
||
CodeEditorWidget::CodeEditorWidget(QWidget *parent)
|
||
: QWidget(parent)
|
||
{
|
||
m_editor = new CodeEditor(this);
|
||
m_findBar = new FindReplaceBar(m_editor, this);
|
||
m_findBar->setVisible(false); // 默认隐藏
|
||
|
||
auto *layout = new QVBoxLayout(this);
|
||
layout->setContentsMargins(0, 0, 0, 0);
|
||
layout->addWidget(m_findBar);
|
||
layout->addWidget(m_editor);
|
||
setLayout(layout);
|
||
|
||
// Ctrl+F / Cmd+F -> 打开查找条
|
||
auto *shortcutFind = new QShortcut(QKeySequence::Find, this);
|
||
connect(shortcutFind, &QShortcut::activated,
|
||
this, [this]() {
|
||
if (!m_findBar) return;
|
||
|
||
m_findBar->show();
|
||
m_findBar->raise();
|
||
|
||
// 选中文本自动填入查找框
|
||
QString sel = m_editor->textCursor().selectedText();
|
||
if (!sel.isEmpty())
|
||
m_findBar->setSearchText(sel);
|
||
|
||
m_findBar->focusSearch();
|
||
});
|
||
}
|
||
|
||
|
||
void CodeEditorWidget::loadFromFile(const QString &filePath)
|
||
{
|
||
QFile file(filePath);
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||
return;
|
||
|
||
QTextStream in(&file);
|
||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||
in.setEncoding(QStringConverter::Utf8);
|
||
#endif
|
||
QString text = in.readAll();
|
||
file.close();
|
||
|
||
m_editor->setPlainText(text);
|
||
m_currentFilePath = filePath;
|
||
}
|
||
|
||
bool CodeEditorWidget::save()
|
||
{
|
||
if (m_currentFilePath.isEmpty())
|
||
return false;
|
||
|
||
return saveAs(m_currentFilePath);
|
||
}
|
||
|
||
bool CodeEditorWidget::saveAs(const QString &filePath)
|
||
{
|
||
QFile file(filePath);
|
||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||
return false;
|
||
|
||
QTextStream out(&file);
|
||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||
out.setEncoding(QStringConverter::Utf8);
|
||
#endif
|
||
out << m_editor->toPlainText();
|
||
file.close();
|
||
|
||
m_currentFilePath = filePath;
|
||
return true;
|
||
}
|
||
|
||
QString CodeEditorWidget::currentText() const
|
||
{
|
||
return m_editor->toPlainText();
|
||
}
|
||
|
||
void CodeEditorWidget::clearEditor()
|
||
{
|
||
m_editor->clear();
|
||
m_currentFilePath.clear();
|
||
}
|
||
|
||
// 下面这些接口先直接转发到内部 CodeEditor,后面你可以拿来做可视化用
|
||
|
||
void CodeEditorWidget::setLineBackground(int line, const QColor &color, qreal alpha)
|
||
{
|
||
m_editor->setLineBackground(line, color, alpha);
|
||
}
|
||
|
||
void CodeEditorWidget::clearLineBackground(int line)
|
||
{
|
||
m_editor->clearLineBackground(line);
|
||
}
|
||
|
||
void CodeEditorWidget::setCharBackground(int line, int column, const QColor &color, qreal alpha)
|
||
{
|
||
m_editor->setCharBackground(line, column, color, alpha);
|
||
}
|
||
|
||
void CodeEditorWidget::clearCharBackground(int line, int column)
|
||
{
|
||
m_editor->clearCharBackground(line, column);
|
||
}
|
||
|
||
void CodeEditor::searchNext(const QString &text)
|
||
{
|
||
QString trimmed = text;
|
||
if (trimmed.isEmpty()) {
|
||
// 清除所有查找状态和高亮
|
||
m_searchText.clear();
|
||
m_searchMatches.clear();
|
||
m_currentMatchIndex = -1;
|
||
m_charBackgrounds.clear();
|
||
refreshExtraSelections();
|
||
return;
|
||
}
|
||
|
||
// 搜索内容变化 -> 重新构建所有匹配
|
||
if (m_searchText.compare(trimmed, Qt::CaseInsensitive) != 0) {
|
||
m_searchText = trimmed;
|
||
rebuildSearchMatches();
|
||
m_currentMatchIndex = -1;
|
||
}
|
||
|
||
if (m_searchMatches.isEmpty()) {
|
||
// 没有匹配 -> 清除高亮
|
||
m_charBackgrounds.clear();
|
||
refreshExtraSelections();
|
||
return;
|
||
}
|
||
|
||
// 循环前进到下一处
|
||
m_currentMatchIndex = (m_currentMatchIndex + 1) % m_searchMatches.size();
|
||
|
||
QTextCursor c = m_searchMatches[m_currentMatchIndex];
|
||
setTextCursor(c);
|
||
centerCursor();
|
||
|
||
applySearchHighlights();
|
||
}
|
||
|
||
void CodeEditor::rebuildSearchMatches()
|
||
{
|
||
m_searchMatches.clear();
|
||
|
||
if (m_searchText.isEmpty())
|
||
return;
|
||
|
||
const QString needleLower = m_searchText.toLower();
|
||
const int needleLen = m_searchText.length();
|
||
|
||
QTextBlock block = document()->begin();
|
||
while (block.isValid()) {
|
||
QString text = block.text();
|
||
QString lower = text.toLower();
|
||
|
||
int idx = lower.indexOf(needleLower);
|
||
while (idx >= 0) {
|
||
QTextCursor c(document());
|
||
c.setPosition(block.position() + idx);
|
||
c.setPosition(block.position() + idx + needleLen, QTextCursor::KeepAnchor);
|
||
m_searchMatches.append(c);
|
||
|
||
idx = lower.indexOf(needleLower, idx + needleLen);
|
||
}
|
||
|
||
block = block.next();
|
||
}
|
||
}
|
||
|
||
void CodeEditor::applySearchHighlights()
|
||
{
|
||
// 先清空之前的“字符级背景”(查找高亮会覆盖之前的字符背景)
|
||
m_charBackgrounds.clear();
|
||
|
||
if (m_searchMatches.isEmpty()) {
|
||
refreshExtraSelections();
|
||
return;
|
||
}
|
||
|
||
for (int i = 0; i < m_searchMatches.size(); ++i) {
|
||
const QTextCursor &c = m_searchMatches[i];
|
||
int start = c.selectionStart();
|
||
int end = c.selectionEnd();
|
||
int len = end - start;
|
||
if (len <= 0) continue;
|
||
|
||
// 当前匹配:深黄;其他匹配:浅黄
|
||
QColor color = (i == m_currentMatchIndex)
|
||
? QColor(255, 215, 0, 220) // 深黄色(当前)
|
||
: QColor(255, 255, 150, 160); // 淡黄色(其他)
|
||
qreal alpha = color.alphaF();
|
||
|
||
for (int offset = 0; offset < len; ++offset) {
|
||
int pos = start + offset;
|
||
QTextBlock block = document()->findBlock(pos);
|
||
if (!block.isValid()) continue;
|
||
|
||
int line = block.blockNumber() + 1; // 1-based
|
||
int column = pos - block.position(); // 列号
|
||
|
||
CharBackgroundInfo info;
|
||
info.column = column;
|
||
info.color = color;
|
||
info.alpha = alpha;
|
||
|
||
m_charBackgrounds[line].append(info);
|
||
}
|
||
}
|
||
|
||
// 一次性刷新所有 ExtraSelection
|
||
refreshExtraSelections();
|
||
}
|
||
|