Files
Compiler_GUI/ui/codeeditorwidget.cpp

1306 lines
38 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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