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