/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "unifieddiffeditorwidget.h" #include "diffeditorconstants.h" #include "diffeditordocument.h" #include "diffutils.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace TextEditor; namespace DiffEditor { namespace Internal { UnifiedDiffEditorWidget::UnifiedDiffEditorWidget(QWidget *parent) : SelectableTextEditorWidget("DiffEditor.UnifiedDiffEditor", parent) , m_controller(this) { DisplaySettings settings = displaySettings(); settings.m_textWrapping = false; settings.m_displayLineNumbers = true; settings.m_markTextChanges = false; settings.m_highlightBlocks = false; SelectableTextEditorWidget::setDisplaySettings(settings); setReadOnly(true); connect(TextEditorSettings::instance(), &TextEditorSettings::displaySettingsChanged, this, &UnifiedDiffEditorWidget::setDisplaySettings); setDisplaySettings(TextEditorSettings::displaySettings()); setCodeStyle(TextEditorSettings::codeStyle()); connect(TextEditorSettings::instance(), &TextEditorSettings::fontSettingsChanged, this, &UnifiedDiffEditorWidget::setFontSettings); setFontSettings(TextEditorSettings::fontSettings()); clear(tr("No document")); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &UnifiedDiffEditorWidget::slotCursorPositionChangedInEditor); auto context = new Core::IContext(this); context->setWidget(this); context->setContext(Core::Context(Constants::UNIFIED_VIEW_ID)); Core::ICore::addContextObject(context); setCodeFoldingSupported(true); } void UnifiedDiffEditorWidget::setDocument(DiffEditorDocument *document) { m_controller.setDocument(document); clear(); QList diffFileList; if (document) diffFileList = document->diffFiles(); setDiff(diffFileList); } DiffEditorDocument *UnifiedDiffEditorWidget::diffDocument() const { return m_controller.document(); } void UnifiedDiffEditorWidget::saveState() { if (!m_state.isNull()) return; m_state = SelectableTextEditorWidget::saveState(); } void UnifiedDiffEditorWidget::restoreState() { if (m_state.isNull()) return; const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange; m_controller.m_ignoreCurrentIndexChange = true; SelectableTextEditorWidget::restoreState(m_state); m_state.clear(); m_controller.m_ignoreCurrentIndexChange = oldIgnore; } void UnifiedDiffEditorWidget::setDisplaySettings(const DisplaySettings &ds) { DisplaySettings settings = displaySettings(); settings.m_visualizeWhitespace = ds.m_visualizeWhitespace; settings.m_displayFoldingMarkers = ds.m_displayFoldingMarkers; settings.m_scrollBarHighlights = ds.m_scrollBarHighlights; settings.m_highlightCurrentLine = ds.m_highlightCurrentLine; SelectableTextEditorWidget::setDisplaySettings(settings); } void UnifiedDiffEditorWidget::setFontSettings(const FontSettings &fontSettings) { m_controller.setFontSettings(fontSettings); } void UnifiedDiffEditorWidget::slotCursorPositionChangedInEditor() { if (m_controller.m_ignoreCurrentIndexChange) return; const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange; m_controller.m_ignoreCurrentIndexChange = true; emit currentDiffFileIndexChanged(fileIndexForBlockNumber(textCursor().blockNumber())); m_controller.m_ignoreCurrentIndexChange = oldIgnore; } void UnifiedDiffEditorWidget::mouseDoubleClickEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) { QTextCursor cursor = cursorForPosition(e->pos()); jumpToOriginalFile(cursor); e->accept(); return; } SelectableTextEditorWidget::mouseDoubleClickEvent(e); } void UnifiedDiffEditorWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { jumpToOriginalFile(textCursor()); e->accept(); return; } SelectableTextEditorWidget::keyPressEvent(e); } void UnifiedDiffEditorWidget::contextMenuEvent(QContextMenuEvent *e) { QPointer menu = createStandardContextMenu(); const QTextCursor tc = textCursor(); QTextCursor start = tc; start.setPosition(tc.selectionStart()); QTextCursor end = tc; end.setPosition(tc.selectionEnd()); const int startBlockNumber = start.blockNumber(); const int endBlockNumber = end.blockNumber(); QTextCursor cursor = cursorForPosition(e->pos()); const int blockNumber = cursor.blockNumber(); const int fileIndex = fileIndexForBlockNumber(blockNumber); const int chunkIndex = chunkIndexForBlockNumber(blockNumber); const ChunkData chunkData = m_controller.chunkData(fileIndex, chunkIndex); QList leftSelection, rightSelection; for (int i = startBlockNumber; i <= endBlockNumber; ++i) { const int currentFileIndex = fileIndexForBlockNumber(i); if (currentFileIndex < fileIndex) continue; if (currentFileIndex > fileIndex) break; const int currentChunkIndex = chunkIndexForBlockNumber(i); if (currentChunkIndex < chunkIndex) continue; if (currentChunkIndex > chunkIndex) break; const int leftRow = m_leftLineNumbers.value(i, qMakePair(-1, -1)).second; const int rightRow = m_rightLineNumbers.value(i, qMakePair(-1, -1)).second; if (leftRow >= 0) leftSelection.append(leftRow); if (rightRow >= 0) rightSelection.append(rightRow); } const ChunkSelection selection(leftSelection, rightSelection); addContextMenuActions(menu, fileIndexForBlockNumber(blockNumber), chunkIndexForBlockNumber(blockNumber), selection); connect(this, &UnifiedDiffEditorWidget::destroyed, menu.data(), &QMenu::deleteLater); menu->exec(e->globalPos()); delete menu; } void UnifiedDiffEditorWidget::addContextMenuActions(QMenu *menu, int fileIndex, int chunkIndex, const ChunkSelection &selection) { menu->addSeparator(); m_controller.addCodePasterAction(menu, fileIndex, chunkIndex); m_controller.addApplyAction(menu, fileIndex, chunkIndex); m_controller.addRevertAction(menu, fileIndex, chunkIndex); m_controller.addExtraActions(menu, fileIndex, chunkIndex, selection); } void UnifiedDiffEditorWidget::clear(const QString &message) { m_leftLineNumberDigits = 1; m_rightLineNumberDigits = 1; m_leftLineNumbers.clear(); m_rightLineNumbers.clear(); m_fileInfo.clear(); m_chunkInfo.clear(); setSelections(QMap >()); const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange; m_controller.m_ignoreCurrentIndexChange = true; SelectableTextEditorWidget::clear(); m_controller.m_contextFileData.clear(); setPlainText(message); m_controller.m_ignoreCurrentIndexChange = oldIgnore; } QString UnifiedDiffEditorWidget::lineNumber(int blockNumber) const { QString lineNumberString; const bool leftLineExists = m_leftLineNumbers.contains(blockNumber); const bool rightLineExists = m_rightLineNumbers.contains(blockNumber); if (leftLineExists || rightLineExists) { const QString leftLine = leftLineExists ? QString::number(m_leftLineNumbers.value(blockNumber).first) : QString(); lineNumberString += QString(m_leftLineNumberDigits - leftLine.count(), ' ') + leftLine; lineNumberString += '|'; const QString rightLine = rightLineExists ? QString::number(m_rightLineNumbers.value(blockNumber).first) : QString(); lineNumberString += QString(m_rightLineNumberDigits - rightLine.count(), ' ') + rightLine; } return lineNumberString; } int UnifiedDiffEditorWidget::lineNumberDigits() const { return m_leftLineNumberDigits + m_rightLineNumberDigits + 1; } void UnifiedDiffEditorWidget::setLeftLineNumber(int blockNumber, int lineNumber, int rowNumberInChunk) { const QString lineNumberString = QString::number(lineNumber); m_leftLineNumbers.insert(blockNumber, qMakePair(lineNumber, rowNumberInChunk)); m_leftLineNumberDigits = qMax(m_leftLineNumberDigits, lineNumberString.count()); } void UnifiedDiffEditorWidget::setRightLineNumber(int blockNumber, int lineNumber, int rowNumberInChunk) { const QString lineNumberString = QString::number(lineNumber); m_rightLineNumbers.insert(blockNumber, qMakePair(lineNumber, rowNumberInChunk)); m_rightLineNumberDigits = qMax(m_rightLineNumberDigits, lineNumberString.count()); } void UnifiedDiffEditorWidget::setFileInfo(int blockNumber, const DiffFileInfo &leftFileInfo, const DiffFileInfo &rightFileInfo) { m_fileInfo[blockNumber] = qMakePair(leftFileInfo, rightFileInfo); } void UnifiedDiffEditorWidget::setChunkIndex(int startBlockNumber, int blockCount, int chunkIndex) { m_chunkInfo.insert(startBlockNumber, qMakePair(blockCount, chunkIndex)); } void UnifiedDiffEditorWidget::setDiff(const QList &diffFileList) { const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange; m_controller.m_ignoreCurrentIndexChange = true; clear(); m_controller.m_contextFileData = diffFileList; showDiff(); m_controller.m_ignoreCurrentIndexChange = oldIgnore; } QString UnifiedDiffEditorWidget::showChunk(const ChunkData &chunkData, bool lastChunk, int *blockNumber, int *charNumber, QMap > *selections) { if (chunkData.contextChunk) return QString(); QString diffText; int leftLineCount = 0; int rightLineCount = 0; int blockCount = 0; int charCount = 0; QList leftBuffer, rightBuffer; QList leftRowsBuffer, rightRowsBuffer; (*selections)[*blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat)); int lastEqualRow = -1; if (lastChunk) { for (int i = chunkData.rows.count(); i > 0; i--) { if (chunkData.rows.at(i - 1).equal) { if (i != chunkData.rows.count()) lastEqualRow = i - 1; break; } } } for (int i = 0; i <= chunkData.rows.count(); i++) { const RowData &rowData = i < chunkData.rows.count() ? chunkData.rows.at(i) : RowData(TextLineData(TextLineData::Separator)); // dummy, // ensure we process buffers to the end. // rowData will be equal if (rowData.equal && i != lastEqualRow) { if (!leftBuffer.isEmpty()) { for (int j = 0; j < leftBuffer.count(); j++) { const TextLineData &lineData = leftBuffer.at(j); const QString line = DiffUtils::makePatchLine( '-', lineData.text, lastChunk, i == chunkData.rows.count() && j == leftBuffer.count() - 1); const int blockDelta = line.count('\n'); // no new line // could have been added for (int k = 0; k < blockDelta; k++) (*selections)[*blockNumber + blockCount + 1 + k].append(&m_controller.m_leftLineFormat); for (auto it = lineData.changedPositions.cbegin(), end = lineData.changedPositions.cend(); it != end; ++it) { const int startPos = it.key() < 0 ? 1 : it.key() + 1; const int endPos = it.value() < 0 ? it.value() : it.value() + 1; (*selections)[*blockNumber + blockCount + 1].append( DiffSelection(startPos, endPos, &m_controller.m_leftCharFormat)); } if (!line.isEmpty()) { setLeftLineNumber(*blockNumber + blockCount + 1, chunkData.leftStartingLineNumber + leftLineCount + 1, leftRowsBuffer.at(j)); blockCount += blockDelta; ++leftLineCount; } diffText += line; charCount += line.count(); } leftBuffer.clear(); leftRowsBuffer.clear(); } if (!rightBuffer.isEmpty()) { for (int j = 0; j < rightBuffer.count(); j++) { const TextLineData &lineData = rightBuffer.at(j); const QString line = DiffUtils::makePatchLine( '+', lineData.text, lastChunk, i == chunkData.rows.count() && j == rightBuffer.count() - 1); const int blockDelta = line.count('\n'); // no new line // could have been added for (int k = 0; k < blockDelta; k++) (*selections)[*blockNumber + blockCount + 1 + k].append(&m_controller.m_rightLineFormat); for (auto it = lineData.changedPositions.cbegin(), end = lineData.changedPositions.cend(); it != end; ++it) { const int startPos = it.key() < 0 ? 1 : it.key() + 1; const int endPos = it.value() < 0 ? it.value() : it.value() + 1; (*selections)[*blockNumber + blockCount + 1].append (DiffSelection(startPos, endPos, &m_controller.m_rightCharFormat)); } if (!line.isEmpty()) { setRightLineNumber(*blockNumber + blockCount + 1, chunkData.rightStartingLineNumber + rightLineCount + 1, rightRowsBuffer.at(j)); blockCount += blockDelta; ++rightLineCount; } diffText += line; charCount += line.count(); } rightBuffer.clear(); rightRowsBuffer.clear(); } if (i < chunkData.rows.count()) { const QString line = DiffUtils::makePatchLine(' ', rowData.rightLine.text, lastChunk, i == chunkData.rows.count() - 1); if (!line.isEmpty()) { setLeftLineNumber(*blockNumber + blockCount + 1, chunkData.leftStartingLineNumber + leftLineCount + 1, i); setRightLineNumber(*blockNumber + blockCount + 1, chunkData.rightStartingLineNumber + rightLineCount + 1, i); blockCount += line.count('\n'); ++leftLineCount; ++rightLineCount; } diffText += line; charCount += line.count(); } } else { if (rowData.leftLine.textLineType == TextLineData::TextLine) { leftBuffer.append(rowData.leftLine); leftRowsBuffer.append(i); } if (rowData.rightLine.textLineType == TextLineData::TextLine) { rightBuffer.append(rowData.rightLine); rightRowsBuffer.append(i); } } } const QString chunkLine = "@@ -" + QString::number(chunkData.leftStartingLineNumber + 1) + ',' + QString::number(leftLineCount) + " +" + QString::number(chunkData.rightStartingLineNumber+ 1) + ',' + QString::number(rightLineCount) + " @@" + chunkData.contextInfo + '\n'; diffText.prepend(chunkLine); *blockNumber += blockCount + 1; // +1 for chunk line *charNumber += charCount + chunkLine.count(); return diffText; } void UnifiedDiffEditorWidget::showDiff() { QString diffText; int blockNumber = 0; int charNumber = 0; // 'foldingIndent' is populated with and folding indentation // value where 1 indicates start of new file and 2 indicates a diff chunk. // Remaining lines (diff contents) are assigned 3. QHash foldingIndent; QMap > selections; for (const FileData &fileData : qAsConst(m_controller.m_contextFileData)) { const QString leftFileInfo = "--- " + fileData.leftFileInfo.fileName + '\n'; const QString rightFileInfo = "+++ " + fileData.rightFileInfo.fileName + '\n'; setFileInfo(blockNumber, fileData.leftFileInfo, fileData.rightFileInfo); foldingIndent.insert(blockNumber, 1); selections[blockNumber].append(DiffSelection(&m_controller.m_fileLineFormat)); blockNumber++; foldingIndent.insert(blockNumber, 1); selections[blockNumber].append(DiffSelection(&m_controller.m_fileLineFormat)); blockNumber++; diffText += leftFileInfo; diffText += rightFileInfo; charNumber += leftFileInfo.count() + rightFileInfo.count(); if (fileData.binaryFiles) { foldingIndent.insert(blockNumber, 2); selections[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat)); blockNumber++; const QString binaryLine = "Binary files " + fileData.leftFileInfo.fileName + " and " + fileData.rightFileInfo.fileName + " differ\n"; diffText += binaryLine; charNumber += binaryLine.count(); } else { for (int j = 0; j < fileData.chunks.count(); j++) { const int oldBlockNumber = blockNumber; foldingIndent.insert(blockNumber, 2); diffText += showChunk(fileData.chunks.at(j), (j == fileData.chunks.count() - 1) && fileData.lastChunkAtTheEndOfFile, &blockNumber, &charNumber, &selections); if (!fileData.chunks.at(j).contextChunk) setChunkIndex(oldBlockNumber, blockNumber - oldBlockNumber, j); } } } if (diffText.isEmpty()) { setPlainText(tr("No difference.")); return; } diffText.replace('\r', ' '); const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange; m_controller.m_ignoreCurrentIndexChange = true; setPlainText(diffText); QTextBlock block = document()->firstBlock(); for (int b = 0; block.isValid(); block = block.next(), ++b) setFoldingIndent(block, foldingIndent.value(b, 3)); m_controller.m_ignoreCurrentIndexChange = oldIgnore; setSelections(selections); } int UnifiedDiffEditorWidget::blockNumberForFileIndex(int fileIndex) const { if (fileIndex < 0 || fileIndex >= m_fileInfo.count()) return -1; return std::next(m_fileInfo.constBegin(), fileIndex).key(); } int UnifiedDiffEditorWidget::fileIndexForBlockNumber(int blockNumber) const { int i = -1; for (auto it = m_fileInfo.cbegin(), end = m_fileInfo.cend(); it != end; ++it, ++i) { if (it.key() > blockNumber) break; } return i; } int UnifiedDiffEditorWidget::chunkIndexForBlockNumber(int blockNumber) const { if (m_chunkInfo.isEmpty()) return -1; auto it = m_chunkInfo.upperBound(blockNumber); if (it == m_chunkInfo.constBegin()) return -1; --it; if (blockNumber < it.key() + it.value().first) return it.value().second; return -1; } void UnifiedDiffEditorWidget::jumpToOriginalFile(const QTextCursor &cursor) { if (m_fileInfo.isEmpty()) return; const int blockNumber = cursor.blockNumber(); const int fileIndex = fileIndexForBlockNumber(blockNumber); if (fileIndex < 0) return; const FileData fileData = m_controller.m_contextFileData.at(fileIndex); const QString leftFileName = fileData.leftFileInfo.fileName; const QString rightFileName = fileData.rightFileInfo.fileName; const int columnNumber = cursor.positionInBlock() - 1; // -1 for the first character in line const int rightLineNumber = m_rightLineNumbers.value(blockNumber, qMakePair(-1, 0)).first; if (rightLineNumber >= 0) { m_controller.jumpToOriginalFile(rightFileName, rightLineNumber, columnNumber); return; } const int leftLineNumber = m_leftLineNumbers.value(blockNumber, qMakePair(-1, 0)).first; if (leftLineNumber >= 0) { if (leftFileName == rightFileName) { for (const ChunkData &chunkData : fileData.chunks) { int newLeftLineNumber = chunkData.leftStartingLineNumber; int newRightLineNumber = chunkData.rightStartingLineNumber; for (const RowData &rowData : chunkData.rows) { if (rowData.leftLine.textLineType == TextLineData::TextLine) newLeftLineNumber++; if (rowData.rightLine.textLineType == TextLineData::TextLine) newRightLineNumber++; if (newLeftLineNumber == leftLineNumber) { m_controller.jumpToOriginalFile(leftFileName, newRightLineNumber, 0); return; } } } } else { m_controller.jumpToOriginalFile(leftFileName, leftLineNumber, columnNumber); } return; } } void UnifiedDiffEditorWidget::setCurrentDiffFileIndex(int diffFileIndex) { if (m_controller.m_ignoreCurrentIndexChange) return; const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange; m_controller.m_ignoreCurrentIndexChange = true; const int blockNumber = blockNumberForFileIndex(diffFileIndex); QTextBlock block = document()->findBlockByNumber(blockNumber); QTextCursor cursor = textCursor(); cursor.setPosition(block.position()); setTextCursor(cursor); verticalScrollBar()->setValue(blockNumber); m_controller.m_ignoreCurrentIndexChange = oldIgnore; } } // namespace Internal } // namespace DiffEditor