sourceformattercontroller.cpp 29.8 KB
Newer Older
1
/* This file is part of KDevelop
2
Copyright 2009 Andreas Pakulat <apaku@gmx.de>
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Copyright (C) 2008 Cédric Pasteur <cedric.pasteur@free.fr>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Library General Public License for more details.

You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB.  If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/

21
#include "sourceformattercontroller.h"
22

23
#include <QAction>
Kevin Funk's avatar
Kevin Funk committed
24
#include <QAbstractButton>
25
#include <QMimeDatabase>
26 27
#include <QRegExp>
#include <QStringList>
28
#include <QUrl>
29
#include <QPointer>
30 31 32 33 34 35 36 37 38

#include <KActionCollection>
#include <KIO/StoredTransferJob>
#include <KLocalizedString>
#include <KMessageBox>
#include <KTextEditor/Command>
#include <KTextEditor/Document>
#include <KTextEditor/Editor>
#include <KTextEditor/View>
39
#include <KParts/MainWindow>
40

41 42
#include <interfaces/context.h>
#include <interfaces/contextmenuextension.h>
43
#include <interfaces/icore.h>
44 45 46
#include <interfaces/idocument.h>
#include <interfaces/idocumentcontroller.h>
#include <interfaces/iplugincontroller.h>
47 48
#include <interfaces/iproject.h>
#include <interfaces/iprojectcontroller.h>
49
#include <interfaces/iruncontroller.h>
50
#include <interfaces/isession.h>
51
#include <interfaces/isourceformatter.h>
52
#include <interfaces/iuicontroller.h>
53 54
#include <language/codegen/coderepresentation.h>
#include <language/interfaces/ilanguagesupport.h>
55
#include <project/projectmodel.h>
56
#include <util/path.h>
57

58 59
#include "core.h"
#include "debug.h"
60
#include "plugincontroller.h"
61
#include "sourceformatterjob.h"
62
#include "textdocument.h"
63

64 65 66
namespace {

namespace Strings {
67
QString SourceFormatter() { return QStringLiteral("SourceFormatter"); }
68
QString UseDefault() { return QStringLiteral("UseDefault"); }
69 70 71 72
}

}

73 74
namespace KDevelop
{
75

76 77 78
class SourceFormatterControllerPrivate
{
public:
79 80
    // cache of formatter plugins, to avoid querying plugincontroller
    QVector<ISourceFormatter*> sourceFormatters;
81 82 83 84 85 86 87
    // GUI actions
    QAction* formatTextAction;
    QAction* formatFilesAction;
    QAction* formatLine;
    QList<KDevelop::ProjectBaseItem*> prjItems;
    QList<QUrl> urls;
    bool enabled = true;
88

89
    ISourceFormatter* formatterForConfigEntry(const QString& entry, const QString& mimename) const;
90 91
};

92
ISourceFormatter* SourceFormatterControllerPrivate::formatterForConfigEntry(const QString& entry, const QString& mimename) const
93 94 95 96 97 98 99
{
    QStringList formatterinfo = entry.split( QStringLiteral("||"), QString::SkipEmptyParts );

    if( formatterinfo.size() != 2 ) {
        qCDebug(SHELL) << "Broken formatting entry for mime:" << mimename << "current value:" << entry;
    }

100 101 102
    auto it = std::find_if(sourceFormatters.begin(), sourceFormatters.end(), [&](ISourceFormatter* iformatter) {
        return (iformatter->name() == formatterinfo.first());
    });
103

104
    return (it != sourceFormatters.end()) ? *it : nullptr;
105 106
}

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
QString SourceFormatterController::kateModeLineConfigKey()
{
    return QStringLiteral("ModelinesEnabled");
}

QString SourceFormatterController::kateOverrideIndentationConfigKey()
{
    return QStringLiteral("OverrideKateIndentation");
}

QString SourceFormatterController::styleCaptionKey()
{
    return QStringLiteral("Caption");
}

QString SourceFormatterController::styleContentKey()
{
    return QStringLiteral("Content");
}

QString SourceFormatterController::styleMimeTypesKey()
{
    return QStringLiteral("MimeTypes");
}

QString SourceFormatterController::styleSampleKey()
{
    return QStringLiteral("StyleSample");
}
136

137
SourceFormatterController::SourceFormatterController(QObject *parent)
138
    : ISourceFormatterController(parent)
139
    , d_ptr(new SourceFormatterControllerPrivate)
140
{
141 142
    Q_D(SourceFormatterController);

143
    setObjectName(QStringLiteral("SourceFormatterController"));
144
    setComponentName(QStringLiteral("kdevsourceformatter"), i18n("Source Formatter"));
145
    setXMLFile(QStringLiteral("kdevsourceformatter.rc"));
146

147
    if (Core::self()->setupFlags() & Core::NoUi) return;
Niko Sams's avatar
Niko Sams committed
148

149 150 151 152
    d->formatTextAction = actionCollection()->addAction(QStringLiteral("edit_reformat_source"));
    d->formatTextAction->setText(i18n("&Reformat Source"));
    d->formatTextAction->setToolTip(i18n("Reformat source using AStyle"));
    d->formatTextAction->setWhatsThis(i18n("Source reformatting functionality using <b>astyle</b> library."));
153
    d->formatTextAction->setEnabled(false);
154
    connect(d->formatTextAction, &QAction::triggered, this, &SourceFormatterController::beautifySource);
155

156 157 158 159
    d->formatLine = actionCollection()->addAction(QStringLiteral("edit_reformat_line"));
    d->formatLine->setText(i18n("Reformat Line"));
    d->formatLine->setToolTip(i18n("Reformat current line using AStyle"));
    d->formatLine->setWhatsThis(i18n("Source reformatting of line under cursor using <b>astyle</b> library."));
160
    d->formatLine->setEnabled(false);
161
    connect(d->formatLine, &QAction::triggered, this, &SourceFormatterController::beautifyLine);
162

163 164 165 166
    d->formatFilesAction = actionCollection()->addAction(QStringLiteral("tools_astyle"));
    d->formatFilesAction->setText(i18n("Reformat Files..."));
    d->formatFilesAction->setToolTip(i18n("Format file(s) using the current theme"));
    d->formatFilesAction->setWhatsThis(i18n("Formatting functionality using <b>astyle</b> library."));
167
    d->formatFilesAction->setEnabled(false);
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
168 169
    connect(d->formatFilesAction, &QAction::triggered,
            this, QOverload<>::of(&SourceFormatterController::formatFiles));
170

171 172 173 174 175 176

    connect(Core::self()->pluginController(), &IPluginController::pluginLoaded,
            this, &SourceFormatterController::pluginLoaded);
    connect(Core::self()->pluginController(), &IPluginController::unloadingPlugin,
            this, &SourceFormatterController::unloadingPlugin);

177 178
    // connect to both documentActivated & documentClosed,
    // otherwise we miss when the last document was closed
179
    connect(Core::self()->documentController(), &IDocumentController::documentActivated,
180 181 182
            this, &SourceFormatterController::updateFormatTextAction);
    connect(Core::self()->documentController(), &IDocumentController::documentClosed,
            this, &SourceFormatterController::updateFormatTextAction);
183 184

    qRegisterMetaType<QPointer<KDevelop::TextDocument>>();
185
    connect(Core::self()->documentController(), &IDocumentController::documentLoaded,
186 187 188 189 190
            // Use a queued connection, because otherwise the view is not yet fully set up
            // but wrap the document in a smart pointer to guard against crashes when it
            // gets deleted in the meantime
            this, [this](IDocument *doc) {
                const auto textDoc = QPointer<TextDocument>(dynamic_cast<TextDocument*>(doc));
191
                QMetaObject::invokeMethod(this, "documentLoaded", Qt::QueuedConnection, Q_ARG(QPointer<KDevelop::TextDocument>, textDoc));
192
            });
193
    connect(Core::self()->projectController(), &IProjectController::projectOpened, this, &SourceFormatterController::projectOpened);
194

195
    updateFormatTextAction();
196 197
}

198
void SourceFormatterController::documentLoaded(const QPointer<TextDocument>& doc)
199
{
200 201 202 203
    // NOTE: explicitly check this here to prevent crashes on shutdown
    //       when this slot gets called (note: delayed connection)
    //       but the text document was already destroyed
    //       there have been unit tests that failed due to that...
204
    if (!doc || !doc->textDocument()) {
205 206
        return;
    }
207 208 209
    const auto url = doc->url();
    const auto mime = QMimeDatabase().mimeTypeForUrl(url);
    adaptEditorIndentationMode(doc->textDocument(), formatterForUrl(url, mime), url);
210 211
}

212 213
void SourceFormatterController::projectOpened(const IProject* project)
{
214 215
    Q_D(SourceFormatterController);

216 217 218 219 220 221 222 223 224
    // Adapt the indentation mode if a project was just opened. Otherwise if a document
    // is loaded before its project, it might not have the correct indentation mode set.

    auto config = project->projectConfiguration()->group(Strings::SourceFormatter());
    if (!config.isValid() || config.readEntry(Strings::UseDefault(), true)) {
        return;
    }

    QHash<QString, ISourceFormatter*> formatters;
225 226
    const auto documents = ICore::self()->documentController()->openDocuments();
    for (const KDevelop::IDocument* doc : documents) {
227 228 229 230 231 232 233 234 235 236 237 238 239 240
        if (project->inProject(IndexedString(doc->url()))) {
            const QString mimename = QMimeDatabase().mimeTypeForUrl(doc->url()).name();
            auto it = formatters.find(mimename);
            if (it == formatters.end()) {
                const auto entry = config.readEntry(mimename, QString());
                it = formatters.insert(mimename, entry.isEmpty() ? nullptr : d->formatterForConfigEntry(entry, mimename));
            }
            if (it.value()) {
                adaptEditorIndentationMode(doc->textDocument(), it.value(), doc->url());
            }
        }
    }
}

241 242
void SourceFormatterController::pluginLoaded(IPlugin* plugin)
{
243 244
    Q_D(SourceFormatterController);

245
    auto* sourceFormatter = plugin->extension<ISourceFormatter>();
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263

    if (!sourceFormatter) {
        return;
    }

    d->sourceFormatters << sourceFormatter;

    resetUi();

    emit formatterLoaded(sourceFormatter);
    // with one plugin now added, hasFormatters turned to true, so report to listeners
    if (d->sourceFormatters.size() == 1) {
        emit hasFormattersChanged(true);
    }
}

void SourceFormatterController::unloadingPlugin(IPlugin* plugin)
{
264 265
    Q_D(SourceFormatterController);

266
    auto* sourceFormatter = plugin->extension<ISourceFormatter>();
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284

    if (!sourceFormatter) {
        return;
    }

    const int idx = d->sourceFormatters.indexOf(sourceFormatter);
    Q_ASSERT(idx != -1);
    d->sourceFormatters.remove(idx);

    resetUi();

    emit formatterUnloading(sourceFormatter);
    if (d->sourceFormatters.isEmpty()) {
        emit hasFormattersChanged(false);
    }
}


285
void SourceFormatterController::initialize()
286 287 288
{
}

289
SourceFormatterController::~SourceFormatterController()
290 291 292
{
}

Milian Wolff's avatar
Milian Wolff committed
293
ISourceFormatter* SourceFormatterController::formatterForUrl(const QUrl &url)
294
{
295
    QMimeType mime = QMimeDatabase().mimeTypeForUrl(url);
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
    return formatterForUrl(url, mime);
}

KConfigGroup SourceFormatterController::configForUrl(const QUrl& url) const
{
    auto core = KDevelop::Core::self();
    auto project = core->projectController()->findProjectForUrl(url);
    if (project) {
        auto config = project->projectConfiguration()->group(Strings::SourceFormatter());
        if (config.isValid() && !config.readEntry(Strings::UseDefault(), true)) {
            return config;
        }
    }

    return core->activeSession()->config()->group( Strings::SourceFormatter() );
311
}
312 313 314

KConfigGroup SourceFormatterController::sessionConfig() const
{
315
    return KDevelop::Core::self()->activeSession()->config()->group( Strings::SourceFormatter() );
316 317 318
}

KConfigGroup SourceFormatterController::globalConfig() const
319
{
320
    return KSharedConfig::openConfig()->group( Strings::SourceFormatter() );
321 322
}

323
ISourceFormatter* SourceFormatterController::findFirstFormatterForMimeType(const QMimeType& mime ) const
324
{
325 326
    Q_D(const SourceFormatterController);

327
    static QHash<QString, ISourceFormatter*> knownFormatters;
328 329 330
    const auto formatterIt = knownFormatters.constFind(mime.name());
    if (formatterIt != knownFormatters.constEnd())
        return *formatterIt;
331

332 333
    auto it = std::find_if(d->sourceFormatters.constBegin(), d->sourceFormatters.constEnd(),
                           [&](ISourceFormatter* iformatter) {
334
        QSharedPointer<SourceFormatter> formatter(createFormatterForPlugin(iformatter));
335 336 337 338 339 340 341 342
        return (formatter->supportedMimeTypes().contains(mime.name()));
    });

    ISourceFormatter* iformatter = (it != d->sourceFormatters.constEnd()) ? *it : nullptr;

    // cache result in any case
    knownFormatters.insert(mime.name(), iformatter);
    return iformatter;
343
}
344

345 346
static void populateStyleFromConfigGroup(SourceFormatterStyle* s, const KConfigGroup& stylegrp)
{
347 348 349 350
    s->setCaption( stylegrp.readEntry( SourceFormatterController::styleCaptionKey(), QString() ) );
    s->setContent( stylegrp.readEntry( SourceFormatterController::styleContentKey(), QString() ) );
    s->setMimeTypes( stylegrp.readEntry<QStringList>( SourceFormatterController::styleMimeTypesKey(), QStringList() ) );
    s->setOverrideSample( stylegrp.readEntry( SourceFormatterController::styleSampleKey(), QString() ) );
351 352 353 354
}

SourceFormatter* SourceFormatterController::createFormatterForPlugin(ISourceFormatter *ifmt) const
{
355
    auto* formatter = new SourceFormatter();
356 357 358
    formatter->formatter = ifmt;

    // Inserted a new formatter. Now fill it with styles
359 360
    const auto predefinedStyles = ifmt->predefinedStyles();
    for (const KDevelop::SourceFormatterStyle& style : predefinedStyles) {
361 362 363 364 365
        formatter->styles[ style.name() ] = new SourceFormatterStyle(style);
    }
    KConfigGroup grp = globalConfig();
    if( grp.hasGroup( ifmt->name() ) ) {
        KConfigGroup fmtgrp = grp.group( ifmt->name() );
366 367
        const auto subgroups = fmtgrp.groupList();
        for (const QString& subgroup : subgroups) {
368
            auto* s = new SourceFormatterStyle( subgroup );
369 370 371 372 373 374
            KConfigGroup stylegrp = fmtgrp.group( subgroup );
            populateStyleFromConfigGroup(s, stylegrp);
            formatter->styles[ s->name() ] = s;
        }
    }
    return formatter;
375 376
}

377
ISourceFormatter* SourceFormatterController::formatterForUrl(const QUrl& url, const QMimeType& mime)
378
{
379 380
    Q_D(SourceFormatterController);

381
    if (!d->enabled || !isMimeTypeSupported(mime)) {
382
        return nullptr;
383
    }
384 385

    const auto formatter = configForUrl(url).readEntry(mime.name(), QString());
386

387 388 389 390
    if( formatter.isEmpty() )
    {
        return findFirstFormatterForMimeType( mime );
    }
391

392
    return d->formatterForConfigEntry(formatter, mime.name());
393 394
}

395
bool SourceFormatterController::isMimeTypeSupported(const QMimeType& mime)
396
{
397 398 399 400
    if( findFirstFormatterForMimeType( mime ) ) {
        return true;
    }
    return false;
401 402
}

403
QString SourceFormatterController::indentationMode(const QMimeType& mime)
404
{
405 406 407 408
    if (mime.inherits(QStringLiteral("text/x-c++src")) || mime.inherits(QStringLiteral("text/x-chdr")) ||
        mime.inherits(QStringLiteral("text/x-c++hdr")) || mime.inherits(QStringLiteral("text/x-csrc")) ||
        mime.inherits(QStringLiteral("text/x-java")) || mime.inherits(QStringLiteral("text/x-csharp"))) {
        return QStringLiteral("cstyle");
409
    }
410
    return QStringLiteral("none");
411 412
}

Milian Wolff's avatar
Milian Wolff committed
413
QString SourceFormatterController::addModelineForCurrentLang(QString input, const QUrl& url, const QMimeType& mime)
414
{
415 416
    if( !isMimeTypeSupported(mime) )
        return input;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
417

418
    QRegExp kateModelineWithNewline(QStringLiteral("\\s*\\n//\\s*kate:(.*)$"));
Dāvis Mosāns's avatar
Dāvis Mosāns committed
419

420 421
    // If there already is a modeline in the document, adapt it while formatting, even
    // if "add modeline" is disabled.
422
    if (!configForUrl(url).readEntry(SourceFormatterController::kateModeLineConfigKey(), false) &&
423 424
            kateModelineWithNewline.indexIn( input ) == -1 )
        return input;
425

426
    ISourceFormatter* fmt = formatterForUrl(url, mime);
427
    ISourceFormatter::Indentation indentation = fmt->indentation(url);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
428

429 430
    if( !indentation.isValid() )
        return input;
Dāvis Mosāns's avatar
Dāvis Mosāns committed
431

432 433 434
    QString output;
    QTextStream os(&output, QIODevice::WriteOnly);
    QTextStream is(&input, QIODevice::ReadOnly);
435

436
    Q_ASSERT(fmt);
437

Dāvis Mosāns's avatar
Dāvis Mosāns committed
438

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
439
    QString modeline(QStringLiteral("// kate: ")
440
                   + QLatin1String("indent-mode ") + indentationMode(mime) + QLatin1String("; "));
441 442 443 444 445 446

    if(indentation.indentWidth) // We know something about indentation-width
        modeline.append(QStringLiteral("indent-width %1; ").arg(indentation.indentWidth));

    if(indentation.indentationTabWidth != 0) // We know something about tab-usage
    {
447 448
        const auto state = (indentation.indentationTabWidth == -1) ? QLatin1String("on") : QLatin1String("off");
        modeline += QLatin1String("replace-tabs ") + state + QLatin1String("; ");
449 450 451 452 453 454
        if(indentation.indentationTabWidth > 0)
            modeline.append(QStringLiteral("tab-width %1; ").arg(indentation.indentationTabWidth));
    }

    qCDebug(SHELL) << "created modeline: " << modeline << endl;

455
    QRegExp kateModeline(QStringLiteral("^\\s*//\\s*kate:(.*)$"));
456 457

    bool modelinefound = false;
458
    QRegExp knownOptions(QStringLiteral("\\s*(indent-width|space-indent|tab-width|indent-mode|replace-tabs)"));
459 460 461 462 463 464 465
    while (!is.atEnd()) {
        QString line = is.readLine();
        // replace only the options we care about
        if (kateModeline.indexIn(line) >= 0) { // match
            qCDebug(SHELL) << "Found a kate modeline: " << line << endl;
            modelinefound = true;
            QString options = kateModeline.cap(1);
466
            const QStringList optionList = options.split(QLatin1Char(';'), QString::SkipEmptyParts);
467 468

            os <<  modeline;
469
            for (QString s : optionList) {
470
                if (knownOptions.indexIn(s) < 0) { // unknown option, add it
471
                    if(s.startsWith(QLatin1Char(' ')))
472
                        s.remove(0, 1);
473 474 475 476 477 478 479 480 481 482 483 484
                    os << s << ";";
                    qCDebug(SHELL) << "Found unknown option: " << s << endl;
                }
            }
            os << endl;
        } else
            os << line << endl;
    }

    if (!modelinefound)
        os << modeline << endl;
    return output;
485 486
}

487 488 489 490
void SourceFormatterController::cleanup()
{
}

491
void SourceFormatterController::updateFormatTextAction()
492
{
493 494
    Q_D(SourceFormatterController);

495
    bool enabled = false;
496

497 498 499 500 501 502 503
    if (!d->sourceFormatters.isEmpty()) {
        IDocument* doc = KDevelop::ICore::self()->documentController()->activeDocument();
        if (doc) {
            QMimeType mime = QMimeDatabase().mimeTypeForUrl(doc->url());
            if (isMimeTypeSupported(mime))
                enabled = true;
        }
504
    }
505

506 507
    d->formatLine->setEnabled(enabled);
    d->formatTextAction->setEnabled(enabled);
508 509 510 511
}

void SourceFormatterController::beautifySource()
{
512
    IDocument* idoc = KDevelop::ICore::self()->documentController()->activeDocument();
513 514
    if (!idoc)
        return;
515 516 517 518 519
    KTextEditor::View* view = idoc->activeTextView();
    if (!view)
        return;
    KTextEditor::Document* doc = view->document();
    // load the appropriate formatter
520 521 522
    const auto url = idoc->url();
    const auto mime = QMimeDatabase().mimeTypeForUrl(url);
    ISourceFormatter* formatter = formatterForUrl(url, mime);
523
        if( !formatter ) {
Dāvis Mosāns's avatar
Dāvis Mosāns committed
524
            qCDebug(SHELL) << "no formatter available for" << mime.name();
525 526
            return;
        }
527

528
    // Ignore the modeline, as the modeline will be changed anyway
529
    adaptEditorIndentationMode(doc, formatter, url, true);
530

531
    bool has_selection = view->selection();
532

533 534
    if (has_selection) {
        QString original = view->selectionText();
535

536
        QString output = formatter->formatSource(view->selectionText(), url, mime,
537 538
                                                doc->text(KTextEditor::Range(KTextEditor::Cursor(0,0),view->selectionRange().start())),
                                                doc->text(KTextEditor::Range(view->selectionRange().end(), doc->documentRange().end())));
539

540
        //remove the final newline character, unless it should be there
541
        if (!original.endsWith(QLatin1Char('\n'))  && output.endsWith(QLatin1Char('\n')))
542 543
            output.resize(output.length() - 1);
        //there was a selection, so only change the part of the text related to it
Dāvis Mosāns's avatar
Dāvis Mosāns committed
544

545 546 547 548 549 550 551 552
        // We don't use KTextEditor::Document directly, because CodeRepresentation transparently works
        // around a possible tab-replacement incompatibility between kate and kdevelop
        DynamicCodeRepresentation::Ptr code( dynamic_cast<DynamicCodeRepresentation*>( KDevelop::createCodeRepresentation( IndexedString( doc->url() ) ).data() ) );
        Q_ASSERT( code );
        code->replace( view->selectionRange(), original, output );
    } else {
        formatDocument(idoc, formatter, mime);
    }
553 554
}

555 556
void SourceFormatterController::beautifyLine()
{
557 558 559 560 561 562 563 564 565
    KDevelop::IDocumentController *docController = KDevelop::ICore::self()->documentController();
    KDevelop::IDocument *doc = docController->activeDocument();
    if (!doc || !doc->isTextDocument())
        return;
    KTextEditor::Document *tDoc = doc->textDocument();
    KTextEditor::View* view = doc->activeTextView();
    if (!view)
        return;
    // load the appropriate formatter
566 567 568
    const auto url = doc->url();
    const auto mime = QMimeDatabase().mimeTypeForUrl(url);
    ISourceFormatter* formatter = formatterForUrl(url, mime);
569 570 571 572 573 574 575 576
    if( !formatter ) {
        qCDebug(SHELL) << "no formatter available for" << mime.name();
        return;
    }

    const KTextEditor::Cursor cursor = view->cursorPosition();
    const QString line = tDoc->line(cursor.line());
    const QString prev = tDoc->text(KTextEditor::Range(0, 0, cursor.line(), 0));
577
    const QString post = QLatin1Char('\n') + tDoc->text(KTextEditor::Range(KTextEditor::Cursor(cursor.line() + 1, 0), tDoc->documentEnd()));
578 579 580 581 582 583 584 585 586 587 588

    const QString formatted = formatter->formatSource(line, doc->url(), mime, prev, post);

    // We don't use KTextEditor::Document directly, because CodeRepresentation transparently works
    // around a possible tab-replacement incompatibility between kate and kdevelop
    DynamicCodeRepresentation::Ptr code(dynamic_cast<DynamicCodeRepresentation*>( KDevelop::createCodeRepresentation( IndexedString( doc->url() ) ).data() ) );
    Q_ASSERT( code );
    code->replace( KTextEditor::Range(cursor.line(), 0, cursor.line(), line.length()), line, formatted );

    // advance cursor one line
    view->setCursorPosition(KTextEditor::Cursor(cursor.line() + 1, 0));
589 590
}

591
void SourceFormatterController::formatDocument(KDevelop::IDocument* doc, ISourceFormatter* formatter, const QMimeType& mime)
592
{
593 594 595 596 597
    Q_ASSERT(doc);
    Q_ASSERT(formatter);

    qCDebug(SHELL) << "Running" << formatter->name() << "on" << doc->url();

598 599 600
    // We don't use KTextEditor::Document directly, because CodeRepresentation transparently works
    // around a possible tab-replacement incompatibility between kate and kdevelop
    CodeRepresentation::Ptr code = KDevelop::createCodeRepresentation( IndexedString( doc->url() ) );
601

602 603 604 605
    KTextEditor::Cursor cursor = doc->cursorPosition();
    QString text = formatter->formatSource(code->text(), doc->url(), mime);
    text = addModelineForCurrentLang(text, doc->url(), mime);
    code->setText(text);
Dāvis Mosāns's avatar
Dāvis Mosāns committed
606

607
    doc->setCursorPosition(cursor);
608 609
}

610 611
void SourceFormatterController::settingsChanged()
{
612 613
    const auto documents = ICore::self()->documentController()->openDocuments();
    for (KDevelop::IDocument* doc : documents) {
614 615
        adaptEditorIndentationMode(doc->textDocument(), formatterForUrl(doc->url()), doc->url());
    }
616 617
}

618
/**
619 620 621 622 623 624 625 626 627 628
* Kate commands:
* Use spaces for indentation:
*   "set-replace-tabs 1"
* Use tabs for indentation (eventually mixed):
*   "set-replace-tabs 0"
* Indent width:
* 	 "set-indent-width X"
* Tab width:
*   "set-tab-width X"
* */
629

630 631
void SourceFormatterController::adaptEditorIndentationMode(KTextEditor::Document *doc, ISourceFormatter *formatter,
                                                           const QUrl& url, bool ignoreModeline)
632
{
633
    if (!formatter || !configForUrl(url).readEntry(SourceFormatterController::kateOverrideIndentationConfigKey(), false) || !doc)
634 635
        return;

636
    qCDebug(SHELL) << "adapting mode for" << url;
637

638
    QRegExp kateModelineWithNewline(QStringLiteral("\\s*\\n//\\s*kate:(.*)$"));
639 640 641 642 643 644 645 646

    // modelines should always take precedence
    if( !ignoreModeline && kateModelineWithNewline.indexIn( doc->text() ) != -1 )
    {
        qCDebug(SHELL) << "ignoring because a kate modeline was found";
        return;
    }

647
    ISourceFormatter::Indentation indentation = formatter->indentation(url);
648 649 650
    if(indentation.isValid())
    {
        struct CommandCaller {
651
            explicit CommandCaller(KTextEditor::Document* _doc) : doc(_doc), editor(KTextEditor::Editor::instance()) {
652 653
                Q_ASSERT(editor);
            }
654
            void operator()(const QString& cmd) {
655 656 657 658
                KTextEditor::Command* command = editor->queryCommand( cmd );
                Q_ASSERT(command);
                QString msg;
                qCDebug(SHELL) << "calling" << cmd;
659 660
                const auto views = doc->views();
                for (KTextEditor::View* view : views) {
661
                    if (!command->exec(view, cmd, msg))
Kevin Funk's avatar
Kevin Funk committed
662
                        qCWarning(SHELL) << "setting indentation width failed: " << msg;
663
                }
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
            }

            KTextEditor::Document* doc;
            KTextEditor::Editor* editor;
        } call(doc);

        if( indentation.indentWidth ) // We know something about indentation-width
            call( QStringLiteral("set-indent-width %1").arg(indentation.indentWidth ) );

        if( indentation.indentationTabWidth != 0 ) // We know something about tab-usage
        {
            call( QStringLiteral("set-replace-tabs %1").arg( (indentation.indentationTabWidth == -1) ? 1 : 0 ) );
            if( indentation.indentationTabWidth > 0 )
                call( QStringLiteral("set-tab-width %1").arg(indentation.indentationTabWidth ) );
        }
    }else{
        qCDebug(SHELL) << "found no valid indentation";
    }
682 683
}

684 685
void SourceFormatterController::formatFiles()
{
686 687
    Q_D(SourceFormatterController);

688
    if (d->prjItems.isEmpty() && d->urls.isEmpty())
689 690 691 692
        return;

    //get a list of all files in this folder recursively
    QList<KDevelop::ProjectFolderItem*> folders;
693
    for (KDevelop::ProjectBaseItem* item : qAsConst(d->prjItems)) {
694 695 696 697 698
        if (!item)
            continue;
        if (item->folder())
            folders.append(item->folder());
        else if (item->file())
699
            d->urls.append(item->file()->path().toUrl());
700
        else if (item->target()) {
701 702 703 704
            const auto files = item->fileList();
            for (KDevelop::ProjectFileItem* f : files) {
                d->urls.append(f->path().toUrl());
            }
705 706 707 708 709
        }
    }

    while (!folders.isEmpty()) {
        KDevelop::ProjectFolderItem *item = folders.takeFirst();
710 711 712 713 714 715 716 717 718 719 720 721 722 723
        const auto folderList = item->folderList();
        for (KDevelop::ProjectFolderItem* f : folderList) {
            folders.append(f);
        }
        const auto targets = item->targetList();
        for (KDevelop::ProjectTargetItem* f : targets) {
            const auto childs = f->fileList();
            for (KDevelop::ProjectFileItem* child : childs) {
                d->urls.append(child->path().toUrl());
            }
        }
        const auto files = item->fileList();
        for (KDevelop::ProjectFileItem* f : files) {
            d->urls.append(f->path().toUrl());
724 725 726
        }
    }

727
    auto win = ICore::self()->uiController()->activeMainWindow()->window();
728 729 730 731 732 733 734 735 736 737

    QMessageBox msgBox(QMessageBox::Question, i18n("Reformat files?"),
                       i18n("Reformat all files in the selected folder?"),
                       QMessageBox::Ok|QMessageBox::Cancel, win);
    msgBox.setDefaultButton(QMessageBox::Cancel);
    auto okButton = msgBox.button(QMessageBox::Ok);
    okButton->setText(i18n("Reformat"));
    msgBox.exec();

    if (msgBox.clickedButton() == okButton) {
738
        auto formatterJob = new SourceFormatterJob(this);
739
        formatterJob->setFiles(d->urls);
740
        ICore::self()->runController()->registerJob(formatterJob);
741
    }
742 743
}

744
KDevelop::ContextMenuExtension SourceFormatterController::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
745
{
746 747
    Q_D(SourceFormatterController);

748 749
    Q_UNUSED(parent);

750
    KDevelop::ContextMenuExtension ext;
751 752
    d->urls.clear();
    d->prjItems.clear();
753

754 755 756 757
    if (d->sourceFormatters.isEmpty()) {
        return ext;
    }

758 759
    if (context->hasType(KDevelop::Context::EditorContext))
    {
760 761
        if (d->formatTextAction->isEnabled())
            ext.addAction(KDevelop::ContextMenuExtension::EditGroup, d->formatTextAction);
762
    } else if (context->hasType(KDevelop::Context::FileContext)) {
763
        auto* filectx = static_cast<KDevelop::FileContext*>(context);
764 765
        d->urls = filectx->urls();
        ext.addAction(KDevelop::ContextMenuExtension::EditGroup, d->formatFilesAction);
766 767
    } else if (context->hasType(KDevelop::Context::CodeContext)) {
    } else if (context->hasType(KDevelop::Context::ProjectItemContext)) {
768
        auto* prjctx = static_cast<KDevelop::ProjectItemContext*>(context);
769 770 771
        d->prjItems = prjctx->items();
        if (!d->prjItems.isEmpty()) {
            ext.addAction(KDevelop::ContextMenuExtension::ExtensionGroup, d->formatFilesAction);
772 773 774
        }
    }
    return ext;
775 776
}

777
SourceFormatterStyle SourceFormatterController::styleForUrl(const QUrl& url, const QMimeType& mime)
778
{
779
    const auto formatter = configForUrl(url).readEntry(mime.name(), QString()).split(QStringLiteral("||"), QString::SkipEmptyParts);
780 781 782 783 784 785 786 787 788 789 790
    if( formatter.count() == 2 )
    {
        SourceFormatterStyle s( formatter.at( 1 ) );
        KConfigGroup fmtgrp = globalConfig().group( formatter.at(0) );
        if( fmtgrp.hasGroup( formatter.at(1) ) ) {
            KConfigGroup stylegrp = fmtgrp.group( formatter.at(1) );
            populateStyleFromConfigGroup(&s, stylegrp);
        }
        return s;
    }
    return SourceFormatterStyle();
791 792
}

793 794
void SourceFormatterController::disableSourceFormatting(bool disable)
{
795 796
    Q_D(SourceFormatterController);

797
    d->enabled = !disable;
798 799 800 801
}

bool SourceFormatterController::sourceFormattingEnabled()
{
802 803
    Q_D(SourceFormatterController);

804
    return d->enabled;
805 806
}

807 808
bool SourceFormatterController::hasFormatters() const
{
809 810
    Q_D(const SourceFormatterController);

811 812 813 814 815
    return !d->sourceFormatters.isEmpty();
}

QVector<ISourceFormatter*> SourceFormatterController::formatters() const
{
816 817
    Q_D(const SourceFormatterController);

818 819 820 821 822
    return d->sourceFormatters;
}

void SourceFormatterController::resetUi()
{
823 824
    Q_D(SourceFormatterController);

825 826 827 828 829
    d->formatFilesAction->setEnabled(!d->sourceFormatters.isEmpty());

    updateFormatTextAction();
}

830
}