Files
qt-creator/src/plugins/diffeditor/sidebysidediffeditorwidget.cpp
Jarek Kobus 64233a4fae Fix repeting Stage/Unstage actions in unified diff editor
Don't store diff file index, chunk index and diff editor
controller anymore. Pass needed indices by value,
retrieve them when needed as lambda copy.

Change-Id: I3a81f1ab6d131c0b1d9899ac4b061b6e25582f51
Reviewed-by: Tobias Hunger <tobias.hunger@qt.io>
2018-02-16 06:48:27 +00:00

1171 lines
44 KiB
C++

/****************************************************************************
**
** 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 "sidebysidediffeditorwidget.h"
#include "selectabletexteditorwidget.h"
#include "diffeditorconstants.h"
#include "diffeditordocument.h"
#include "diffutils.h"
#include <QMenu>
#include <QPainter>
#include <QScrollBar>
#include <QTextBlock>
#include <QVBoxLayout>
#include <texteditor/textdocument.h>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditorsettings.h>
#include <texteditor/fontsettings.h>
#include <texteditor/displaysettings.h>
#include <coreplugin/icore.h>
#include <coreplugin/find/highlightscrollbarcontroller.h>
#include <coreplugin/minisplitter.h>
#include <utils/tooltip/tooltip.h>
using namespace Core;
using namespace TextEditor;
using namespace Utils;
namespace DiffEditor {
namespace Internal {
class SideDiffEditorWidget : public SelectableTextEditorWidget
{
Q_OBJECT
public:
SideDiffEditorWidget(QWidget *parent = nullptr);
// block number, file info
QMap<int, DiffFileInfo> fileInfo() const { return m_fileInfo; }
void setLineNumber(int blockNumber, int lineNumber);
void setFileInfo(int blockNumber, const DiffFileInfo &fileInfo);
void setSkippedLines(int blockNumber, int skippedLines, const QString &contextInfo = QString()) {
m_skippedLines[blockNumber] = qMakePair(skippedLines, contextInfo);
setSeparator(blockNumber, true);
}
void setChunkIndex(int startBlockNumber, int blockCount, int chunkIndex);
void setSeparator(int blockNumber, bool separator) {
m_separators[blockNumber] = separator;
}
bool isFileLine(int blockNumber) const {
return m_fileInfo.contains(blockNumber);
}
int blockNumberForFileIndex(int fileIndex) const;
int fileIndexForBlockNumber(int blockNumber) const;
int chunkIndexForBlockNumber(int blockNumber) const;
bool isChunkLine(int blockNumber) const {
return m_skippedLines.contains(blockNumber);
}
void clearAll(const QString &message);
void clearAllData();
void saveState();
void restoreState();
void setFolded(int blockNumber, bool folded);
void setDisplaySettings(const DisplaySettings &ds) override;
signals:
void jumpToOriginalFileRequested(int diffFileIndex,
int lineNumber,
int columnNumber);
void contextMenuRequested(QMenu *menu,
int diffFileIndex,
int chunkIndex);
void foldChanged(int blockNumber, bool folded);
void gotDisplaySettings();
void gotFocus();
protected:
int extraAreaWidth(int *markWidthPtr = nullptr) const override {
return SelectableTextEditorWidget::extraAreaWidth(markWidthPtr);
}
void applyFontSettings() override;
QString lineNumber(int blockNumber) const override;
int lineNumberDigits() const override;
bool selectionVisible(int blockNumber) const override;
bool replacementVisible(int blockNumber) const override;
QColor replacementPenColor(int blockNumber) const override;
QString plainTextFromSelection(const QTextCursor &cursor) const override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void scrollContentsBy(int dx, int dy) override;
void customDrawCollapsedBlockPopup(QPainter &painter,
const QTextBlock &block,
QPointF offset,
const QRect &clip);
void drawCollapsedBlockPopup(QPainter &painter,
const QTextBlock &block,
QPointF offset,
const QRect &clip) override;
void focusInEvent(QFocusEvent *e) override;
private:
void paintSeparator(QPainter &painter, QColor &color, const QString &text,
const QTextBlock &block, int top);
void jumpToOriginalFile(const QTextCursor &cursor);
// block number, visual line number.
QMap<int, int> m_lineNumbers;
int m_lineNumberDigits = 1;
// block number, fileInfo. Set for file lines only.
QMap<int, DiffFileInfo> m_fileInfo;
// block number, skipped lines and context info. Set for chunk lines only.
QMap<int, QPair<int, QString> > m_skippedLines;
// start block number, block count of a chunk, chunk index inside a file.
QMap<int, QPair<int, int> > m_chunkInfo;
// block number, separator. Set for file, chunk or span line.
QMap<int, bool> m_separators;
QColor m_fileLineForeground;
QColor m_chunkLineForeground;
QColor m_textForeground;
QByteArray m_state;
QTextBlock m_drawCollapsedBlock;
QPointF m_drawCollapsedOffset;
QRect m_drawCollapsedClip;
};
SideDiffEditorWidget::SideDiffEditorWidget(QWidget *parent)
: SelectableTextEditorWidget("DiffEditor.SideDiffEditor", parent)
{
DisplaySettings settings = displaySettings();
settings.m_textWrapping = false;
settings.m_displayLineNumbers = true;
settings.m_markTextChanges = false;
settings.m_highlightBlocks = false;
SelectableTextEditorWidget::setDisplaySettings(settings);
connect(this, &TextEditorWidget::tooltipRequested, [this](const QPoint &point, int position) {
const int block = document()->findBlock(position).blockNumber();
const auto it = m_fileInfo.constFind(block);
if (it != m_fileInfo.constEnd())
ToolTip::show(point, it.value().fileName, this);
else
ToolTip::hide();
});
TextDocumentLayout *documentLayout = qobject_cast<TextDocumentLayout*>(document()->documentLayout());
if (documentLayout)
connect(documentLayout, &TextDocumentLayout::foldChanged,
this, &SideDiffEditorWidget::foldChanged);
setCodeFoldingSupported(true);
}
void SideDiffEditorWidget::saveState()
{
if (!m_state.isNull())
return;
m_state = SelectableTextEditorWidget::saveState();
}
void SideDiffEditorWidget::restoreState()
{
if (m_state.isNull())
return;
SelectableTextEditorWidget::restoreState(m_state);
m_state.clear();
}
void SideDiffEditorWidget::setFolded(int blockNumber, bool folded)
{
QTextBlock block = document()->findBlockByNumber(blockNumber);
if (!block.isValid())
return;
if (TextDocumentLayout::isFolded(block) == folded)
return;
TextDocumentLayout::doFoldOrUnfold(block, !folded);
TextDocumentLayout *documentLayout = qobject_cast<TextDocumentLayout*>(document()->documentLayout());
documentLayout->requestUpdate();
documentLayout->emitDocumentSizeChanged();
}
void SideDiffEditorWidget::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);
emit gotDisplaySettings();
}
void SideDiffEditorWidget::applyFontSettings()
{
SelectableTextEditorWidget::applyFontSettings();
const FontSettings &fs = textDocument()->fontSettings();
m_fileLineForeground = fs.formatFor(C_DIFF_FILE_LINE).foreground();
m_chunkLineForeground = fs.formatFor(C_DIFF_CONTEXT_LINE).foreground();
m_textForeground = fs.toTextCharFormat(C_TEXT).foreground().color();
update();
}
QString SideDiffEditorWidget::lineNumber(int blockNumber) const
{
const auto it = m_lineNumbers.constFind(blockNumber);
if (it != m_lineNumbers.constEnd())
return QString::number(it.value());
return QString();
}
int SideDiffEditorWidget::lineNumberDigits() const
{
return m_lineNumberDigits;
}
bool SideDiffEditorWidget::selectionVisible(int blockNumber) const
{
return !m_separators.value(blockNumber, false);
}
bool SideDiffEditorWidget::replacementVisible(int blockNumber) const
{
return isChunkLine(blockNumber) || (isFileLine(blockNumber)
&& TextDocumentLayout::isFolded(document()->findBlockByNumber(blockNumber)));
}
QColor SideDiffEditorWidget::replacementPenColor(int blockNumber) const
{
Q_UNUSED(blockNumber)
return m_chunkLineForeground;
}
QString SideDiffEditorWidget::plainTextFromSelection(const QTextCursor &cursor) const
{
const int startPosition = cursor.selectionStart();
const int endPosition = cursor.selectionEnd();
if (startPosition == endPosition)
return QString(); // no selection
const QTextBlock startBlock = document()->findBlock(startPosition);
const QTextBlock endBlock = document()->findBlock(endPosition);
QTextBlock block = startBlock;
QString text;
bool textInserted = false;
while (block.isValid() && block.blockNumber() <= endBlock.blockNumber()) {
if (selectionVisible(block.blockNumber())) {
if (block == startBlock) {
if (block == endBlock)
text = cursor.selectedText(); // just one line text
else
text = block.text().mid(startPosition - block.position());
} else {
if (textInserted)
text += '\n';
if (block == endBlock)
text += block.text().leftRef(endPosition - block.position());
else
text += block.text();
}
textInserted = true;
}
block = block.next();
}
return convertToPlainText(text);
}
void SideDiffEditorWidget::setLineNumber(int blockNumber, int lineNumber)
{
const QString lineNumberString = QString::number(lineNumber);
m_lineNumbers.insert(blockNumber, lineNumber);
m_lineNumberDigits = qMax(m_lineNumberDigits, lineNumberString.count());
}
void SideDiffEditorWidget::setFileInfo(int blockNumber, const DiffFileInfo &fileInfo)
{
m_fileInfo[blockNumber] = fileInfo;
setSeparator(blockNumber, true);
}
void SideDiffEditorWidget::setChunkIndex(int startBlockNumber, int blockCount, int chunkIndex)
{
m_chunkInfo.insert(startBlockNumber, qMakePair(blockCount, chunkIndex));
}
int SideDiffEditorWidget::blockNumberForFileIndex(int fileIndex) const
{
if (fileIndex < 0 || fileIndex >= m_fileInfo.count())
return -1;
return (m_fileInfo.constBegin() + fileIndex).key();
}
int SideDiffEditorWidget::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 SideDiffEditorWidget::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 SideDiffEditorWidget::clearAll(const QString &message)
{
setBlockSelection(false);
clear();
clearAllData();
setExtraSelections(TextEditorWidget::OtherSelection,
QList<QTextEdit::ExtraSelection>());
setPlainText(message);
}
void SideDiffEditorWidget::clearAllData()
{
m_lineNumberDigits = 1;
m_lineNumbers.clear();
m_fileInfo.clear();
m_skippedLines.clear();
m_chunkInfo.clear();
m_separators.clear();
setSelections(QMap<int, QList<DiffSelection> >());
}
void SideDiffEditorWidget::scrollContentsBy(int dx, int dy)
{
SelectableTextEditorWidget::scrollContentsBy(dx, dy);
// TODO: update only chunk lines
viewport()->update();
}
void SideDiffEditorWidget::paintSeparator(QPainter &painter,
QColor &color,
const QString &text,
const QTextBlock &block,
int top)
{
QPointF offset = contentOffset();
painter.save();
QColor foreground = color;
if (!foreground.isValid())
foreground = m_textForeground;
if (!foreground.isValid())
foreground = palette().foreground().color();
painter.setPen(foreground);
const QString replacementText = " {" + foldReplacementText(block) + "}; ";
const int replacementTextWidth = fontMetrics().width(replacementText) + 24;
int x = replacementTextWidth + int(offset.x());
if (x < document()->documentMargin()
|| !TextDocumentLayout::isFolded(block)) {
x = int(document()->documentMargin());
}
const QString elidedText = fontMetrics().elidedText(text,
Qt::ElideRight,
viewport()->width() - x);
QTextLayout *layout = block.layout();
QTextLine textLine = layout->lineAt(0);
QRectF lineRect = textLine.naturalTextRect().translated(offset.x(), top);
QRect clipRect = contentsRect();
clipRect.setLeft(x);
painter.setClipRect(clipRect);
painter.drawText(QPointF(x, lineRect.top() + textLine.ascent()),
elidedText);
painter.restore();
}
void SideDiffEditorWidget::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 SideDiffEditorWidget::keyPressEvent(QKeyEvent *e)
{
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
jumpToOriginalFile(textCursor());
e->accept();
return;
}
SelectableTextEditorWidget::keyPressEvent(e);
}
void SideDiffEditorWidget::contextMenuEvent(QContextMenuEvent *e)
{
QPointer<QMenu> menu = createStandardContextMenu();
QTextCursor cursor = cursorForPosition(e->pos());
const int blockNumber = cursor.blockNumber();
emit contextMenuRequested(menu, fileIndexForBlockNumber(blockNumber),
chunkIndexForBlockNumber(blockNumber));
connect(this, &SideDiffEditorWidget::destroyed, menu.data(), &QMenu::deleteLater);
menu->exec(e->globalPos());
delete menu;
}
void SideDiffEditorWidget::jumpToOriginalFile(const QTextCursor &cursor)
{
if (m_fileInfo.isEmpty())
return;
const int blockNumber = cursor.blockNumber();
if (!m_lineNumbers.contains(blockNumber))
return;
const int lineNumber = m_lineNumbers.value(blockNumber);
const int columnNumber = cursor.positionInBlock();
emit jumpToOriginalFileRequested(fileIndexForBlockNumber(blockNumber),
lineNumber, columnNumber);
}
static QString skippedText(int skippedNumber)
{
if (skippedNumber > 0)
return SideBySideDiffEditorWidget::tr("Skipped %n lines...", 0, skippedNumber);
if (skippedNumber == -2)
return SideBySideDiffEditorWidget::tr("Binary files differ");
return SideBySideDiffEditorWidget::tr("Skipped unknown number of lines...");
}
void SideDiffEditorWidget::paintEvent(QPaintEvent *e)
{
SelectableTextEditorWidget::paintEvent(e);
QPainter painter(viewport());
QPointF offset = contentOffset();
QTextBlock currentBlock = firstVisibleBlock();
while (currentBlock.isValid()) {
if (currentBlock.isVisible()) {
qreal top = blockBoundingGeometry(currentBlock).translated(offset).top();
qreal bottom = top + blockBoundingRect(currentBlock).height();
if (top > e->rect().bottom())
break;
if (bottom >= e->rect().top()) {
const int blockNumber = currentBlock.blockNumber();
auto it = m_skippedLines.constFind(blockNumber);
if (it != m_skippedLines.constEnd()) {
QString skippedRowsText = '[' + skippedText(it->first) + ']';
if (!it->second.isEmpty())
skippedRowsText += ' ' + it->second;
paintSeparator(painter, m_chunkLineForeground,
skippedRowsText, currentBlock, top);
}
const DiffFileInfo fileInfo = m_fileInfo.value(blockNumber);
if (!fileInfo.fileName.isEmpty()) {
const QString fileNameText = fileInfo.typeInfo.isEmpty()
? fileInfo.fileName
: tr("[%1] %2").arg(fileInfo.typeInfo)
.arg(fileInfo.fileName);
paintSeparator(painter, m_fileLineForeground,
fileNameText, currentBlock, top);
}
}
}
currentBlock = currentBlock.next();
}
if (m_drawCollapsedBlock.isValid()) {
// draw it now
customDrawCollapsedBlockPopup(painter,
m_drawCollapsedBlock,
m_drawCollapsedOffset,
m_drawCollapsedClip);
// reset the data for the drawing
m_drawCollapsedBlock = QTextBlock();
}
}
void SideDiffEditorWidget::customDrawCollapsedBlockPopup(QPainter &painter,
const QTextBlock &block,
QPointF offset,
const QRect &clip)
{
int margin = block.document()->documentMargin();
qreal maxWidth = 0;
qreal blockHeight = 0;
QTextBlock b = block;
while (!b.isVisible()) {
const int blockNumber = b.blockNumber();
if (!m_skippedLines.contains(blockNumber) && !m_separators.contains(blockNumber)) {
b.setVisible(true); // make sure block bounding rect works
QRectF r = blockBoundingRect(b).translated(offset);
QTextLayout *layout = b.layout();
for (int i = layout->lineCount()-1; i >= 0; --i)
maxWidth = qMax(maxWidth, layout->lineAt(i).naturalTextWidth() + 2*margin);
blockHeight += r.height();
b.setVisible(false); // restore previous state
b.setLineCount(0); // restore 0 line count for invisible block
}
b = b.next();
}
painter.save();
painter.setRenderHint(QPainter::Antialiasing, true);
painter.translate(.5, .5);
QBrush brush = palette().base();
const QTextCharFormat &ifdefedOutFormat
= textDocument()->fontSettings().toTextCharFormat(C_DISABLED_CODE);
if (ifdefedOutFormat.hasProperty(QTextFormat::BackgroundBrush))
brush = ifdefedOutFormat.background();
painter.setBrush(brush);
painter.drawRoundedRect(QRectF(offset.x(),
offset.y(),
maxWidth, blockHeight).adjusted(0, 0, 0, 0), 3, 3);
painter.restore();
QTextBlock end = b;
b = block;
while (b != end) {
const int blockNumber = b.blockNumber();
if (!m_skippedLines.contains(blockNumber) && !m_separators.contains(blockNumber)) {
b.setVisible(true); // make sure block bounding rect works
QRectF r = blockBoundingRect(b).translated(offset);
QTextLayout *layout = b.layout();
QVector<QTextLayout::FormatRange> selections;
layout->draw(&painter, offset, selections, clip);
b.setVisible(false); // restore previous state
b.setLineCount(0); // restore 0 line count for invisible block
offset.ry() += r.height();
}
b = b.next();
}
}
void SideDiffEditorWidget::drawCollapsedBlockPopup(QPainter &painter,
const QTextBlock &block,
QPointF offset,
const QRect &clip)
{
Q_UNUSED(painter)
// Called from inside SelectableTextEditorWidget::paintEvent().
// Postpone the drawing for now, do it after our paintEvent's
// custom painting. Store the data for the future redraw.
m_drawCollapsedBlock = block;
m_drawCollapsedOffset = offset;
m_drawCollapsedClip = clip;
}
void SideDiffEditorWidget::focusInEvent(QFocusEvent *e)
{
SelectableTextEditorWidget::focusInEvent(e);
emit gotFocus();
}
//////////////////
SideBySideDiffEditorWidget::SideBySideDiffEditorWidget(QWidget *parent)
: QWidget(parent)
, m_controller(this)
{
m_leftEditor = new SideDiffEditorWidget(this);
m_leftEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_leftEditor->setReadOnly(true);
m_leftEditor->setCodeStyle(TextEditorSettings::codeStyle());
connect(m_leftEditor, &SideDiffEditorWidget::jumpToOriginalFileRequested,
this, &SideBySideDiffEditorWidget::slotLeftJumpToOriginalFileRequested);
connect(m_leftEditor, &SideDiffEditorWidget::contextMenuRequested,
this, &SideBySideDiffEditorWidget::slotLeftContextMenuRequested,
Qt::DirectConnection);
m_rightEditor = new SideDiffEditorWidget(this);
m_rightEditor->setReadOnly(true);
m_rightEditor->setCodeStyle(TextEditorSettings::codeStyle());
connect(m_rightEditor, &SideDiffEditorWidget::jumpToOriginalFileRequested,
this, &SideBySideDiffEditorWidget::slotRightJumpToOriginalFileRequested);
connect(m_rightEditor, &SideDiffEditorWidget::contextMenuRequested,
this, &SideBySideDiffEditorWidget::slotRightContextMenuRequested,
Qt::DirectConnection);
auto setupHighlightController = [this]() {
HighlightScrollBarController *highlightController = m_leftEditor->highlightScrollBarController();
if (highlightController)
highlightController->setScrollArea(m_rightEditor);
};
setupHighlightController();
connect(m_leftEditor, &SideDiffEditorWidget::gotDisplaySettings, setupHighlightController);
m_rightEditor->verticalScrollBar()->setFocusProxy(m_leftEditor);
connect(m_leftEditor, &SideDiffEditorWidget::gotFocus, [this]() {
if (m_rightEditor->verticalScrollBar()->focusProxy() == m_leftEditor)
return; // We already did it before.
// Hack #1. If the left editor got a focus last time
// we don't want to focus right editor when clicking the right
// scrollbar.
m_rightEditor->verticalScrollBar()->setFocusProxy(m_leftEditor);
// Hack #2. If the focus is currently not on the scrollbar's proxy
// and we click on the scrollbar, the focus will go to the parent
// of the scrollbar. In order to give the focus to the proxy
// we need to set a click focus policy on the scrollbar.
// See QApplicationPrivate::giveFocusAccordingToFocusPolicy().
m_rightEditor->verticalScrollBar()->setFocusPolicy(Qt::ClickFocus);
// Hack #3. Setting the focus policy is not orthogonal to setting
// the focus proxy and unfortuantely it changes the policy of the proxy
// too. We bring back the original policy to keep tab focus working.
m_leftEditor->setFocusPolicy(Qt::StrongFocus);
});
connect(m_rightEditor, &SideDiffEditorWidget::gotFocus, [this]() {
// Unhack #1.
m_rightEditor->verticalScrollBar()->setFocusProxy(nullptr);
// Unhack #2.
m_rightEditor->verticalScrollBar()->setFocusPolicy(Qt::NoFocus);
});
connect(TextEditorSettings::instance(),
&TextEditorSettings::fontSettingsChanged,
this, &SideBySideDiffEditorWidget::setFontSettings);
setFontSettings(TextEditorSettings::fontSettings());
connect(m_leftEditor->verticalScrollBar(), &QAbstractSlider::valueChanged,
this, &SideBySideDiffEditorWidget::leftVSliderChanged);
connect(m_leftEditor->verticalScrollBar(), &QAbstractSlider::actionTriggered,
this, &SideBySideDiffEditorWidget::leftVSliderChanged);
connect(m_leftEditor->horizontalScrollBar(), &QAbstractSlider::valueChanged,
this, &SideBySideDiffEditorWidget::leftHSliderChanged);
connect(m_leftEditor->horizontalScrollBar(), &QAbstractSlider::actionTriggered,
this, &SideBySideDiffEditorWidget::leftHSliderChanged);
connect(m_leftEditor, &QPlainTextEdit::cursorPositionChanged,
this, &SideBySideDiffEditorWidget::leftCursorPositionChanged);
connect(m_rightEditor->verticalScrollBar(), &QAbstractSlider::valueChanged,
this, &SideBySideDiffEditorWidget::rightVSliderChanged);
connect(m_rightEditor->verticalScrollBar(), &QAbstractSlider::actionTriggered,
this, &SideBySideDiffEditorWidget::rightVSliderChanged);
connect(m_rightEditor->horizontalScrollBar(), &QAbstractSlider::valueChanged,
this, &SideBySideDiffEditorWidget::rightHSliderChanged);
connect(m_rightEditor->horizontalScrollBar(), &QAbstractSlider::actionTriggered,
this, &SideBySideDiffEditorWidget::rightHSliderChanged);
connect(m_rightEditor, &QPlainTextEdit::cursorPositionChanged,
this, &SideBySideDiffEditorWidget::rightCursorPositionChanged);
connect(m_leftEditor, &SideDiffEditorWidget::foldChanged,
m_rightEditor, &SideDiffEditorWidget::setFolded);
connect(m_rightEditor, &SideDiffEditorWidget::foldChanged,
m_leftEditor, &SideDiffEditorWidget::setFolded);
m_splitter = new MiniSplitter(this);
m_splitter->addWidget(m_leftEditor);
m_splitter->addWidget(m_rightEditor);
QVBoxLayout *l = new QVBoxLayout(this);
l->setMargin(0);
l->addWidget(m_splitter);
setFocusProxy(m_leftEditor);
m_leftContext = new IContext(this);
m_leftContext->setWidget(m_leftEditor);
m_leftContext->setContext(Core::Context(Core::Id(Constants::SIDE_BY_SIDE_VIEW_ID).withSuffix(1)));
Core::ICore::addContextObject(m_leftContext);
m_rightContext = new IContext(this);
m_rightContext->setWidget(m_rightEditor);
m_rightContext->setContext(Core::Context(Core::Id(Constants::SIDE_BY_SIDE_VIEW_ID).withSuffix(2)));
Core::ICore::addContextObject(m_rightContext);
}
SideBySideDiffEditorWidget::~SideBySideDiffEditorWidget()
{
Core::ICore::removeContextObject(m_leftContext);
Core::ICore::removeContextObject(m_rightContext);
}
TextEditorWidget *SideBySideDiffEditorWidget::leftEditorWidget() const
{
return m_leftEditor;
}
TextEditorWidget *SideBySideDiffEditorWidget::rightEditorWidget() const
{
return m_rightEditor;
}
void SideBySideDiffEditorWidget::setDocument(DiffEditorDocument *document)
{
m_controller.setDocument(document);
clear();
QList<FileData> diffFileList;
QString workingDirectory;
if (document) {
diffFileList = document->diffFiles();
workingDirectory = document->baseDirectory();
}
setDiff(diffFileList, workingDirectory);
}
DiffEditorDocument *SideBySideDiffEditorWidget::diffDocument() const
{
return m_controller.document();
}
void SideBySideDiffEditorWidget::clear(const QString &message)
{
const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
m_controller.m_ignoreCurrentIndexChange = true;
setDiff(QList<FileData>(), QString());
m_leftEditor->clearAll(message);
m_rightEditor->clearAll(message);
m_controller.m_ignoreCurrentIndexChange = oldIgnore;
}
void SideBySideDiffEditorWidget::setDiff(const QList<FileData> &diffFileList,
const QString &workingDirectory)
{
Q_UNUSED(workingDirectory)
const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
m_controller.m_ignoreCurrentIndexChange = true;
m_leftEditor->clear();
m_rightEditor->clear();
m_controller.m_contextFileData = diffFileList;
if (m_controller.m_contextFileData.isEmpty()) {
const QString msg = tr("No difference.");
m_leftEditor->setPlainText(msg);
m_rightEditor->setPlainText(msg);
} else {
showDiff();
}
m_controller.m_ignoreCurrentIndexChange = oldIgnore;
}
void SideBySideDiffEditorWidget::setCurrentDiffFileIndex(int diffFileIndex)
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
const int blockNumber = m_leftEditor->blockNumberForFileIndex(diffFileIndex);
const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
m_controller.m_ignoreCurrentIndexChange = true;
QTextBlock leftBlock = m_leftEditor->document()->findBlockByNumber(blockNumber);
QTextCursor leftCursor = m_leftEditor->textCursor();
leftCursor.setPosition(leftBlock.position());
m_leftEditor->setTextCursor(leftCursor);
m_leftEditor->verticalScrollBar()->setValue(blockNumber);
QTextBlock rightBlock = m_rightEditor->document()->findBlockByNumber(blockNumber);
QTextCursor rightCursor = m_rightEditor->textCursor();
rightCursor.setPosition(rightBlock.position());
m_rightEditor->setTextCursor(rightCursor);
m_rightEditor->verticalScrollBar()->setValue(blockNumber);
m_controller.m_ignoreCurrentIndexChange = oldIgnore;
}
void SideBySideDiffEditorWidget::setHorizontalSync(bool sync)
{
m_horizontalSync = sync;
rightHSliderChanged();
}
void SideBySideDiffEditorWidget::saveState()
{
m_leftEditor->saveState();
m_rightEditor->saveState();
}
void SideBySideDiffEditorWidget::restoreState()
{
m_leftEditor->restoreState();
m_rightEditor->restoreState();
}
void SideBySideDiffEditorWidget::showDiff()
{
QMap<int, QList<DiffSelection> > leftFormats;
QMap<int, QList<DiffSelection> > rightFormats;
QString leftTexts, rightTexts;
int blockNumber = 0;
QChar separator = '\n';
QHash<int, int> foldingIndent;
for (const FileData &contextFileData : m_controller.m_contextFileData) {
QString leftText, rightText;
foldingIndent.insert(blockNumber, 1);
leftFormats[blockNumber].append(DiffSelection(&m_controller.m_fileLineFormat));
rightFormats[blockNumber].append(DiffSelection(&m_controller.m_fileLineFormat));
m_leftEditor->setFileInfo(blockNumber, contextFileData.leftFileInfo);
m_rightEditor->setFileInfo(blockNumber, contextFileData.rightFileInfo);
leftText = separator;
rightText = separator;
blockNumber++;
int lastLeftLineNumber = -1;
if (contextFileData.binaryFiles) {
foldingIndent.insert(blockNumber, 2);
leftFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
rightFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
m_leftEditor->setSkippedLines(blockNumber, -2);
m_rightEditor->setSkippedLines(blockNumber, -2);
leftText += separator;
rightText += separator;
blockNumber++;
} else {
for (int j = 0; j < contextFileData.chunks.count(); j++) {
const ChunkData &chunkData = contextFileData.chunks.at(j);
int leftLineNumber = chunkData.leftStartingLineNumber;
int rightLineNumber = chunkData.rightStartingLineNumber;
if (!chunkData.contextChunk) {
const int skippedLines = leftLineNumber - lastLeftLineNumber - 1;
if (skippedLines > 0) {
foldingIndent.insert(blockNumber, 2);
leftFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
rightFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
m_leftEditor->setSkippedLines(blockNumber, skippedLines, chunkData.contextInfo);
m_rightEditor->setSkippedLines(blockNumber, skippedLines, chunkData.contextInfo);
leftText += separator;
rightText += separator;
blockNumber++;
}
m_leftEditor->setChunkIndex(blockNumber, chunkData.rows.count(), j);
m_rightEditor->setChunkIndex(blockNumber, chunkData.rows.count(), j);
for (const RowData &rowData : chunkData.rows) {
TextLineData leftLineData = rowData.leftLine;
TextLineData rightLineData = rowData.rightLine;
if (leftLineData.textLineType == TextLineData::TextLine) {
leftText += leftLineData.text;
lastLeftLineNumber = leftLineNumber;
leftLineNumber++;
m_leftEditor->setLineNumber(blockNumber, leftLineNumber);
} else if (leftLineData.textLineType == TextLineData::Separator) {
m_leftEditor->setSeparator(blockNumber, true);
}
if (rightLineData.textLineType == TextLineData::TextLine) {
rightText += rightLineData.text;
rightLineNumber++;
m_rightEditor->setLineNumber(blockNumber, rightLineNumber);
} else if (rightLineData.textLineType == TextLineData::Separator) {
m_rightEditor->setSeparator(blockNumber, true);
}
if (!rowData.equal) {
if (rowData.leftLine.textLineType == TextLineData::TextLine)
leftFormats[blockNumber].append(DiffSelection(&m_controller.m_leftLineFormat));
else
leftFormats[blockNumber].append(DiffSelection(&m_spanLineFormat));
if (rowData.rightLine.textLineType == TextLineData::TextLine)
rightFormats[blockNumber].append(DiffSelection(&m_controller.m_rightLineFormat));
else
rightFormats[blockNumber].append(DiffSelection(&m_spanLineFormat));
}
for (auto it = leftLineData.changedPositions.cbegin(),
end = leftLineData.changedPositions.cend(); it != end; ++it) {
leftFormats[blockNumber].append(
DiffSelection(it.key(), it.value(),
&m_controller.m_leftCharFormat));
}
for (auto it = rightLineData.changedPositions.cbegin(),
end = rightLineData.changedPositions.cend(); it != end; ++it) {
rightFormats[blockNumber].append(
DiffSelection(it.key(), it.value(),
&m_controller.m_rightCharFormat));
}
leftText += separator;
rightText += separator;
blockNumber++;
}
}
if (j == contextFileData.chunks.count() - 1) { // the last chunk
int skippedLines = -2;
if (chunkData.contextChunk) {
// if it's context chunk
skippedLines = chunkData.rows.count();
} else if (!contextFileData.lastChunkAtTheEndOfFile
&& !contextFileData.contextChunksIncluded) {
// if not a context chunk and not a chunk at the end of file
// and context lines not included
skippedLines = -1; // unknown count skipped by the end of file
}
if (skippedLines >= -1) {
leftFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
rightFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
m_leftEditor->setSkippedLines(blockNumber, skippedLines);
m_rightEditor->setSkippedLines(blockNumber, skippedLines);
leftText += separator;
rightText += separator;
blockNumber++;
} // otherwise nothing skipped
}
}
}
leftText.replace('\r', ' ');
rightText.replace('\r', ' ');
leftTexts += leftText;
rightTexts += rightText;
}
if (leftTexts.isEmpty() && rightTexts.isEmpty())
return;
const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
m_controller.m_ignoreCurrentIndexChange = true;
m_leftEditor->clear();
m_leftEditor->setPlainText(leftTexts);
m_rightEditor->clear();
m_rightEditor->setPlainText(rightTexts);
m_controller.m_ignoreCurrentIndexChange = oldIgnore;
QTextBlock block = m_leftEditor->document()->firstBlock();
for (int b = 0; block.isValid(); block = block.next(), ++b)
SelectableTextEditorWidget::setFoldingIndent(block, foldingIndent.value(b, 3));
block = m_rightEditor->document()->firstBlock();
for (int b = 0; block.isValid(); block = block.next(), ++b)
SelectableTextEditorWidget::setFoldingIndent(block, foldingIndent.value(b, 3));
m_leftEditor->setSelections(leftFormats);
m_rightEditor->setSelections(rightFormats);
}
void SideBySideDiffEditorWidget::setFontSettings(
const FontSettings &fontSettings)
{
m_spanLineFormat = fontSettings.toTextCharFormat(C_LINE_NUMBER);
m_controller.setFontSettings(fontSettings);
}
void SideBySideDiffEditorWidget::slotLeftJumpToOriginalFileRequested(
int diffFileIndex,
int lineNumber,
int columnNumber)
{
if (diffFileIndex < 0 || diffFileIndex >= m_controller.m_contextFileData.count())
return;
const FileData fileData = m_controller.m_contextFileData.at(diffFileIndex);
const QString leftFileName = fileData.leftFileInfo.fileName;
const QString rightFileName = fileData.rightFileInfo.fileName;
if (leftFileName == rightFileName) {
// The same file (e.g. in git diff), jump to the line number taken from the right editor.
// Warning: git show SHA^ vs SHA or git diff HEAD vs Index
// (when Working tree has changed in meantime) will not work properly.
for (const ChunkData &chunkData : fileData.chunks) {
int leftLineNumber = chunkData.leftStartingLineNumber;
int rightLineNumber = chunkData.rightStartingLineNumber;
for (int j = 0; j < chunkData.rows.count(); j++) {
const RowData rowData = chunkData.rows.at(j);
if (rowData.leftLine.textLineType == TextLineData::TextLine)
leftLineNumber++;
if (rowData.rightLine.textLineType == TextLineData::TextLine)
rightLineNumber++;
if (leftLineNumber == lineNumber) {
int colNr = rowData.equal ? columnNumber : 0;
m_controller.jumpToOriginalFile(leftFileName, rightLineNumber, colNr);
return;
}
}
}
} else {
// different file (e.g. in Tools | Diff...)
m_controller.jumpToOriginalFile(leftFileName, lineNumber, columnNumber);
}
}
void SideBySideDiffEditorWidget::slotRightJumpToOriginalFileRequested(
int diffFileIndex,
int lineNumber,
int columnNumber)
{
if (diffFileIndex < 0 || diffFileIndex >= m_controller.m_contextFileData.count())
return;
const FileData fileData = m_controller.m_contextFileData.at(diffFileIndex);
const QString fileName = fileData.rightFileInfo.fileName;
m_controller.jumpToOriginalFile(fileName, lineNumber, columnNumber);
}
void SideBySideDiffEditorWidget::slotLeftContextMenuRequested(QMenu *menu,
int fileIndex,
int chunkIndex)
{
menu->addSeparator();
m_controller.addCodePasterAction(menu, fileIndex, chunkIndex);
m_controller.addApplyAction(menu, fileIndex, chunkIndex);
m_controller.addExtraActions(menu, fileIndex, chunkIndex);
}
void SideBySideDiffEditorWidget::slotRightContextMenuRequested(QMenu *menu,
int fileIndex,
int chunkIndex)
{
menu->addSeparator();
m_controller.addCodePasterAction(menu, fileIndex, chunkIndex);
m_controller.addRevertAction(menu, fileIndex, chunkIndex);
m_controller.addExtraActions(menu, fileIndex, chunkIndex);
}
void SideBySideDiffEditorWidget::leftVSliderChanged()
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
m_rightEditor->verticalScrollBar()->setValue(m_leftEditor->verticalScrollBar()->value());
}
void SideBySideDiffEditorWidget::rightVSliderChanged()
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
m_leftEditor->verticalScrollBar()->setValue(m_rightEditor->verticalScrollBar()->value());
}
void SideBySideDiffEditorWidget::leftHSliderChanged()
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
if (m_horizontalSync)
m_rightEditor->horizontalScrollBar()->setValue(m_leftEditor->horizontalScrollBar()->value());
}
void SideBySideDiffEditorWidget::rightHSliderChanged()
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
if (m_horizontalSync)
m_leftEditor->horizontalScrollBar()->setValue(m_rightEditor->horizontalScrollBar()->value());
}
void SideBySideDiffEditorWidget::leftCursorPositionChanged()
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
handlePositionChange(m_leftEditor, m_rightEditor);
leftVSliderChanged();
leftHSliderChanged();
}
void SideBySideDiffEditorWidget::rightCursorPositionChanged()
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
handlePositionChange(m_rightEditor, m_leftEditor);
rightVSliderChanged();
rightHSliderChanged();
}
void SideBySideDiffEditorWidget::handlePositionChange(SideDiffEditorWidget *source, SideDiffEditorWidget *dest)
{
if (m_controller.m_ignoreCurrentIndexChange)
return;
const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
m_controller.m_ignoreCurrentIndexChange = true;
syncCursor(source, dest);
emit currentDiffFileIndexChanged(
source->fileIndexForBlockNumber(source->textCursor().blockNumber()));
m_controller.m_ignoreCurrentIndexChange = oldIgnore;
}
void SideBySideDiffEditorWidget::syncCursor(SideDiffEditorWidget *source, SideDiffEditorWidget *dest)
{
const int oldHSliderPos = dest->horizontalScrollBar()->value();
const QTextCursor sourceCursor = source->textCursor();
const int sourceLine = sourceCursor.blockNumber();
const int sourceColumn = sourceCursor.positionInBlock();
QTextCursor destCursor = dest->textCursor();
const QTextBlock destBlock = dest->document()->findBlockByNumber(sourceLine);
const int destColumn = qMin(sourceColumn, destBlock.length());
const int destPosition = destBlock.position() + destColumn;
destCursor.setPosition(destPosition);
dest->setTextCursor(destCursor);
dest->horizontalScrollBar()->setValue(oldHSliderPos);
}
} // namespace Internal
} // namespace DiffEditor
#include "sidebysidediffeditorwidget.moc"