Beautifier: Move formatting tools to TextEditor

Formatting is moved from Beautifier plugin to formattexteditor.h/.cpp.
Diff and Differ classes are extracted from DiffEditor to Utils
to prevent extra TextEditor dependencies.

This change will make possible to use formatCurrentFile
and similar functions not only from Beautifier code.

Change-Id: Ic5ca668afe88f4e9376d49e6bd3594807172b0dd
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Ivan Donchevskii
2018-08-29 14:36:47 +02:00
parent 7777df42cf
commit 5c658ac968
26 changed files with 503 additions and 397 deletions

View File

@@ -41,7 +41,7 @@ publication by Neil Fraser: http://neil.fraser.name/writing/diff/
#include <QCoreApplication>
#include <QFutureInterfaceBase>
namespace DiffEditor {
namespace Utils {
static int commonPrefix(const QString &text1, const QString &text2)
{
@@ -1555,4 +1555,4 @@ QList<Diff> Differ::cleanupSemanticsLossless(const QList<Diff> &diffList)
return newDiffList;
}
} // namespace DiffEditor
} // namespace Utils

View File

@@ -25,7 +25,7 @@
#pragma once
#include "diffeditor_global.h"
#include "utils_global.h"
#include <QString>
QT_BEGIN_NAMESPACE
@@ -34,9 +34,9 @@ class QMap;
class QFutureInterfaceBase;
QT_END_NAMESPACE
namespace DiffEditor {
namespace Utils {
class DIFFEDITOR_EXPORT Diff
class QTCREATOR_UTILS_EXPORT Diff
{
public:
enum Command {
@@ -54,7 +54,7 @@ public:
static QString commandString(Command com);
};
class DIFFEDITOR_EXPORT Differ
class QTCREATOR_UTILS_EXPORT Differ
{
public:
enum DiffMode
@@ -114,4 +114,4 @@ private:
QFutureInterfaceBase *m_jobController = nullptr;
};
} // namespace DiffEditor
} // namespace Utils

View File

@@ -121,7 +121,8 @@ SOURCES += \
$$PWD/url.cpp \
$$PWD/filecrumblabel.cpp \
$$PWD/fixedsizeclicklabel.cpp \
$$PWD/removefiledialog.cpp
$$PWD/removefiledialog.cpp \
$$PWD/differ.cpp
win32:SOURCES += $$PWD/consoleprocess_win.cpp
else:SOURCES += $$PWD/consoleprocess_unix.cpp
@@ -258,7 +259,8 @@ HEADERS += \
$$PWD/linecolumn.h \
$$PWD/link.h \
$$PWD/fixedsizeclicklabel.h \
$$PWD/removefiledialog.h
$$PWD/removefiledialog.h \
$$PWD/differ.h
FORMS += $$PWD/filewizardpage.ui \
$$PWD/newclasswidget.ui \

View File

@@ -75,6 +75,8 @@ Project {
"detailsbutton.h",
"detailswidget.cpp",
"detailswidget.h",
"differ.cpp",
"differ.h",
"dropsupport.cpp",
"dropsupport.h",
"elfreader.cpp",

View File

@@ -44,12 +44,15 @@
#include <cppeditor/cppeditorconstants.h>
#include <projectexplorer/projecttree.h>
#include <projectexplorer/project.h>
#include <texteditor/formattexteditor.h>
#include <utils/fileutils.h>
#include <utils/hostosinfo.h>
#include <QAction>
#include <QMenu>
using namespace TextEditor;
namespace Beautifier {
namespace Internal {
namespace ArtisticStyle {
@@ -101,7 +104,7 @@ void ArtisticStyle::formatFile()
BeautifierPlugin::showError(BeautifierPlugin::msgCannotGetConfigurationFile(
tr(Constants::ArtisticStyle::DISPLAY_NAME)));
} else {
BeautifierPlugin::formatCurrentFile(command(cfgFileName));
formatCurrentFile(command(cfgFileName));
}
}

View File

@@ -46,7 +46,7 @@ public:
bool initialize() override;
QString id() const override;
void updateActions(Core::IEditor *editor) override;
Command command() const override;
TextEditor::Command command() const override;
bool isApplicable(const Core::IDocument *document) const override;
private:
@@ -54,7 +54,7 @@ private:
QAction *m_formatFile = nullptr;
ArtisticStyleSettings *m_settings;
QString configurationFile() const;
Command command(const QString &cfgFile) const;
TextEditor::Command command(const QString &cfgFile) const;
};
} // namespace ArtisticStyle

View File

@@ -5,7 +5,6 @@ HEADERS += \
beautifierabstracttool.h \
beautifierconstants.h \
beautifierplugin.h \
command.h \
configurationdialog.h \
configurationeditor.h \
configurationpanel.h \
@@ -27,7 +26,6 @@ HEADERS += \
SOURCES += \
abstractsettings.cpp \
beautifierplugin.cpp \
command.cpp \
configurationdialog.cpp \
configurationeditor.cpp \
configurationpanel.cpp \

View File

@@ -20,8 +20,6 @@ QtcPlugin {
"beautifierconstants.h",
"beautifierplugin.cpp",
"beautifierplugin.h",
"command.cpp",
"command.h",
"configurationdialog.cpp",
"configurationdialog.h",
"configurationdialog.ui",

View File

@@ -25,7 +25,7 @@
#pragma once
#include "command.h"
#include <texteditor/command.h>
#include <QObject>
@@ -53,7 +53,7 @@ public:
*
* @note The received command may be invalid.
*/
virtual Command command() const = 0;
virtual TextEditor::Command command() const = 0;
virtual bool isApplicable(const Core::IDocument *document) const = 0;
};

View File

@@ -42,9 +42,9 @@
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/messagemanager.h>
#include <cppeditor/cppeditorconstants.h>
#include <diffeditor/differ.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projecttree.h>
#include <texteditor/formattexteditor.h>
#include <texteditor/textdocument.h>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
@@ -72,127 +72,6 @@ using namespace TextEditor;
namespace Beautifier {
namespace Internal {
struct FormatTask
{
FormatTask(QPlainTextEdit *_editor, const QString &_filePath, const QString &_sourceData,
const Command &_command, int _startPos = -1, int _endPos = 0) :
editor(_editor),
filePath(_filePath),
sourceData(_sourceData),
command(_command),
startPos(_startPos),
endPos(_endPos) {}
QPointer<QPlainTextEdit> editor;
QString filePath;
QString sourceData;
Command command;
int startPos = -1;
int endPos = 0;
QString formattedData;
QString error;
};
FormatTask format(FormatTask task)
{
task.error.clear();
task.formattedData.clear();
const QString executable = task.command.executable();
if (executable.isEmpty())
return task;
switch (task.command.processing()) {
case Command::FileProcessing: {
// Save text to temporary file
const QFileInfo fi(task.filePath);
Utils::TempFileSaver sourceFile(Utils::TemporaryDirectory::masterDirectoryPath()
+ "/qtc_beautifier_XXXXXXXX."
+ fi.suffix());
sourceFile.setAutoRemove(true);
sourceFile.write(task.sourceData.toUtf8());
if (!sourceFile.finalize()) {
task.error = BeautifierPlugin::tr("Cannot create temporary file \"%1\": %2.")
.arg(sourceFile.fileName()).arg(sourceFile.errorString());
return task;
}
// Format temporary file
QStringList options = task.command.options();
options.replaceInStrings(QLatin1String("%file"), sourceFile.fileName());
Utils::SynchronousProcess process;
process.setTimeoutS(5);
Utils::SynchronousProcessResponse response = process.runBlocking(executable, options);
if (response.result != Utils::SynchronousProcessResponse::Finished) {
task.error = BeautifierPlugin::tr("Failed to format: %1.").arg(response.exitMessage(executable, 5));
return task;
}
const QString output = response.stdErr();
if (!output.isEmpty())
task.error = executable + QLatin1String(": ") + output;
// Read text back
Utils::FileReader reader;
if (!reader.fetch(sourceFile.fileName(), QIODevice::Text)) {
task.error = BeautifierPlugin::tr("Cannot read file \"%1\": %2.")
.arg(sourceFile.fileName()).arg(reader.errorString());
return task;
}
task.formattedData = QString::fromUtf8(reader.data());
}
return task;
case Command::PipeProcessing: {
QProcess process;
QStringList options = task.command.options();
options.replaceInStrings("%filename", QFileInfo(task.filePath).fileName());
options.replaceInStrings("%file", task.filePath);
process.start(executable, options);
if (!process.waitForStarted(3000)) {
task.error = BeautifierPlugin::tr("Cannot call %1 or some other error occurred.")
.arg(executable);
return task;
}
process.write(task.sourceData.toUtf8());
process.closeWriteChannel();
if (!process.waitForFinished(5000) && process.state() == QProcess::Running) {
process.kill();
task.error = BeautifierPlugin::tr("Cannot call %1 or some other error occurred. Timeout "
"reached while formatting file %2.")
.arg(executable).arg(task.filePath);
return task;
}
const QByteArray errorText = process.readAllStandardError();
if (!errorText.isEmpty()) {
task.error = QString::fromLatin1("%1: %2").arg(executable)
.arg(QString::fromUtf8(errorText));
return task;
}
task.formattedData = QString::fromUtf8(process.readAllStandardOutput());
if (task.command.pipeAddsNewline() && task.formattedData.endsWith('\n')) {
task.formattedData.chop(1);
if (task.formattedData.endsWith('\r'))
task.formattedData.chop(1);
}
if (task.command.returnsCRLF())
task.formattedData.replace("\r\n", "\n");
return task;
}
}
return task;
}
QString sourceData(TextEditorWidget *editor, int startPos, int endPos)
{
return (startPos < 0)
? editor->toPlainText()
: Utils::Text::textAt(editor->textCursor(), startPos, (endPos - startPos));
}
bool isAutoFormatApplicable(const Core::IDocument *document,
const QList<Utils::MimeType> &allowedMimeTypes)
{
@@ -215,17 +94,9 @@ public:
void updateActions(Core::IEditor *editor = nullptr);
void formatEditor(TextEditor::TextEditorWidget *editor, const Command &command,
int startPos = -1, int endPos = 0);
void formatEditorAsync(TextEditor::TextEditorWidget *editor, const Command &command,
int startPos = -1, int endPos = 0);
void checkAndApplyTask(const FormatTask &task);
void updateEditorText(QPlainTextEdit *editor, const QString &text);
void autoFormatOnSave(Core::IDocument *document);
QSharedPointer<GeneralSettings> m_generalSettings;
QHash<QObject*, QMetaObject::Connection> m_autoFormatConnections;
ArtisticStyle::ArtisticStyle artisticStyleBeautifier;
ClangFormat::ClangFormat clangFormatBeautifier;
@@ -304,221 +175,17 @@ void BeautifierPluginPrivate::autoFormatOnSave(Core::IDocument *document)
if (tool != m_tools.constEnd()) {
if (!(*tool)->isApplicable(document))
return;
const Command command = (*tool)->command();
const TextEditor::Command command = (*tool)->command();
if (!command.isValid())
return;
const QList<Core::IEditor *> editors = Core::DocumentModel::editorsForDocument(document);
if (editors.isEmpty())
return;
if (TextEditorWidget* widget = qobject_cast<TextEditorWidget *>(editors.first()->widget()))
formatEditor(widget, command);
TextEditor::formatEditor(widget, command);
}
}
void BeautifierPlugin::formatCurrentFile(const Command &command, int startPos, int endPos)
{
QTC_ASSERT(dd, return);
if (TextEditorWidget *editor = TextEditorWidget::currentTextEditorWidget())
dd->formatEditorAsync(editor, command, startPos, endPos);
}
/**
* Formats the text of @a editor using @a command. @a startPos and @a endPos specifies the range of
* the editor's text that will be formatted. If @a startPos is negative the editor's entire text is
* formatted.
*
* @pre @a endPos must be greater than or equal to @a startPos
*/
void BeautifierPluginPrivate::formatEditor(TextEditorWidget *editor, const Command &command,
int startPos, int endPos)
{
QTC_ASSERT(startPos <= endPos, return);
const QString sd = sourceData(editor, startPos, endPos);
if (sd.isEmpty())
return;
checkAndApplyTask(format(FormatTask(editor, editor->textDocument()->filePath().toString(), sd,
command, startPos, endPos)));
}
/**
* Behaves like formatEditor except that the formatting is done asynchronously.
*/
void BeautifierPluginPrivate::formatEditorAsync(TextEditorWidget *editor, const Command &command,
int startPos, int endPos)
{
QTC_ASSERT(startPos <= endPos, return);
const QString sd = sourceData(editor, startPos, endPos);
if (sd.isEmpty())
return;
QFutureWatcher<FormatTask> *watcher = new QFutureWatcher<FormatTask>;
const TextDocument *doc = editor->textDocument();
connect(doc, &TextDocument::contentsChanged, watcher, &QFutureWatcher<FormatTask>::cancel);
connect(watcher, &QFutureWatcherBase::finished, [this, watcher] {
if (watcher->isCanceled())
BeautifierPlugin::showError(BeautifierPlugin::tr("File was modified."));
else
checkAndApplyTask(watcher->result());
watcher->deleteLater();
});
watcher->setFuture(Utils::runAsync(&format, FormatTask(editor, doc->filePath().toString(), sd,
command, startPos, endPos)));
}
/**
* Checks the state of @a task and if the formatting was successful calls updateEditorText() with
* the respective members of @a task.
*/
void BeautifierPluginPrivate::checkAndApplyTask(const FormatTask &task)
{
if (!task.error.isEmpty()) {
BeautifierPlugin::showError(task.error);
return;
}
if (task.formattedData.isEmpty()) {
BeautifierPlugin::showError(BeautifierPlugin::tr("Could not format file %1.").arg(task.filePath));
return;
}
QPlainTextEdit *textEditor = task.editor;
if (!textEditor) {
BeautifierPlugin::showError(BeautifierPlugin::tr("File %1 was closed.").arg(task.filePath));
return;
}
const QString formattedData = (task.startPos < 0)
? task.formattedData
: QString(textEditor->toPlainText()).replace(
task.startPos, (task.endPos - task.startPos), task.formattedData);
updateEditorText(textEditor, formattedData);
}
/**
* Sets the text of @a editor to @a text. Instead of replacing the entire text, however, only the
* actually changed parts are updated while preserving the cursor position, the folded
* blocks, and the scroll bar position.
*/
void BeautifierPluginPrivate::updateEditorText(QPlainTextEdit *editor, const QString &text)
{
const QString editorText = editor->toPlainText();
if (editorText == text)
return;
// Calculate diff
DiffEditor::Differ differ;
const QList<DiffEditor::Diff> diff = differ.diff(editorText, text);
// Since QTextCursor does not work properly with folded blocks, all blocks must be unfolded.
// To restore the current state at the end, keep track of which block is folded.
QList<int> foldedBlocks;
QTextBlock block = editor->document()->firstBlock();
while (block.isValid()) {
if (const TextBlockUserData *userdata = static_cast<TextBlockUserData *>(block.userData())) {
if (userdata->folded()) {
foldedBlocks << block.blockNumber();
TextDocumentLayout::doFoldOrUnfold(block, true);
}
}
block = block.next();
}
editor->update();
// Save the current viewport position of the cursor to ensure the same vertical position after
// the formatted text has set to the editor.
int absoluteVerticalCursorOffset = editor->cursorRect().y();
// Update changed lines and keep track of the cursor position
QTextCursor cursor = editor->textCursor();
int charactersInfrontOfCursor = cursor.position();
int newCursorPos = charactersInfrontOfCursor;
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
for (const DiffEditor::Diff &d : diff) {
switch (d.command) {
case DiffEditor::Diff::Insert:
{
// Adjust cursor position if we do work in front of the cursor.
if (charactersInfrontOfCursor > 0) {
const int size = d.text.size();
charactersInfrontOfCursor += size;
newCursorPos += size;
}
// Adjust folded blocks, if a new block is added.
if (d.text.contains('\n')) {
const int newLineCount = d.text.count('\n');
const int number = cursor.blockNumber();
const int total = foldedBlocks.size();
for (int i = 0; i < total; ++i) {
if (foldedBlocks.at(i) > number)
foldedBlocks[i] += newLineCount;
}
}
cursor.insertText(d.text);
break;
}
case DiffEditor::Diff::Delete:
{
// Adjust cursor position if we do work in front of the cursor.
if (charactersInfrontOfCursor > 0) {
const int size = d.text.size();
charactersInfrontOfCursor -= size;
newCursorPos -= size;
// Cursor was inside the deleted text, so adjust the new cursor position
if (charactersInfrontOfCursor < 0)
newCursorPos -= charactersInfrontOfCursor;
}
// Adjust folded blocks, if at least one block is being deleted.
if (d.text.contains('\n')) {
const int newLineCount = d.text.count('\n');
const int number = cursor.blockNumber();
for (int i = 0, total = foldedBlocks.size(); i < total; ++i) {
if (foldedBlocks.at(i) > number) {
foldedBlocks[i] -= newLineCount;
if (foldedBlocks[i] < number) {
foldedBlocks.removeAt(i);
--i;
--total;
}
}
}
}
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, d.text.size());
cursor.removeSelectedText();
break;
}
case DiffEditor::Diff::Equal:
// Adjust cursor position
charactersInfrontOfCursor -= d.text.size();
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, d.text.size());
break;
}
}
cursor.endEditBlock();
cursor.setPosition(newCursorPos);
editor->setTextCursor(cursor);
// Adjust vertical scrollbar
absoluteVerticalCursorOffset = editor->cursorRect().y() - absoluteVerticalCursorOffset;
const double fontHeight = QFontMetrics(editor->document()->defaultFont()).height();
editor->verticalScrollBar()->setValue(editor->verticalScrollBar()->value()
+ absoluteVerticalCursorOffset / fontHeight);
// Restore folded blocks
const QTextDocument *doc = editor->document();
for (int blockId : foldedBlocks) {
const QTextBlock block = doc->findBlockByNumber(qMax(0, blockId));
if (block.isValid())
TextDocumentLayout::doFoldOrUnfold(block, false);
}
editor->document()->setModified(true);
}
void BeautifierPlugin::showError(const QString &error)
{
Core::MessageManager::write(tr("Error in Beautifier: %1").arg(error.trimmed()));

View File

@@ -25,9 +25,8 @@
#pragma once
#include "command.h"
#include <extensionsystem/iplugin.h>
#include <texteditor/command.h>
namespace Beautifier {
namespace Internal {
@@ -38,8 +37,6 @@ class BeautifierPlugin : public ExtensionSystem::IPlugin
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Beautifier.json")
public:
static void formatCurrentFile(const Command &command, int startPos = -1, int endPos = 0);
static QString msgCannotGetConfigurationFile(const QString &command);
static QString msgFormatCurrentFile();
static QString msgFormatSelectedText();

View File

@@ -42,6 +42,7 @@
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>
#include <cppeditor/cppeditorconstants.h>
#include <texteditor/formattexteditor.h>
#include <texteditor/texteditor.h>
#include <utils/algorithm.h>
#include <utils/fileutils.h>
@@ -50,6 +51,8 @@
#include <QMenu>
#include <QTextBlock>
using namespace TextEditor;
namespace Beautifier {
namespace Internal {
namespace ClangFormat {
@@ -114,13 +117,12 @@ void ClangFormat::updateActions(Core::IEditor *editor)
void ClangFormat::formatFile()
{
BeautifierPlugin::formatCurrentFile(command());
formatCurrentFile(command());
}
void ClangFormat::formatAtCursor()
{
const TextEditor::TextEditorWidget *widget
= TextEditor::TextEditorWidget::currentTextEditorWidget();
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
if (!widget)
return;
@@ -128,7 +130,7 @@ void ClangFormat::formatAtCursor()
if (tc.hasSelection()) {
const int offset = tc.selectionStart();
const int length = tc.selectionEnd() - offset;
BeautifierPlugin::formatCurrentFile(command(offset, length));
formatCurrentFile(command(offset, length));
} else {
// Pretend that the current line was selected.
// Note that clang-format will extend the range to the next bigger
@@ -136,13 +138,13 @@ void ClangFormat::formatAtCursor()
const QTextBlock block = tc.block();
const int offset = block.position();
const int length = block.length();
BeautifierPlugin::formatCurrentFile(command(offset, length));
formatCurrentFile(command(offset, length));
}
}
void ClangFormat::disableFormattingSelectedText()
{
TextEditor::TextEditorWidget *widget = TextEditor::TextEditorWidget::currentTextEditorWidget();
TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
if (!widget)
return;
@@ -172,8 +174,7 @@ void ClangFormat::disableFormattingSelectedText()
// The indentation of these markers might be undesired, so reformat.
// This is not optimal because two undo steps will be needed to remove the markers.
const int reformatTextLength = insertCursor.position() - selectionStartBlock.position();
BeautifierPlugin::formatCurrentFile(command(selectionStartBlock.position(),
reformatTextLength));
formatCurrentFile(command(selectionStartBlock.position(), reformatTextLength));
}
Command ClangFormat::command() const

View File

@@ -46,7 +46,7 @@ public:
QString id() const override;
bool initialize() override;
void updateActions(Core::IEditor *editor) override;
Command command() const override;
TextEditor::Command command() const override;
bool isApplicable(const Core::IDocument *document) const override;
private:
@@ -57,7 +57,7 @@ private:
QAction *m_formatRange = nullptr;
QAction *m_disableFormattingSelectedText = nullptr;
ClangFormatSettings *m_settings;
Command command(int offset, int length) const;
TextEditor::Command command(int offset, int length) const;
};
} // namespace ClangFormat

View File

@@ -44,12 +44,15 @@
#include <cppeditor/cppeditorconstants.h>
#include <projectexplorer/projecttree.h>
#include <projectexplorer/project.h>
#include <texteditor/formattexteditor.h>
#include <texteditor/texteditor.h>
#include <utils/fileutils.h>
#include <QAction>
#include <QMenu>
using namespace TextEditor;
namespace Beautifier {
namespace Internal {
namespace Uncrustify {
@@ -111,7 +114,7 @@ void Uncrustify::formatFile()
BeautifierPlugin::showError(BeautifierPlugin::msgCannotGetConfigurationFile(
tr(Constants::Uncrustify::DISPLAY_NAME)));
} else {
BeautifierPlugin::formatCurrentFile(command(cfgFileName));
formatCurrentFile(command(cfgFileName));
}
}
@@ -124,8 +127,7 @@ void Uncrustify::formatSelectedText()
return;
}
const TextEditor::TextEditorWidget *widget
= TextEditor::TextEditorWidget::currentTextEditorWidget();
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
if (!widget)
return;
@@ -141,7 +143,7 @@ void Uncrustify::formatSelectedText()
if (tc.positionInBlock() > 0)
tc.movePosition(QTextCursor::EndOfLine);
const int endPos = tc.position();
BeautifierPlugin::formatCurrentFile(command(cfgFileName, true), startPos, endPos);
formatCurrentFile(command(cfgFileName, true), startPos, endPos);
} else if (m_settings->formatEntireFileFallback()) {
formatFile();
}

View File

@@ -45,7 +45,7 @@ public:
bool initialize() override;
QString id() const override;
void updateActions(Core::IEditor *editor) override;
Command command() const override;
TextEditor::Command command() const override;
bool isApplicable(const Core::IDocument *document) const override;
private:
@@ -55,7 +55,7 @@ private:
QAction *m_formatRange = nullptr;
UncrustifySettings *m_settings;
QString configurationFile() const;
Command command(const QString &cfgFile, bool fragment = false) const;
TextEditor::Command command(const QString &cfgFile, bool fragment = false) const;
};
} // namespace Uncrustify

View File

@@ -11,7 +11,6 @@ HEADERS += \
diffeditorfactory.h \
diffeditorplugin.h \
diffeditorwidgetcontroller.h \
differ.h \
diffutils.h \
diffview.h \
selectabletexteditorwidget.h \
@@ -27,7 +26,6 @@ SOURCES += \
diffeditorfactory.cpp \
diffeditorplugin.cpp \
diffeditorwidgetcontroller.cpp \
differ.cpp \
diffutils.cpp \
diffview.cpp \
selectabletexteditorwidget.cpp \

View File

@@ -32,8 +32,6 @@ QtcPlugin {
"diffeditorplugin.h",
"diffeditorwidgetcontroller.cpp",
"diffeditorwidgetcontroller.h",
"differ.cpp",
"differ.h",
"diffutils.cpp",
"diffutils.h",
"diffview.cpp",

View File

@@ -29,7 +29,6 @@
#include "diffeditorcontroller.h"
#include "diffeditordocument.h"
#include "diffeditorfactory.h"
#include "differ.h"
#include <QAction>
#include <QFileDialog>
@@ -50,10 +49,12 @@
#include <texteditor/texteditor.h>
#include <utils/algorithm.h>
#include <utils/differ.h>
#include <utils/mapreduce.h>
#include <utils/qtcassert.h>
using namespace Core;
using namespace Utils;
namespace DiffEditor {
namespace Internal {

View File

@@ -24,15 +24,17 @@
****************************************************************************/
#include "diffutils.h"
#include "differ.h"
#include "texteditor/fontsettings.h"
#include <texteditor/fontsettings.h>
#include <utils/differ.h>
#include <QFutureInterfaceBase>
#include <QRegularExpression>
#include <QStringList>
#include <QTextStream>
using namespace Utils;
namespace DiffEditor {
static QList<TextLineData> assemblyRows(const QList<TextLineData> &lines,
@@ -103,7 +105,7 @@ static void handleDifference(const QString &text,
* The number of equalities on both lists must be the same.
*/
ChunkData DiffUtils::calculateOriginalData(const QList<Diff> &leftDiffList,
const QList<Diff> &rightDiffList)
const QList<Diff> &rightDiffList)
{
int i = 0;
int j = 0;

View File

@@ -36,9 +36,9 @@ QT_END_NAMESPACE
namespace TextEditor { class FontSettings; }
namespace DiffEditor {
namespace Utils { class Diff; }
class Diff;
namespace DiffEditor {
class DIFFEDITOR_EXPORT DiffFileInfo {
public:
@@ -128,8 +128,8 @@ public:
GitFormat = AddLevel | 0x2, // Add line 'diff ..' as git does
};
static ChunkData calculateOriginalData(const QList<Diff> &leftDiffList,
const QList<Diff> &rightDiffList);
static ChunkData calculateOriginalData(const QList<Utils::Diff> &leftDiffList,
const QList<Utils::Diff> &rightDiffList);
static FileData calculateContextData(const ChunkData &originalData,
int contextLineCount,
int joinChunkThreshold = 1);

View File

@@ -25,8 +25,7 @@
#include "command.h"
namespace Beautifier {
namespace Internal {
namespace TextEditor {
bool Command::isValid() const
{
@@ -83,5 +82,4 @@ void Command::setReturnsCRLF(bool returnsCRLF)
m_returnsCRLF = returnsCRLF;
}
} // namespace Internal
} // namespace Beautifier
} // namespace TextEditor

View File

@@ -25,13 +25,14 @@
#pragma once
#include "texteditor_global.h"
#include <QString>
#include <QStringList>
namespace Beautifier {
namespace Internal {
namespace TextEditor {
class Command
class TEXTEDITOR_EXPORT Command
{
public:
enum Processing {
@@ -64,5 +65,4 @@ private:
bool m_returnsCRLF = false;
};
} // namespace Internal
} // namespace Beautifier
} // namespace TextEditor

View File

@@ -0,0 +1,364 @@
/****************************************************************************
**
** Copyright (C) 2018 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 "formattexteditor.h"
#include "textdocument.h"
#include "textdocumentlayout.h"
#include "texteditor.h"
#include <coreplugin/messagemanager.h>
#include <utils/differ.h>
#include <utils/runextensions.h>
#include <utils/synchronousprocess.h>
#include <utils/temporarydirectory.h>
#include <utils/textutils.h>
#include <utils/qtcassert.h>
#include <QFileInfo>
#include <QFutureWatcher>
#include <QScrollBar>
#include <QTextBlock>
using namespace Utils;
namespace TextEditor {
void formatCurrentFile(const Command &command, int startPos, int endPos)
{
if (TextEditorWidget *editor = TextEditorWidget::currentTextEditorWidget())
formatEditorAsync(editor, command, startPos, endPos);
}
static QString sourceData(TextEditorWidget *editor, int startPos, int endPos)
{
return (startPos < 0)
? editor->toPlainText()
: Utils::Text::textAt(editor->textCursor(), startPos, (endPos - startPos));
}
static FormatTask format(FormatTask task)
{
task.error.clear();
task.formattedData.clear();
const QString executable = task.command.executable();
if (executable.isEmpty())
return task;
switch (task.command.processing()) {
case Command::FileProcessing: {
// Save text to temporary file
const QFileInfo fi(task.filePath);
Utils::TempFileSaver sourceFile(Utils::TemporaryDirectory::masterDirectoryPath()
+ "/qtc_beautifier_XXXXXXXX."
+ fi.suffix());
sourceFile.setAutoRemove(true);
sourceFile.write(task.sourceData.toUtf8());
if (!sourceFile.finalize()) {
task.error = QString(QT_TRANSLATE_NOOP("TextEditor",
"Cannot create temporary file \"%1\": %2."))
.arg(sourceFile.fileName(), sourceFile.errorString());
return task;
}
// Format temporary file
QStringList options = task.command.options();
options.replaceInStrings(QLatin1String("%file"), sourceFile.fileName());
Utils::SynchronousProcess process;
process.setTimeoutS(5);
Utils::SynchronousProcessResponse response = process.runBlocking(executable, options);
if (response.result != Utils::SynchronousProcessResponse::Finished) {
task.error = QString(QT_TRANSLATE_NOOP("TextEditor", "Failed to format: %1."))
.arg(response.exitMessage(executable, 5));
return task;
}
const QString output = response.stdErr();
if (!output.isEmpty())
task.error = executable + QLatin1String(": ") + output;
// Read text back
Utils::FileReader reader;
if (!reader.fetch(sourceFile.fileName(), QIODevice::Text)) {
task.error = QString(QT_TRANSLATE_NOOP("TextEditor", "Cannot read file \"%1\": %2."))
.arg(sourceFile.fileName(), reader.errorString());
return task;
}
task.formattedData = QString::fromUtf8(reader.data());
}
return task;
case Command::PipeProcessing: {
QProcess process;
QStringList options = task.command.options();
options.replaceInStrings("%filename", QFileInfo(task.filePath).fileName());
options.replaceInStrings("%file", task.filePath);
process.start(executable, options);
if (!process.waitForStarted(3000)) {
task.error = QString(QT_TRANSLATE_NOOP("TextEditor",
"Cannot call %1 or some other error occurred."))
.arg(executable);
return task;
}
process.write(task.sourceData.toUtf8());
process.closeWriteChannel();
if (!process.waitForFinished(5000) && process.state() == QProcess::Running) {
process.kill();
task.error = QString(QT_TRANSLATE_NOOP("TextEditor",
"Cannot call %1 or some other error occurred. Timeout "
"reached while formatting file %2."))
.arg(executable, task.filePath);
return task;
}
const QByteArray errorText = process.readAllStandardError();
if (!errorText.isEmpty()) {
task.error = QString::fromLatin1("%1: %2").arg(executable,
QString::fromUtf8(errorText));
return task;
}
task.formattedData = QString::fromUtf8(process.readAllStandardOutput());
if (task.command.pipeAddsNewline() && task.formattedData.endsWith('\n')) {
task.formattedData.chop(1);
if (task.formattedData.endsWith('\r'))
task.formattedData.chop(1);
}
if (task.command.returnsCRLF())
task.formattedData.replace("\r\n", "\n");
return task;
}
}
return task;
}
/**
* Sets the text of @a editor to @a text. Instead of replacing the entire text, however, only the
* actually changed parts are updated while preserving the cursor position, the folded
* blocks, and the scroll bar position.
*/
static void updateEditorText(QPlainTextEdit *editor, const QString &text)
{
const QString editorText = editor->toPlainText();
if (editorText == text)
return;
// Calculate diff
Differ differ;
const QList<Diff> diff = differ.diff(editorText, text);
// Since QTextCursor does not work properly with folded blocks, all blocks must be unfolded.
// To restore the current state at the end, keep track of which block is folded.
QList<int> foldedBlocks;
QTextBlock block = editor->document()->firstBlock();
while (block.isValid()) {
if (const TextBlockUserData *userdata = static_cast<TextBlockUserData *>(block.userData())) {
if (userdata->folded()) {
foldedBlocks << block.blockNumber();
TextDocumentLayout::doFoldOrUnfold(block, true);
}
}
block = block.next();
}
editor->update();
// Save the current viewport position of the cursor to ensure the same vertical position after
// the formatted text has set to the editor.
int absoluteVerticalCursorOffset = editor->cursorRect().y();
// Update changed lines and keep track of the cursor position
QTextCursor cursor = editor->textCursor();
int charactersInfrontOfCursor = cursor.position();
int newCursorPos = charactersInfrontOfCursor;
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
for (const Diff &d : diff) {
switch (d.command) {
case Diff::Insert:
{
// Adjust cursor position if we do work in front of the cursor.
if (charactersInfrontOfCursor > 0) {
const int size = d.text.size();
charactersInfrontOfCursor += size;
newCursorPos += size;
}
// Adjust folded blocks, if a new block is added.
if (d.text.contains('\n')) {
const int newLineCount = d.text.count('\n');
const int number = cursor.blockNumber();
const int total = foldedBlocks.size();
for (int i = 0; i < total; ++i) {
if (foldedBlocks.at(i) > number)
foldedBlocks[i] += newLineCount;
}
}
cursor.insertText(d.text);
break;
}
case Diff::Delete:
{
// Adjust cursor position if we do work in front of the cursor.
if (charactersInfrontOfCursor > 0) {
const int size = d.text.size();
charactersInfrontOfCursor -= size;
newCursorPos -= size;
// Cursor was inside the deleted text, so adjust the new cursor position
if (charactersInfrontOfCursor < 0)
newCursorPos -= charactersInfrontOfCursor;
}
// Adjust folded blocks, if at least one block is being deleted.
if (d.text.contains('\n')) {
const int newLineCount = d.text.count('\n');
const int number = cursor.blockNumber();
for (int i = 0, total = foldedBlocks.size(); i < total; ++i) {
if (foldedBlocks.at(i) > number) {
foldedBlocks[i] -= newLineCount;
if (foldedBlocks[i] < number) {
foldedBlocks.removeAt(i);
--i;
--total;
}
}
}
}
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, d.text.size());
cursor.removeSelectedText();
break;
}
case Diff::Equal:
// Adjust cursor position
charactersInfrontOfCursor -= d.text.size();
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, d.text.size());
break;
}
}
cursor.endEditBlock();
cursor.setPosition(newCursorPos);
editor->setTextCursor(cursor);
// Adjust vertical scrollbar
absoluteVerticalCursorOffset = editor->cursorRect().y() - absoluteVerticalCursorOffset;
const double fontHeight = QFontMetrics(editor->document()->defaultFont()).height();
editor->verticalScrollBar()->setValue(editor->verticalScrollBar()->value()
+ absoluteVerticalCursorOffset / fontHeight);
// Restore folded blocks
const QTextDocument *doc = editor->document();
for (int blockId : foldedBlocks) {
const QTextBlock block = doc->findBlockByNumber(qMax(0, blockId));
if (block.isValid())
TextDocumentLayout::doFoldOrUnfold(block, false);
}
editor->document()->setModified(true);
}
static void showError(const QString &error)
{
Core::MessageManager::write(
QString(QT_TRANSLATE_NOOP("TextEditor", "Error in text formatting: %1"))
.arg(error.trimmed()));
}
/**
* Checks the state of @a task and if the formatting was successful calls updateEditorText() with
* the respective members of @a task.
*/
void checkAndApplyTask(const FormatTask &task)
{
if (!task.error.isEmpty()) {
showError(task.error);
return;
}
if (task.formattedData.isEmpty()) {
showError(QString(QT_TRANSLATE_NOOP("TextEditor", "Could not format file %1.")).arg(
task.filePath));
return;
}
QPlainTextEdit *textEditor = task.editor;
if (!textEditor) {
showError(QString(QT_TRANSLATE_NOOP("TextEditor", "File %1 was closed.")).arg(
task.filePath));
return;
}
const QString formattedData = (task.startPos < 0)
? task.formattedData
: QString(textEditor->toPlainText()).replace(
task.startPos, (task.endPos - task.startPos), task.formattedData);
updateEditorText(textEditor, formattedData);
}
/**
* Formats the text of @a editor using @a command. @a startPos and @a endPos specifies the range of
* the editor's text that will be formatted. If @a startPos is negative the editor's entire text is
* formatted.
*
* @pre @a endPos must be greater than or equal to @a startPos
*/
void formatEditor(TextEditorWidget *editor, const Command &command, int startPos, int endPos)
{
QTC_ASSERT(startPos <= endPos, return);
const QString sd = sourceData(editor, startPos, endPos);
if (sd.isEmpty())
return;
checkAndApplyTask(format(FormatTask(editor, editor->textDocument()->filePath().toString(), sd,
command, startPos, endPos)));
}
/**
* Behaves like formatEditor except that the formatting is done asynchronously.
*/
void formatEditorAsync(TextEditorWidget *editor, const Command &command, int startPos, int endPos)
{
QTC_ASSERT(startPos <= endPos, return);
const QString sd = sourceData(editor, startPos, endPos);
if (sd.isEmpty())
return;
auto *watcher = new QFutureWatcher<FormatTask>;
const TextDocument *doc = editor->textDocument();
QObject::connect(doc, &TextDocument::contentsChanged, watcher, &QFutureWatcher<FormatTask>::cancel);
QObject::connect(watcher, &QFutureWatcherBase::finished, [watcher] {
if (watcher->isCanceled())
showError(QString(QT_TRANSLATE_NOOP("TextEditor", "File was modified.")));
else
checkAndApplyTask(watcher->result());
watcher->deleteLater();
});
watcher->setFuture(Utils::runAsync(&format, FormatTask(editor, doc->filePath().toString(), sd,
command, startPos, endPos)));
}
} // namespace TextEditor

View File

@@ -0,0 +1,67 @@
/****************************************************************************
**
** Copyright (C) 2018 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.
**
****************************************************************************/
#pragma once
#include "texteditor_global.h"
#include "command.h"
#include <QPlainTextEdit>
#include <QPointer>
namespace TextEditor {
class TextEditorWidget;
class TEXTEDITOR_EXPORT FormatTask
{
public:
FormatTask(QPlainTextEdit *_editor, const QString &_filePath, const QString &_sourceData,
const Command &_command, int _startPos = -1, int _endPos = 0) :
editor(_editor),
filePath(_filePath),
sourceData(_sourceData),
command(_command),
startPos(_startPos),
endPos(_endPos) {}
QPointer<QPlainTextEdit> editor;
QString filePath;
QString sourceData;
TextEditor::Command command;
int startPos = -1;
int endPos = 0;
QString formattedData;
QString error;
};
TEXTEDITOR_EXPORT void formatCurrentFile(const TextEditor::Command &command, int startPos = -1, int endPos = 0);
TEXTEDITOR_EXPORT void formatEditor(TextEditorWidget *editor, const TextEditor::Command &command,
int startPos = -1, int endPos = 0);
TEXTEDITOR_EXPORT void formatEditorAsync(TextEditorWidget *editor, const TextEditor::Command &command,
int startPos = -1, int endPos = 0);
} // namespace TextEditor

View File

@@ -101,7 +101,9 @@ SOURCES += texteditorplugin.cpp \
codeassist/keywordscompletionassist.cpp \
completionsettingspage.cpp \
commentssettings.cpp \
marginsettings.cpp
marginsettings.cpp \
formattexteditor.cpp \
command.cpp
HEADERS += texteditorplugin.h \
plaintexteditorfactory.h \
@@ -214,7 +216,9 @@ HEADERS += texteditorplugin.h \
blockrange.h \
completionsettingspage.h \
commentssettings.h \
textstyles.h
textstyles.h \
formattexteditor.h \
command.h
FORMS += \
displaysettingspage.ui \

View File

@@ -51,6 +51,8 @@ Project {
"colorschemeedit.cpp",
"colorschemeedit.h",
"colorschemeedit.ui",
"command.cpp",
"command.h",
"commentssettings.cpp",
"commentssettings.h",
"completionsettings.cpp",
@@ -76,6 +78,8 @@ Project {
"fontsettingspage.cpp",
"fontsettingspage.h",
"fontsettingspage.ui",
"formattexteditor.cpp",
"formattexteditor.h",
"helpitem.cpp",
"helpitem.h",
"highlighterutils.cpp",