TextEditor: highlight selected text

Change-Id: Ib46908decef51d478f6954a116108c48e4a07ed3
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
David Schulz
2023-10-11 12:39:47 +02:00
parent 5ac19328fd
commit 6f4b9639ce
18 changed files with 215 additions and 4 deletions

View File

@@ -432,6 +432,7 @@ VcsBase_FileUnmerged_TextColor=ffff4040
Bookmarks_TextMarkColor=ff8080ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ff888888
TextEditor_CurrentLine_ScrollBarColor=ffffffff
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -424,6 +424,7 @@ VcsBase_FileUnmerged_TextColor=ffee0000
Bookmarks_TextMarkColor=ffa0a0ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ffcccccc
TextEditor_CurrentLine_ScrollBarColor=ff404040
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -437,6 +437,7 @@ VcsBase_FileUnmerged_TextColor=ffee0000
Bookmarks_TextMarkColor=ffa0a0ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ffcccccc
TextEditor_CurrentLine_ScrollBarColor=ff404040
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -440,6 +440,7 @@ VcsBase_FileUnmerged_TextColor=ffff4040
Bookmarks_TextMarkColor=ff8080ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ff888888
TextEditor_CurrentLine_ScrollBarColor=ffffffff
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -436,6 +436,7 @@ VcsBase_FileUnmerged_TextColor=ffff4040
Bookmarks_TextMarkColor=ff8080ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ff888888
TextEditor_CurrentLine_ScrollBarColor=ffffffff
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -433,6 +433,7 @@ VcsBase_FileUnmerged_TextColor=ffee0000
Bookmarks_TextMarkColor=ffa0a0ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ffcccccc
TextEditor_CurrentLine_ScrollBarColor=ff404040
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -431,6 +431,7 @@ VcsBase_FileUnmerged_TextColor=ffee0000
Bookmarks_TextMarkColor=ffa0a0ff
TextEditor_SearchResult_ScrollBarColor=ff00c000
TextEditor_Selection_ScrollBarColor=ffcccccc
TextEditor_CurrentLine_ScrollBarColor=ff404040
ProjectExplorer_TaskError_TextMarkColor=error

View File

@@ -15,6 +15,17 @@ bool Position::operator==(const Position &other) const
return line == other.line && column == other.column;
}
int Position::positionInDocument(QTextDocument *doc) const
{
if (!isValid())
return -1;
QTC_ASSERT(doc, return -1);
QTextBlock block = doc->findBlockByNumber(line - 1);
if (!block.isValid())
return -1;
return block.position() + column;
}
/*!
Returns the text position of a \a fileName and sets the \a postfixPos if
it can find a positional postfix.
@@ -107,6 +118,14 @@ bool Range::operator==(const Range &other) const
return begin == other.begin && end == other.end;
}
QTextCursor Range::toTextCursor(QTextDocument *doc) const
{
QTextCursor cursor(doc);
cursor.setPosition(begin.positionInDocument(doc));
cursor.setPosition(end.positionInDocument(doc), QTextCursor::KeepAnchor);
return cursor;
}
bool convertPosition(const QTextDocument *document, int pos, int *line, int *column)
{
QTextBlock block = document->findBlock(pos);

View File

@@ -30,6 +30,8 @@ public:
bool isValid() const { return line > 0 && column >= 0; }
int positionInDocument(QTextDocument *doc) const;
static Position fromFileName(QStringView fileName, int &postfixPos);
static Position fromPositionInDocument(const QTextDocument *document, int pos);
static Position fromCursor(const QTextCursor &cursor);
@@ -49,6 +51,8 @@ public:
bool operator==(const Range &other) const;
bool operator!=(const Range &other) const { return !(operator==(other)); }
QTextCursor toTextCursor(QTextDocument *doc) const;
};
// line is 1-based, column is 0-based

View File

@@ -259,6 +259,7 @@ public:
/* TextEditor Plugin */
TextEditor_SearchResult_ScrollBarColor,
TextEditor_Selection_ScrollBarColor,
TextEditor_CurrentLine_ScrollBarColor,
/* Debugger Plugin */

View File

@@ -4,6 +4,7 @@
#include "highlightscrollbarcontroller.h"
#include <QAbstractScrollArea>
#include <QLoggingCategory>
#include <QPainter>
#include <QResizeEvent>
#include <QScrollBar>
@@ -12,6 +13,8 @@
using namespace Utils;
static Q_LOGGING_CATEGORY(LOG, "qtc.utils.highlightscrollbar", QtWarningMsg)
namespace Core {
/*!
@@ -403,6 +406,7 @@ void HighlightScrollBarController::addHighlight(Highlight highlight)
if (!m_overlay)
return;
qCDebug(LOG) << "addHighlight" << highlight.category.toString() << highlight.position;
m_highlights[highlight.category] << highlight;
m_overlay->scheduleUpdate();
}
@@ -412,6 +416,7 @@ void HighlightScrollBarController::removeHighlights(Id category)
if (!m_overlay)
return;
qCDebug(LOG) << "removeHighlights" << category.toString();
m_highlights.remove(category);
m_overlay->scheduleUpdate();
}
@@ -421,6 +426,7 @@ void HighlightScrollBarController::removeAllHighlights()
if (!m_overlay)
return;
qCDebug(LOG) << "removeAllHighlights";
m_highlights.clear();
m_overlay->scheduleUpdate();
}

View File

@@ -37,6 +37,7 @@ const char animateWithinFileTimeMaxKey[] = "AnimateWithinFileTimeMax";
const char displayAnnotationsKey[] = "DisplayAnnotations";
const char annotationAlignmentKey[] = "AnnotationAlignment";
const char minimalAnnotationContentKey[] = "MinimalAnnotationContent";
const char highlightSelectionKey[] = "HighlightSelection";
const char groupPostfix[] = "textDisplaySettings";
void DisplaySettings::toSettings(QtcSettings *s) const
@@ -61,6 +62,7 @@ void DisplaySettings::toSettings(QtcSettings *s) const
s->setValue(animateNavigationWithinFileKey, m_animateNavigationWithinFile);
s->setValue(displayAnnotationsKey, m_displayAnnotations);
s->setValue(annotationAlignmentKey, static_cast<int>(m_annotationAlignment));
s->setValue(highlightSelectionKey, m_highlightSelection);
s->endGroup();
}
@@ -92,6 +94,7 @@ void DisplaySettings::fromSettings(QtcSettings *s)
s->value(annotationAlignmentKey,
static_cast<int>(m_annotationAlignment)).toInt());
m_minimalAnnotationContent = s->value(minimalAnnotationContentKey, m_minimalAnnotationContent).toInt();
m_highlightSelection = s->value(highlightSelectionKey, m_highlightSelection).toBool();
s->endGroup();
}
@@ -119,6 +122,7 @@ bool DisplaySettings::equals(const DisplaySettings &ds) const
&& m_displayAnnotations == ds.m_displayAnnotations
&& m_annotationAlignment == ds.m_annotationAlignment
&& m_minimalAnnotationContent == ds.m_minimalAnnotationContent
&& m_highlightSelection == ds.m_highlightSelection
;
}

View File

@@ -52,6 +52,7 @@ public:
bool m_displayFileLineEnding = true;
bool m_scrollBarHighlights = true;
bool m_animateNavigationWithinFile = false;
bool m_highlightSelection = true;
int m_animateWithinFileTimeMax = 333; // read only setting
bool m_displayAnnotations = true;
AnnotationAlignment m_annotationAlignment = AnnotationAlignment::RightSide;

View File

@@ -98,6 +98,10 @@ public:
visualizeWhitespace = new QCheckBox(Tr::tr("&Visualize whitespace"));
visualizeWhitespace->setToolTip(Tr::tr("Shows tabs and spaces."));
highlightSelection = new QCheckBox(Tr::tr("&Highlight Selection"));
highlightSelection->setToolTip(Tr::tr("Adds a colored background and a marker to the "
"scrollbar to occurrences of the selected text."));
leftAligned = new QRadioButton(Tr::tr("Next to editor content"));
atMargin = new QRadioButton(Tr::tr("Next to right margin"));
rightAligned = new QRadioButton(Tr::tr("Aligned at right side"));
@@ -143,6 +147,7 @@ public:
autoFoldFirstComment,
scrollBarHighlights,
animateNavigationWithinFile,
highlightSelection,
},
Column {
highlightCurrentLine,
@@ -195,6 +200,7 @@ public:
QCheckBox *openLinksInNextSplit;
QCheckBox *highlightMatchingParentheses;
QCheckBox *visualizeWhitespace;
QCheckBox *highlightSelection;
QGroupBox *displayAnnotations;
QRadioButton *leftAligned;
QRadioButton *atMargin;
@@ -238,6 +244,7 @@ void DisplaySettingsWidget::settingsFromUI(DisplaySettings &displaySettings,
displaySettings.m_scrollBarHighlights = scrollBarHighlights->isChecked();
displaySettings.m_animateNavigationWithinFile = animateNavigationWithinFile->isChecked();
displaySettings.m_displayAnnotations = displayAnnotations->isChecked();
displaySettings.m_highlightSelection = highlightSelection->isChecked();
if (leftAligned->isChecked())
displaySettings.m_annotationAlignment = AnnotationAlignment::NextToContent;
else if (atMargin->isChecked())
@@ -276,6 +283,7 @@ void DisplaySettingsWidget::settingsToUI()
scrollBarHighlights->setChecked(displaySettings.m_scrollBarHighlights);
animateNavigationWithinFile->setChecked(displaySettings.m_animateNavigationWithinFile);
displayAnnotations->setChecked(displaySettings.m_displayAnnotations);
highlightSelection->setChecked(displaySettings.m_highlightSelection);
switch (displaySettings.m_annotationAlignment) {
case AnnotationAlignment::NextToContent: leftAligned->setChecked(true); break;
case AnnotationAlignment::NextToMargin: atMargin->setChecked(true); break;

View File

@@ -79,6 +79,13 @@ public:
IAssistProvider *m_quickFixProvider = nullptr;
QScopedPointer<Indenter> m_indenter;
QScopedPointer<Formatter> m_formatter;
struct PlainTextCache
{
int revision = -1;
QString plainText;
};
PlainTextCache m_plainTextCache;
int m_autoSaveRevision = -1;
bool m_silentReload = false;
@@ -305,7 +312,11 @@ QString TextDocument::convertToPlainText(const QString &rawText)
QString TextDocument::plainText() const
{
return convertToPlainText(d->m_document.toRawText());
if (d->m_plainTextCache.revision != d->m_document.revision()) {
d->m_plainTextCache.plainText = convertToPlainText(d->m_document.toRawText());
d->m_plainTextCache.revision = d->m_document.revision();
}
return d->m_plainTextCache.plainText;
}
QString TextDocument::textAt(int pos, int length) const

View File

@@ -605,6 +605,7 @@ public:
void paintRightMarginLine(const PaintEventData &data, QPainter &painter) const;
void paintBlockHighlight(const PaintEventData &data, QPainter &painter) const;
void paintSearchResultOverlay(const PaintEventData &data, QPainter &painter) const;
void paintSelectionOverlay(const PaintEventData &data, QPainter &painter) const;
void paintIfDefedOutBlocks(const PaintEventData &data, QPainter &painter) const;
void paintFindScope(const PaintEventData &data, QPainter &painter) const;
void paintCurrentLineHighlight(const PaintEventData &data, QPainter &painter) const;
@@ -690,6 +691,7 @@ public:
int length;
};
void addSearchResultsToScrollBar(const QVector<SearchResult> &results);
void addSelectionHighlightToScrollBar(const QVector<SearchResult> &selections);
void adjustScrollBarRanges();
void setFindScope(const MultiTextCursor &scope);
@@ -766,6 +768,7 @@ public:
TextEditorOverlay *m_overlay = nullptr;
SnippetOverlay *m_snippetOverlay = nullptr;
TextEditorOverlay *m_searchResultOverlay = nullptr;
TextEditorOverlay *m_selectionHighlightOverlay = nullptr;
bool snippetCheckCursor(const QTextCursor &cursor);
void snippetTabOrBacktab(bool forward);
@@ -811,6 +814,7 @@ public:
QString m_findText;
FindFlags m_findFlags;
void highlightSearchResults(const QTextBlock &block, const PaintEventData &data) const;
void highlightSelection(const QTextBlock &block) const;
QTimer m_delayedUpdateTimer;
void setExtraSelections(Utils::Id kind, const QList<QTextEdit::ExtraSelection> &selections);
@@ -871,7 +875,9 @@ public:
CommentDefinition m_commentDefinition;
QFuture<SearchResultItems> m_searchFuture;
QFuture<SearchResultItems> m_selectionHighlightFuture;
QVector<SearchResult> m_searchResults;
QVector<SearchResult> m_selectionResults;
QTimer m_scrollBarUpdateTimer;
HighlightScrollBarController *m_highlightScrollBarController = nullptr;
bool m_scrollBarUpdateScheduled = false;
@@ -995,6 +1001,7 @@ TextEditorWidgetPrivate::TextEditorWidgetPrivate(TextEditorWidget *parent)
, m_overlay(new TextEditorOverlay(q))
, m_snippetOverlay(new SnippetOverlay(q))
, m_searchResultOverlay(new TextEditorOverlay(q))
, m_selectionHighlightOverlay(new TextEditorOverlay(q))
, m_refactorOverlay(new RefactorOverlay(q))
, m_marksVisible(false)
, m_codeFoldingVisible(false)
@@ -1010,6 +1017,7 @@ TextEditorWidgetPrivate::TextEditorWidgetPrivate(TextEditorWidget *parent)
, m_clipboardAssistProvider(new ClipboardAssistProvider)
, m_autoCompleter(new AutoCompleter)
{
m_selectionHighlightOverlay->show();
auto aggregate = new Aggregation::Aggregate;
m_find = new TextEditorWidgetFind(q);
connect(m_find, &BaseTextFind::highlightAllRequested,
@@ -1139,6 +1147,8 @@ TextEditorWidgetPrivate::~TextEditorWidgetPrivate()
delete m_highlightScrollBarController;
if (m_searchFuture.isRunning())
m_searchFuture.cancel();
if (m_selectionHighlightFuture.isRunning())
m_selectionHighlightFuture.cancel();
}
static QFrame *createSeparator(const QString &styleSheet)
@@ -3346,10 +3356,12 @@ void TextEditorWidgetPrivate::documentAboutToBeReloaded()
m_overlay->clear();
m_snippetOverlay->clear();
m_searchResultOverlay->clear();
m_selectionHighlightOverlay->clear();
m_refactorOverlay->clear();
// clear search results
m_searchResults.clear();
m_selectionResults.clear();
}
void TextEditorWidgetPrivate::documentReloadFinished(bool success)
@@ -4070,6 +4082,35 @@ void TextEditorWidgetPrivate::highlightSearchResults(const QTextBlock &block, co
}
}
void TextEditorWidgetPrivate::highlightSelection(const QTextBlock &block) const
{
if (!m_displaySettings.m_highlightSelection || m_cursors.hasMultipleCursors())
return;
const QString selection = m_cursors.selectedText();
if (selection.trimmed().isEmpty())
return;
const int blockPosition = block.position();
QString text = block.text();
text.replace(QChar::Nbsp, QLatin1Char(' '));
const int l = selection.length();
for (int idx = text.indexOf(selection, 0, Qt::CaseInsensitive);
idx >= 0;
idx = text.indexOf(selection, idx + 1, Qt::CaseInsensitive)) {
const int start = blockPosition + idx;
const int end = start + l;
if (!Utils::contains(m_selectionHighlightOverlay->selections(),
[&](const OverlaySelection &selection) {
return selection.m_cursor_begin.position() == start
&& selection.m_cursor_end.position() == end;
})) {
m_selectionHighlightOverlay->addOverlaySelection(start, end, {}, {});
}
}
}
void TextEditorWidgetPrivate::startCursorFlashTimer()
{
const int flashTime = QApplication::cursorFlashTime();
@@ -4108,6 +4149,44 @@ void TextEditorWidgetPrivate::updateCursorSelections()
selections << QTextEdit::ExtraSelection{cursor, selectionFormat};
}
q->setExtraSelections(TextEditorWidget::CursorSelection, selections);
m_selectionHighlightOverlay->clear();
if (m_selectionHighlightFuture.isRunning())
m_selectionHighlightFuture.cancel();
m_selectionResults.clear();
if (!m_highlightScrollBarController)
return;
m_highlightScrollBarController->removeHighlights(Constants::SCROLL_BAR_SELECTION);
if (!m_displaySettings.m_highlightSelection || m_cursors.hasMultipleCursors())
return;
const QString txt = m_cursors.selectedText();
if (txt.trimmed().isEmpty())
return;
m_selectionHighlightFuture = Utils::asyncRun(Utils::searchInContents,
txt,
FindFlags{},
m_document->filePath(),
m_document->plainText());
Utils::onResultReady(m_selectionHighlightFuture,
this,
[this](const SearchResultItems &resultList) {
QList<SearchResult> results;
for (const SearchResultItem &item : resultList) {
int start = item.mainRange().begin.positionInDocument(
m_document->document());
int end = item.mainRange().end.positionInDocument(
m_document->document());
results << SearchResult{start, end - start};
}
m_selectionResults = results;
addSelectionHighlightToScrollBar(results);
});
}
void TextEditorWidgetPrivate::moveCursor(QTextCursor::MoveOperation operation,
@@ -4489,6 +4568,40 @@ void TextEditorWidgetPrivate::paintSearchResultOverlay(const PaintEventData &dat
data.eventRect);
}
void TextEditorWidgetPrivate::paintSelectionOverlay(const PaintEventData &data,
QPainter &painter) const
{
if (m_cursors.hasMultipleCursors())
return;
const QString expr = m_cursors.selectedText();
if (expr.isEmpty())
return;
const int margin = 5;
QTextBlock block = data.block;
QPointF offset = data.offset;
while (block.isValid()) {
QRectF blockBoundingRect = q->blockBoundingRect(block).translated(offset);
if (blockBoundingRect.bottom() >= data.eventRect.top() - margin
&& blockBoundingRect.top() <= data.eventRect.bottom() + margin) {
highlightSelection(block);
}
offset.ry() += blockBoundingRect.height();
if (offset.y() > data.viewportRect.height() + margin)
break;
block = TextEditor::nextVisibleBlock(block, data.doc);
}
QColor selection = m_document->fontSettings().toTextCharFormat(C_SELECTION).background().color();
const QColor text = m_document->fontSettings().toTextCharFormat(C_TEXT).background().color();
selection.setAlphaF(StyleHelper::luminance(text) > 0.5 ? 0.25 : 0.5);
m_selectionHighlightOverlay->fill(&painter, selection, data.eventRect);
}
void TextEditorWidgetPrivate::paintIfDefedOutBlocks(const PaintEventData &data,
QPainter &painter) const
{
@@ -5076,6 +5189,8 @@ void TextEditorWidget::paintEvent(QPaintEvent *e)
d->paintFindScope(data, painter);
// paint search results on top of the find scope
d->paintSearchResultOverlay(data, painter);
// paint selection highlights
d->paintSelectionOverlay(data, painter);
}
while (data.block.isValid()) {
@@ -6947,6 +7062,33 @@ void TextEditorWidgetPrivate::addSearchResultsToScrollBar(const QVector<SearchRe
}
}
void TextEditorWidgetPrivate::addSelectionHighlightToScrollBar(
const QVector<SearchResult> &selections)
{
if (!m_highlightScrollBarController)
return;
for (const SearchResult &result : selections) {
const QTextBlock &block = q->document()->findBlock(result.start);
if (block.isValid() && block.isVisible()) {
if (q->lineWrapMode() == QPlainTextEdit::WidgetWidth) {
const int firstLine = block.layout()->lineForTextPosition(result.start - block.position()).lineNumber();
const int lastLine = block.layout()->lineForTextPosition(result.start - block.position() + result.length).lineNumber();
for (int line = firstLine; line <= lastLine; ++line) {
m_highlightScrollBarController->addHighlight(
{Constants::SCROLL_BAR_SELECTION, block.firstLineNumber() + line,
Theme::TextEditor_Selection_ScrollBarColor, Highlight::NormalPriority});
}
} else {
m_highlightScrollBarController->addHighlight(
{Constants::SCROLL_BAR_SELECTION,
block.blockNumber(),
Theme::TextEditor_Selection_ScrollBarColor,
Highlight::NormalPriority});
}
}
}
}
Highlight markToHighlight(TextMark *mark, int lineNumber)
{
return Highlight(mark->category().id,
@@ -6968,6 +7110,9 @@ void TextEditorWidgetPrivate::updateHighlightScrollBarNow()
// update search results
addSearchResultsToScrollBar(m_searchResults);
// update search selection
addSelectionHighlightToScrollBar(m_selectionResults);
// update text marks
const TextMarks marks = m_document->marks();
for (TextMark *mark : marks) {
@@ -7900,6 +8045,7 @@ void TextEditorWidget::setDisplaySettings(const DisplaySettings &ds)
d->updateFileLineEndingVisible();
d->updateHighlights();
d->setupScrollBar();
d->updateCursorSelections();
viewport()->update();
extraArea()->update();
}

View File

@@ -216,6 +216,7 @@ const char JUMP_TO_FILE_UNDER_CURSOR[] = "TextEditor.JumpToFileUnderCursor";
const char JUMP_TO_FILE_UNDER_CURSOR_IN_NEXT_SPLIT[] = "TextEditor.JumpToFileUnderCursorInNextSplit";
const char SCROLL_BAR_SEARCH_RESULT[] = "TextEditor.ScrollBarSearchResult";
const char SCROLL_BAR_SELECTION[] = "TextEditor.ScrollBarSelection";
const char SCROLL_BAR_CURRENT_LINE[] = "TextEditor.ScrollBarCurrentLine";
const TEXTEDITOR_EXPORT char *nameForStyle(TextStyle style);

View File

@@ -82,10 +82,13 @@ void TextEditorOverlay::addOverlaySelection(int begin, int end,
if (m_selections.isEmpty())
m_firstSelectionOriginalBegin = begin;
else if (begin < m_firstSelectionOriginalBegin)
qWarning() << "overlay selections not in order";
m_selections.append(selection);
const auto it = std::find_if(m_selections.cbegin(),
m_selections.cend(),
[&](const OverlaySelection &selection) {
return begin > selection.m_cursor_begin.position();
});
m_selections.insert(it, selection);
update();
}