beautifierplugin.cpp 18.9 KB
Newer Older
1
/****************************************************************************
Lorenz Haas's avatar
Lorenz Haas committed
2
**
3 4
** Copyright (C) 2016 Lorenz Haas
** Contact: https://www.qt.io/licensing/
Lorenz Haas's avatar
Lorenz Haas committed
5 6 7 8 9 10 11
**
** 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
12 13 14
** 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.
Lorenz Haas's avatar
Lorenz Haas committed
15
**
16 17 18 19 20 21 22
** 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.
Lorenz Haas's avatar
Lorenz Haas committed
23 24 25 26 27 28
**
****************************************************************************/

#include "beautifierplugin.h"

#include "beautifierconstants.h"
29 30
#include "generaloptionspage.h"
#include "generalsettings.h"
31

Lorenz Haas's avatar
Lorenz Haas committed
32 33 34 35 36 37 38 39
#include "artisticstyle/artisticstyle.h"
#include "clangformat/clangformat.h"
#include "uncrustify/uncrustify.h"

#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/coreconstants.h>
40
#include <coreplugin/editormanager/documentmodel.h>
Lorenz Haas's avatar
Lorenz Haas committed
41 42 43
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/messagemanager.h>
44
#include <cppeditor/cppeditorconstants.h>
Lorenz Haas's avatar
Lorenz Haas committed
45
#include <diffeditor/differ.h>
46 47
#include <projectexplorer/project.h>
#include <projectexplorer/projecttree.h>
48
#include <texteditor/convenience.h>
49 50 51
#include <texteditor/textdocument.h>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
52 53 54 55
#include <texteditor/texteditorconstants.h>
#include <utils/algorithm.h>
#include <utils/fileutils.h>
#include <utils/mimetypes/mimedatabase.h>
56
#include <utils/qtcassert.h>
Eike Ziller's avatar
Eike Ziller committed
57
#include <utils/runextensions.h>
58
#include <utils/synchronousprocess.h>
Lorenz Haas's avatar
Lorenz Haas committed
59

hjk's avatar
hjk committed
60
#include <QDir>
Lorenz Haas's avatar
Lorenz Haas committed
61
#include <QFileInfo>
62
#include <QFutureWatcher>
63
#include <QMenu>
Lorenz Haas's avatar
Lorenz Haas committed
64 65 66 67 68
#include <QPlainTextEdit>
#include <QProcess>
#include <QScrollBar>
#include <QTextBlock>

69 70
using namespace TextEditor;

Lorenz Haas's avatar
Lorenz Haas committed
71 72 73
namespace Beautifier {
namespace Internal {

74
FormatTask format(FormatTask task)
Lorenz Haas's avatar
Lorenz Haas committed
75
{
76 77
    task.error.clear();
    task.formattedData.clear();
Lorenz Haas's avatar
Lorenz Haas committed
78

79
    const QString executable = task.command.executable();
80
    if (executable.isEmpty())
81
        return task;
Lorenz Haas's avatar
Lorenz Haas committed
82

83
    switch (task.command.processing()) {
84 85
    case Command::FileProcessing: {
        // Save text to temporary file
86
        const QFileInfo fi(task.filePath);
87
        Utils::TempFileSaver sourceFile(QDir::tempPath() + "/qtc_beautifier_XXXXXXXX."
88 89
                                        + fi.suffix());
        sourceFile.setAutoRemove(true);
90
        sourceFile.write(task.sourceData.toUtf8());
91
        if (!sourceFile.finalize()) {
Takumi ASAKI's avatar
Takumi ASAKI committed
92
            task.error = BeautifierPlugin::tr("Cannot create temporary file \"%1\": %2.")
93 94
                    .arg(sourceFile.fileName()).arg(sourceFile.errorString());
            return task;
95 96 97
        }

        // Format temporary file
98
        QStringList options = task.command.options();
99 100 101
        options.replaceInStrings(QLatin1String("%file"), sourceFile.fileName());
        Utils::SynchronousProcess process;
        process.setTimeoutS(5);
102
        Utils::SynchronousProcessResponse response = process.runBlocking(executable, options);
103 104
        if (response.result != Utils::SynchronousProcessResponse::Finished) {
            task.error = QObject::tr("Failed to format: %1.").arg(response.exitMessage(executable, 5));
105
            return task;
106
        }
107
        const QString output = response.stdErr;
108
        if (!output.isEmpty())
109
            task.error = executable + QLatin1String(": ") + output;
110 111 112 113

        // Read text back
        Utils::FileReader reader;
        if (!reader.fetch(sourceFile.fileName(), QIODevice::Text)) {
Takumi ASAKI's avatar
Takumi ASAKI committed
114
            task.error = BeautifierPlugin::tr("Cannot read file \"%1\": %2.")
115 116
                    .arg(sourceFile.fileName()).arg(reader.errorString());
            return task;
117
        }
118
        task.formattedData = QString::fromUtf8(reader.data());
119
    }
120
    return task;
121 122 123

    case Command::PipeProcessing: {
        QProcess process;
124
        QStringList options = task.command.options();
125 126
        options.replaceInStrings("%filename", QFileInfo(task.filePath).fileName());
        options.replaceInStrings("%file", task.filePath);
127
        process.start(executable, options);
128
        if (!process.waitForStarted(3000)) {
Takumi ASAKI's avatar
Takumi ASAKI committed
129
            task.error = BeautifierPlugin::tr("Cannot call %1 or some other error occurred.")
130 131
                    .arg(executable);
            return task;
132
        }
133
        process.write(task.sourceData.toUtf8());
134
        process.closeWriteChannel();
135
        if (!process.waitForFinished(5000) && process.state() == QProcess::Running) {
136
            process.kill();
Takumi ASAKI's avatar
Takumi ASAKI committed
137
            task.error = BeautifierPlugin::tr("Cannot call %1 or some other error occurred. Timeout "
138 139 140
                                     "reached while formatting file %2.")
                    .arg(executable).arg(task.filePath);
            return task;
141
        }
142 143
        const QByteArray errorText = process.readAllStandardError();
        if (!errorText.isEmpty()) {
144 145 146
            task.error = QString::fromLatin1("%1: %2").arg(executable)
                    .arg(QString::fromUtf8(errorText));
            return task;
147 148
        }

149 150
        const bool addsNewline = task.command.pipeAddsNewline();
        const bool returnsCRLF = task.command.returnsCRLF();
151
        if (addsNewline || returnsCRLF) {
152
            task.formattedData = QString::fromUtf8(process.readAllStandardOutput());
153
            if (addsNewline)
154
                task.formattedData.remove(QRegExp("(\\r\\n|\\n)$"));
155
            if (returnsCRLF)
156
                task.formattedData.replace("\r\n", "\n");
157
            return task;
158
        }
159 160
        task.formattedData = QString::fromUtf8(process.readAllStandardOutput());
        return task;
Lorenz Haas's avatar
Lorenz Haas committed
161 162
    }
    }
163

164
    return task;
Lorenz Haas's avatar
Lorenz Haas committed
165 166
}

167
QString sourceData(TextEditorWidget *editor, int startPos, int endPos)
Lorenz Haas's avatar
Lorenz Haas committed
168
{
169 170 171 172
    return (startPos < 0)
            ? editor->toPlainText()
            : Convenience::textAt(editor->textCursor(), startPos, (endPos - startPos));
}
173

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
bool isAutoFormatApplicable(const QString &filePath, const QList<Utils::MimeType> &allowedMimeTypes)
{
    if (allowedMimeTypes.isEmpty())
        return true;

    const Utils::MimeDatabase mdb;
    const QList<Utils::MimeType> fileMimeTypes = mdb.mimeTypesForFileName(filePath);
    auto inheritedByFileMimeTypes = [&fileMimeTypes](const Utils::MimeType &mimeType){
        const QString name = mimeType.name();
        return Utils::anyOf(fileMimeTypes, [&name](const Utils::MimeType &fileMimeType){
            return fileMimeType.inherits(name);
        });
    };
    return Utils::anyOf(allowedMimeTypes, inheritedByFileMimeTypes);
}

190 191 192 193 194 195 196
bool BeautifierPlugin::initialize(const QStringList &arguments, QString *errorString)
{
    Q_UNUSED(arguments)
    Q_UNUSED(errorString)

    Core::ActionContainer *menu = Core::ActionManager::createMenu(Constants::MENU_ID);
    menu->menu()->setTitle(QCoreApplication::translate("Beautifier", Constants::OPTION_TR_CATEGORY));
197
    menu->setOnAllDisabledBehavior(Core::ActionContainer::Show);
198 199
    Core::ActionManager::actionContainer(Core::Constants::M_TOOLS)->addMenu(menu);

200 201 202 203 204 205
    m_tools << new ArtisticStyle::ArtisticStyle(this);
    m_tools << new ClangFormat::ClangFormat(this);
    m_tools << new Uncrustify::Uncrustify(this);

    QStringList toolIds;
    toolIds.reserve(m_tools.count());
206
    for (BeautifierAbstractTool *tool : m_tools) {
207
        toolIds << tool->id();
208 209
        tool->initialize();
        const QList<QObject *> autoReleasedObjects = tool->autoReleaseObjects();
210
        for (QObject *object : autoReleasedObjects)
211
            addAutoReleasedObject(object);
212
    }
213

214 215 216 217
    m_generalSettings = new GeneralSettings;
    auto settingsPage = new GeneralOptionsPage(m_generalSettings, toolIds, this);
    addAutoReleasedObject(settingsPage);

218
    updateActions();
219 220 221 222 223
    return true;
}

void BeautifierPlugin::extensionsInitialized()
{
224 225 226 227 228
    const Core::EditorManager *editorManager = Core::EditorManager::instance();
    connect(editorManager, &Core::EditorManager::currentEditorChanged,
            this, &BeautifierPlugin::updateActions);
    connect(editorManager, &Core::EditorManager::aboutToSave,
            this, &BeautifierPlugin::autoFormatOnSave);
229 230 231 232 233
}

ExtensionSystem::IPlugin::ShutdownFlag BeautifierPlugin::aboutToShutdown()
{
    return SynchronousShutdown;
234 235
}

236
void BeautifierPlugin::updateActions(Core::IEditor *editor)
237
{
238
    for (BeautifierAbstractTool *tool : m_tools)
239
        tool->updateActions(editor);
240 241
}

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
void BeautifierPlugin::autoFormatOnSave(Core::IDocument *document)
{
    if (!m_generalSettings->autoFormatOnSave())
        return;

    // Check that we are dealing with a cpp editor
    if (document->id() != CppEditor::Constants::CPPEDITOR_ID)
        return;
    const QString filePath = document->filePath().toString();

    if (!isAutoFormatApplicable(filePath, m_generalSettings->autoFormatMime()))
        return;

    // Check if file is contained in the current project (if wished)
    if (m_generalSettings->autoFormatOnlyCurrentProject()) {
        const ProjectExplorer::Project *pro = ProjectExplorer::ProjectTree::currentProject();
        if (!pro || !pro->files(ProjectExplorer::Project::SourceFiles).contains(filePath))
            return;
    }

    // Find tool to use by id and format file!
    const QString id = m_generalSettings->autoFormatTool();
    auto tool = std::find_if(m_tools.constBegin(), m_tools.constEnd(),
                             [&id](const BeautifierAbstractTool *t){return t->id() == id;});
    if (tool != m_tools.constEnd()) {
        const 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);
    }
}

278
void BeautifierPlugin::formatCurrentFile(const Command &command, int startPos, int endPos)
279
{
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
    if (TextEditorWidget *editor = TextEditorWidget::currentTextEditorWidget())
        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 BeautifierPlugin::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())
298
        return;
299 300 301
    checkAndApplyTask(format(FormatTask(editor, editor->textDocument()->filePath().toString(), sd,
                                        command, startPos, endPos)));
}
302

303 304 305 306 307 308 309 310 311 312
/**
 * Behaves like formatEditor except that the formatting is done asynchronously.
 */
void BeautifierPlugin::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())
313 314
        return;

315 316 317 318 319 320 321 322 323 324 325 326 327
    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())
            showError(tr("File was modified."));
        else
            checkAndApplyTask(watcher->result());
        watcher->deleteLater();
    });
    watcher->setFuture(Utils::runAsync(&format, FormatTask(editor, doc->filePath().toString(), sd,
                                                           command, startPos, endPos)));
}
328

329 330 331 332 333 334 335 336
/**
 * Checks the state of @a task and if the formatting was successful calls updateEditorText() with
 * the respective members of @a task.
 */
void BeautifierPlugin::checkAndApplyTask(const FormatTask &task)
{
    if (!task.error.isEmpty()) {
        showError(task.error);
337 338 339
        return;
    }

340 341 342 343 344
    if (task.formattedData.isEmpty()) {
        showError(tr("Could not format file %1.").arg(task.filePath));
        return;
    }

345 346 347 348 349 350
    QPlainTextEdit *textEditor = task.editor;
    if (!textEditor) {
        showError(tr("File %1 was closed.").arg(task.filePath));
        return;
    }

351 352
    const QString formattedData = (task.startPos < 0)
            ? task.formattedData
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
            : 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 BeautifierPlugin::updateEditorText(QPlainTextEdit *editor, const QString &text)
{
    const QString editorText = editor->toPlainText();
    if (editorText == text)
Lorenz Haas's avatar
Lorenz Haas committed
368 369
        return;

370 371 372
    // Calculate diff
    DiffEditor::Differ differ;
    const QList<DiffEditor::Diff> diff = differ.diff(editorText, text);
373

Lorenz Haas's avatar
Lorenz Haas committed
374 375 376
    // 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;
377
    QTextBlock block = editor->document()->firstBlock();
Lorenz Haas's avatar
Lorenz Haas committed
378
    while (block.isValid()) {
379
        if (const TextBlockUserData *userdata = static_cast<TextBlockUserData *>(block.userData())) {
Lorenz Haas's avatar
Lorenz Haas committed
380 381
            if (userdata->folded()) {
                foldedBlocks << block.blockNumber();
382
                TextDocumentLayout::doFoldOrUnfold(block, true);
Lorenz Haas's avatar
Lorenz Haas committed
383 384 385 386
            }
        }
        block = block.next();
    }
387
    editor->update();
Lorenz Haas's avatar
Lorenz Haas committed
388 389 390

    // Save the current viewport position of the cursor to ensure the same vertical position after
    // the formatted text has set to the editor.
391
    int absoluteVerticalCursorOffset = editor->cursorRect().y();
Lorenz Haas's avatar
Lorenz Haas committed
392 393

    // Update changed lines and keep track of the cursor position
394
    QTextCursor cursor = editor->textCursor();
Lorenz Haas's avatar
Lorenz Haas committed
395 396 397 398
    int charactersInfrontOfCursor = cursor.position();
    int newCursorPos = charactersInfrontOfCursor;
    cursor.beginEditBlock();
    cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
399
    for (const DiffEditor::Diff &d : diff) {
Lorenz Haas's avatar
Lorenz Haas committed
400 401 402 403 404 405 406 407 408 409
        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.
410 411
            if (d.text.contains('\n')) {
                const int newLineCount = d.text.count('\n');
Lorenz Haas's avatar
Lorenz Haas committed
412 413 414 415 416 417 418 419 420
                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;
421 422
        }

Lorenz Haas's avatar
Lorenz Haas committed
423 424 425 426 427 428 429 430 431 432 433 434
        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.
435 436
            if (d.text.contains('\n')) {
                const int newLineCount = d.text.count('\n');
Lorenz Haas's avatar
Lorenz Haas committed
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
                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;
452 453
        }

Lorenz Haas's avatar
Lorenz Haas committed
454 455 456 457 458 459 460 461 462
        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);
463
    editor->setTextCursor(cursor);
Lorenz Haas's avatar
Lorenz Haas committed
464 465

    // Adjust vertical scrollbar
466 467 468
    absoluteVerticalCursorOffset = editor->cursorRect().y() - absoluteVerticalCursorOffset;
    const double fontHeight = QFontMetrics(editor->document()->defaultFont()).height();
    editor->verticalScrollBar()->setValue(editor->verticalScrollBar()->value()
Lorenz Haas's avatar
Lorenz Haas committed
469 470
                                              + absoluteVerticalCursorOffset / fontHeight);
    // Restore folded blocks
471
    const QTextDocument *doc = editor->document();
472
    for (int blockId : foldedBlocks) {
473
        const QTextBlock block = doc->findBlockByNumber(qMax(0, blockId));
Lorenz Haas's avatar
Lorenz Haas committed
474
        if (block.isValid())
475
            TextDocumentLayout::doFoldOrUnfold(block, false);
Lorenz Haas's avatar
Lorenz Haas committed
476 477
    }

478
    editor->document()->setModified(true);
Lorenz Haas's avatar
Lorenz Haas committed
479 480 481 482
}

void BeautifierPlugin::showError(const QString &error)
{
483
    Core::MessageManager::write(tr("Error in Beautifier: %1").arg(error.trimmed()));
Lorenz Haas's avatar
Lorenz Haas committed
484 485
}

486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
QString BeautifierPlugin::msgCannotGetConfigurationFile(const QString &command)
{
    return tr("Cannot get configuration file for %1.").arg(command);
}

QString BeautifierPlugin::msgFormatCurrentFile()
{
    //: Menu entry
    return tr("Format Current File");
}

QString BeautifierPlugin::msgFormatSelectedText()
{
    //: Menu entry
    return tr("Format Selected Text");
}

QString BeautifierPlugin::msgCommandPromptDialogTitle(const QString &command)
{
    //: File dialog title for path chooser when choosing binary
    return tr("%1 Command").arg(command);
}

Lorenz Haas's avatar
Lorenz Haas committed
509 510
} // namespace Internal
} // namespace Beautifier