#include "codeeditorwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ==================== 辅助类:行号区域 ==================== 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 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("%1%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(blockBoundingGeometry(block) .translated(contentOffset()).top()); int bottom = top + static_cast(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(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(blockBoundingGeometry(block) .translated(contentOffset()).top()); int bottom = top + static_cast(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(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 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 &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(); } } // 处理回车键: // - 普通行:复制当前行缩进 // - 光标在 '{' 后: // {} // 变为: // { // // } 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(); }