diff --git a/src/libs/utils/textutils.cpp b/src/libs/utils/textutils.cpp index 840325dd975..9b653bafa69 100644 --- a/src/libs/utils/textutils.cpp +++ b/src/libs/utils/textutils.cpp @@ -39,8 +39,9 @@ bool convertPosition(const QTextDocument *document, int pos, int *line, int *col (*column) = -1; return false; } else { + // line and column are both 1-based (*line) = block.blockNumber() + 1; - (*column) = pos - block.position(); + (*column) = pos - block.position() + 1; return true; } } diff --git a/src/plugins/clangcodemodel/clangassistproposalitem.cpp b/src/plugins/clangcodemodel/clangassistproposalitem.cpp index 28e8401d8ff..6b4d0b81629 100644 --- a/src/plugins/clangcodemodel/clangassistproposalitem.cpp +++ b/src/plugins/clangcodemodel/clangassistproposalitem.cpp @@ -27,6 +27,7 @@ #include "clangcompletionchunkstotextconverter.h" #include "clangfixitoperation.h" +#include "clangutils.h" #include #include @@ -34,10 +35,13 @@ #include #include +#include #include #include +#include #include +#include #include #include @@ -45,6 +49,7 @@ using namespace CPlusPlus; using namespace ClangBackEnd; +using namespace TextEditor; namespace ClangCodeModel { namespace Internal { @@ -73,7 +78,7 @@ bool ClangAssistProposalItem::implicitlyApplies() const return true; } -static QString textUntilPreviousStatement(TextEditor::TextDocumentManipulatorInterface &manipulator, +static QString textUntilPreviousStatement(TextDocumentManipulatorInterface &manipulator, int startPosition) { static const QString stopCharacters(";{}#"); @@ -90,7 +95,7 @@ static QString textUntilPreviousStatement(TextEditor::TextDocumentManipulatorInt } // 7.3.3: using typename(opt) nested-name-specifier unqualified-id ; -static bool isAtUsingDeclaration(TextEditor::TextDocumentManipulatorInterface &manipulator, +static bool isAtUsingDeclaration(TextDocumentManipulatorInterface &manipulator, int basePosition) { SimpleLexer lexer; @@ -105,7 +110,7 @@ static bool isAtUsingDeclaration(TextEditor::TextDocumentManipulatorInterface &m if (lastToken.kind() != T_COLON_COLON) return false; - return Utils::contains(tokens, [](const Token &token) { + return ::Utils::contains(tokens, [](const Token &token) { return token.kind() == T_USING; }); } @@ -135,17 +140,17 @@ static QString methodDefinitionParameters(const CodeCompletionChunks &chunks) return result; } -void ClangAssistProposalItem::apply(TextEditor::TextDocumentManipulatorInterface &manipulator, +void ClangAssistProposalItem::apply(TextDocumentManipulatorInterface &manipulator, int basePosition) const { const CodeCompletion ccr = firstCodeCompletion(); if (!ccr.requiredFixIts.empty()) { + // Important: Calculate shift before changing the document. + basePosition += fixItsShift(manipulator); + ClangFixItOperation fixItOperation(Utf8String(), ccr.requiredFixIts); fixItOperation.perform(); - - const int shift = fixItsShift(manipulator); - basePosition += shift; } QString textToBeInserted = m_text; @@ -175,8 +180,8 @@ void ClangAssistProposalItem::apply(TextEditor::TextDocumentManipulatorInterface textToBeInserted = converter.text(); } else if (!ccr.text.isEmpty()) { - const TextEditor::CompletionSettings &completionSettings = - TextEditor::TextEditorSettings::instance()->completionSettings(); + const CompletionSettings &completionSettings = + TextEditorSettings::instance()->completionSettings(); const bool autoInsertBrackets = completionSettings.m_autoInsertBrackets; if (autoInsertBrackets && @@ -193,9 +198,9 @@ void ClangAssistProposalItem::apply(TextEditor::TextDocumentManipulatorInterface QTextCursor cursor = manipulator.textCursorAt(basePosition); bool abandonParen = false; - if (Utils::Text::matchPreviousWord(manipulator, cursor, "&")) { - Utils::Text::moveToPrevChar(manipulator, cursor); - Utils::Text::moveToPrevChar(manipulator, cursor); + if (::Utils::Text::matchPreviousWord(manipulator, cursor, "&")) { + ::Utils::Text::moveToPrevChar(manipulator, cursor); + ::Utils::Text::moveToPrevChar(manipulator, cursor); const QChar prevChar = manipulator.characterAt(cursor.position()); cursor.setPosition(basePosition); abandonParen = QString("(;,{}").contains(prevChar); @@ -206,7 +211,7 @@ void ClangAssistProposalItem::apply(TextEditor::TextDocumentManipulatorInterface if (!abandonParen && ccr.completionKind == CodeCompletion::FunctionDefinitionCompletionKind) { const CodeCompletionChunk resultType = ccr.chunks.first(); QTC_ASSERT(resultType.kind == CodeCompletionChunk::ResultType, return;); - if (Utils::Text::matchPreviousWord(manipulator, cursor, resultType.text.toString())) { + if (::Utils::Text::matchPreviousWord(manipulator, cursor, resultType.text.toString())) { extraCharacters += methodDefinitionParameters(ccr.chunks); // To skip the next block. abandonParen = true; @@ -306,7 +311,7 @@ void ClangAssistProposalItem::setText(const QString &text) QString ClangAssistProposalItem::text() const { - return m_text + (requiresFixIts() ? fixItText() : QString()); + return m_text; } const QVector &ClangAssistProposalItem::firstCompletionFixIts() const @@ -314,39 +319,53 @@ const QVector &ClangAssistProposalItem::firstCompl return firstCodeCompletion().requiredFixIts; } -// FIXME: Indicate required fix-it without adding extra text. +std::pair fixItPositionsRange(const FixItContainer &fixIt, const QTextCursor &cursor) +{ + const QTextBlock startLine = cursor.document()->findBlockByNumber(fixIt.range.start.line - 1); + const QTextBlock endLine = cursor.document()->findBlockByNumber(fixIt.range.end.line - 1); + + const int fixItStartPos = ::Utils::Text::positionInText( + cursor.document(), + static_cast(fixIt.range.start.line), + Utils::cppEditorColumn(startLine, static_cast(fixIt.range.start.column))); + const int fixItEndPos = ::Utils::Text::positionInText( + cursor.document(), + static_cast(fixIt.range.end.line), + Utils::cppEditorColumn(endLine, static_cast(fixIt.range.end.column))); + return std::make_pair(fixItStartPos, fixItEndPos); +} + +static QString textReplacedByFixit(const FixItContainer &fixIt) +{ + TextEditorWidget *textEditorWidget = TextEditorWidget::currentTextEditorWidget(); + if (!textEditorWidget) + return QString(); + const std::pair fixItPosRange = fixItPositionsRange(fixIt, + textEditorWidget->textCursor()); + return textEditorWidget->textAt(fixItPosRange.first, + fixItPosRange.second - fixItPosRange.first); +} + QString ClangAssistProposalItem::fixItText() const { const FixItContainer &fixIt = firstCompletionFixIts().first(); - const SourceRangeContainer &range = fixIt.range; return QCoreApplication::translate("ClangCodeModel::ClangAssistProposalItem", - " (requires to correct [%1:%2-%3:%4] to \"%5\")") - .arg(range.start.line) - .arg(range.start.column) - .arg(range.end.line) - .arg(range.end.column) + "Requires to correct \"%1\" to \"%2\"") + .arg(textReplacedByFixit(fixIt)) .arg(fixIt.text.toString()); } -int ClangAssistProposalItem::fixItsShift( - const TextEditor::TextDocumentManipulatorInterface &manipulator) const +int ClangAssistProposalItem::fixItsShift(const TextDocumentManipulatorInterface &manipulator) const { const QVector &requiredFixIts = firstCompletionFixIts(); if (requiredFixIts.empty()) return 0; int shift = 0; - QTextCursor cursor = manipulator.textCursorAt(0); + const QTextCursor cursor = manipulator.textCursorAt(0); for (const FixItContainer &fixIt : requiredFixIts) { - const int fixItStartPos = Utils::Text::positionInText( - cursor.document(), - static_cast(fixIt.range.start.line), - static_cast(fixIt.range.start.column)); - const int fixItEndPos = Utils::Text::positionInText( - cursor.document(), - static_cast(fixIt.range.end.line), - static_cast(fixIt.range.end.column)); - shift += fixIt.text.toString().length() - (fixItEndPos - fixItStartPos); + const std::pair fixItPosRange = fixItPositionsRange(fixIt, cursor); + shift += fixIt.text.toString().length() - (fixItPosRange.second - fixItPosRange.first); } return shift; } @@ -425,10 +444,14 @@ QString ClangAssistProposalItem::detail() const detail += "
"; detail += CompletionChunksToTextConverter::convertToToolTipWithHtml( codeCompletion.chunks, codeCompletion.completionKind); + if (!codeCompletion.briefComment.isEmpty()) detail += "
" + codeCompletion.briefComment.toString(); } + if (requiresFixIts()) + detail += "

" + fixItText() + ""; + return detail; } diff --git a/src/plugins/clangcodemodel/clangutils.cpp b/src/plugins/clangcodemodel/clangutils.cpp index 2d74d7f4ef1..971c0551749 100644 --- a/src/plugins/clangcodemodel/clangutils.cpp +++ b/src/plugins/clangcodemodel/clangutils.cpp @@ -155,8 +155,18 @@ int clangColumn(const QTextBlock &line, int cppEditorColumn) // (2) The return value is the column in Clang which is the utf8 byte offset from the beginning // of the line. // Here we convert column from (1) to (2). - // '+ 1' is for 1-based columns - return line.text().left(cppEditorColumn).toUtf8().size() + 1; + // '- 1' and '+ 1' are because of 1-based columns + return line.text().left(cppEditorColumn - 1).toUtf8().size() + 1; +} + +int cppEditorColumn(const QTextBlock &line, int clangColumn) +{ + // (1) clangColumn is the column in Clang which is the utf8 byte offset from the beginning + // of the line. + // (2) The return value is the actual column shown by CppEditor. + // Here we convert column from (1) to (2). + // '- 1' and '+ 1' are because of 1-based columns + return QString::fromUtf8(line.text().toUtf8().left(clangColumn - 1)).size() + 1; } ::Utils::CodeModelIcon::Type iconTypeForToken(const ClangBackEnd::TokenInfoContainer &token) diff --git a/src/plugins/clangcodemodel/clangutils.h b/src/plugins/clangcodemodel/clangutils.h index 883dfcdf29e..956f1dee4aa 100644 --- a/src/plugins/clangcodemodel/clangutils.h +++ b/src/plugins/clangcodemodel/clangutils.h @@ -57,7 +57,8 @@ CppTools::ProjectPart::Ptr projectPartForFile(const QString &filePath); CppTools::ProjectPart::Ptr projectPartForFileBasedOnProcessor(const QString &filePath); bool isProjectPartLoaded(const CppTools::ProjectPart::Ptr projectPart); QString projectPartIdForFile(const QString &filePath); -int clangColumn(const QTextBlock &lineText, int cppEditorColumn); +int clangColumn(const QTextBlock &line, int cppEditorColumn); +int cppEditorColumn(const QTextBlock &line, int clangColumn); QString diagnosticCategoryPrefixRemoved(const QString &text); diff --git a/src/plugins/texteditor/codeassist/genericproposalwidget.cpp b/src/plugins/texteditor/codeassist/genericproposalwidget.cpp index 8f5d7291482..079b7b2db1d 100644 --- a/src/plugins/texteditor/codeassist/genericproposalwidget.cpp +++ b/src/plugins/texteditor/codeassist/genericproposalwidget.cpp @@ -31,9 +31,11 @@ #include #include #include +#include #include #include +#include #include #include @@ -49,6 +51,7 @@ #include #include #include +#include using namespace Utils; @@ -92,6 +95,8 @@ QVariant ModelAdapter::data(const QModelIndex &index, int role) const return m_completionModel->icon(index.row()); else if (role == Qt::WhatsThisRole) return m_completionModel->detail(index.row()); + else if (role == Qt::UserRole) + return m_completionModel->proposalItem(index.row())->requiresFixIts(); return QVariant(); } @@ -146,6 +151,7 @@ private: // ----------------------- class GenericProposalListView : public QListView { + friend class ProposalItemDelegate; public: GenericProposalListView(QWidget *parent); @@ -160,10 +166,53 @@ public: void selectLastRow() { selectRow(model()->rowCount() - 1); } }; +class ProposalItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit ProposalItemDelegate(GenericProposalListView *parent = nullptr) + : QStyledItemDelegate(parent) + , m_parent(parent) + { + } + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + static const QIcon fixItIcon = ::Utils::Icons::CODEMODEL_FIXIT.icon(); + + QStyledItemDelegate::paint(painter, option, index); + + if (m_parent->model()->data(index, Qt::UserRole).toBool()) { + const QRect itemRect = m_parent->rectForIndex(index); + const QScrollBar *verticalScrollBar = m_parent->verticalScrollBar(); + + const int x = m_parent->width() - itemRect.height() - (verticalScrollBar->isVisible() + ? verticalScrollBar->width() + : 0); + const int iconSize = itemRect.height() - 5; + fixItIcon.paint(painter, QRect(x, itemRect.y() - m_parent->verticalOffset(), + iconSize, iconSize)); + } + } + + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override + { + QSize size(QStyledItemDelegate::sizeHint(option, index)); + if (m_parent->model()->data(index, Qt::UserRole).toBool()) + size.setWidth(size.width() + m_parent->rectForIndex(index).height() - 5); + return size; + } +private: + GenericProposalListView *m_parent; +}; + GenericProposalListView::GenericProposalListView(QWidget *parent) : QListView(parent) { setVerticalScrollMode(QAbstractItemView::ScrollPerItem); + setItemDelegate(new ProposalItemDelegate(this)); } QSize GenericProposalListView::calculateSize() const