Files
Compiler_GUI/ui/codeeditorwidget.cpp

1306 lines
38 KiB
C++
Raw Normal View History

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