kde-extraapps/kdevelop/formatters/customscript/customscript_plugin.cpp
2015-07-26 14:23:17 +03:00

561 lines
17 KiB
C++

/* This file is part of KDevelop
Copyright (C) 2008 Cédric Pasteur <cedric.pasteur@free.fr>
Copyright (C) 2011 David Nolden <david.nolden.kdevelop@art-master.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This program 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
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "customscript_plugin.h"
#include <KPluginLoader>
#include <KPluginFactory>
#include <KAboutData>
#include <QTextStream>
#include <QTemporaryFile>
#include <KDebug>
#include <KProcess>
#include <interfaces/icore.h>
#include <interfaces/isourceformattercontroller.h>
#include <interfaces/isourceformatter.h>
#include <memory>
#include <QDir>
#include <util/formattinghelpers.h>
#include <interfaces/iprojectcontroller.h>
#include <interfaces/iproject.h>
#include <KMessageBox>
#include <interfaces/iuicontroller.h>
#include <KParts/MainWindow>
#include <interfaces/ilanguagecontroller.h>
#include <interfaces/ilanguage.h>
#include <language/interfaces/ilanguagesupport.h>
using namespace KDevelop;
static QWeakPointer<CustomScriptPlugin> indentPluginSingleton;
K_PLUGIN_FACTORY(CustomScriptFactory, registerPlugin<CustomScriptPlugin>();)
K_EXPORT_PLUGIN(CustomScriptFactory(KAboutData("kdevcustomscript","kdevformatters", ki18n("Custom Script Formatter"), "0.2", ki18n("A formatter using custom scripts"), KAboutData::License_GPL)))
// Replaces ${KEY} in command with variables[KEY]
static QString replaceVariables( QString command, QMap<QString, QString> variables )
{
while( command.contains("${"))
{
int pos = command.indexOf("${");
int end = command.indexOf("}", pos+2);
if(end == -1)
break;
QString key = command.mid( pos+2, end-pos-2 );
if( variables.contains( key ) )
{
command.replace( pos, 1 + end - pos, variables[key] );
}else{
kDebug() << "found no variable while replacing in shell-command" << command << "key" << key << "available:" << variables;
command.replace( pos, 1 + end - pos, "" );
}
}
return command;
}
CustomScriptPlugin::CustomScriptPlugin(QObject *parent, const QVariantList&)
: IPlugin(CustomScriptFactory::componentData(), parent)
{
KDEV_USE_EXTENSION_INTERFACE(ISourceFormatter)
m_currentStyle = predefinedStyles().at(0);
indentPluginSingleton = this;
}
CustomScriptPlugin::~CustomScriptPlugin()
{
}
QString CustomScriptPlugin::name()
{
// This needs to match the X-KDE-PluginInfo-Name entry from the .desktop file!
return "kdevcustomscript";
}
QString CustomScriptPlugin::caption()
{
return "Custom Script Formatter";
}
QString CustomScriptPlugin::description()
{
return i18n("<b>Indent and Format Source Code.</b><br />"
"This plugin allows using powerful external formatting tools "
"that can be invoked through the command-line.<br />"
"For example, the <b>uncrustify</b>, <b>astyle</b> or <b>indent</b> "
"formatters can be used.<br />"
"The advantage of command-line formatters is that formatting configurations "
"can be easily shared by all team members, independent of their preferred IDE.");
}
QString CustomScriptPlugin::formatSourceWithStyle(SourceFormatterStyle style, const QString& text, const KUrl& url, const KMimeType::Ptr& /*mime*/, const QString& leftContext, const QString& rightContext)
{
KProcess proc;
QTextStream ios(&proc);
std::unique_ptr<QTemporaryFile> tmpFile;
if (style.content().isEmpty())
{
style = predefinedStyle(style.name());
if (style.content().isEmpty())
{
kWarning() << "Empty contents for style" << style.name() << "for indent plugin";
return text;
}
}
QString useText = text;
useText = leftContext + useText + rightContext;
QMap<QString, QString> projectVariables;
foreach(IProject* project, ICore::self()->projectController()->projects())
projectVariables[project->name()] = project->folder().toLocalFile();
QString command = style.content();
// Replace ${Project} with the project path
command = replaceVariables( command, projectVariables );
command.replace("$FILE", url.toLocalFile());
if(command.contains("$TMPFILE"))
{
tmpFile.reset(new QTemporaryFile(QDir::tempPath() + "/code"));
tmpFile->setAutoRemove(false);
if(tmpFile->open())
{
kDebug() << "using temporary file" << tmpFile->fileName();
command.replace("$TMPFILE", tmpFile->fileName());
QByteArray useTextArray = useText.toLocal8Bit();
if( tmpFile->write(useTextArray) != useTextArray.size() )
{
kWarning() << "failed to write text to temporary file";
return text;
}
}else{
kWarning() << "Failed to create a temporary file";
return text;
}
tmpFile->close();
}
kDebug() << "using shell command for indentation: " << command;
proc.setShellCommand(command);
proc.setOutputChannelMode(KProcess::OnlyStdoutChannel);
proc.start();
if(!proc.waitForStarted()) {
kDebug() << "Unable to start indent" << endl;
return text;
}
if(!tmpFile.get())
proc.write(useText.toLocal8Bit());
proc.closeWriteChannel();
if(!proc.waitForFinished()) {
kDebug() << "Process doesn't finish" << endl;
return text;
}
QString output;
if(tmpFile.get())
{
QFile f(tmpFile->fileName());
if( f.open(QIODevice::ReadOnly) )
{
output = QString::fromLocal8Bit(f.readAll());
}else{
kWarning() << "Failed opening the temporary file for reading";
return text;
}
}else{
output = ios.readAll();
}
if (output.isEmpty())
{
kWarning() << "indent returned empty text for style" << style.name() << style.content();
return text;
}
int tabWidth = 4;
if((!leftContext.isEmpty() || !rightContext.isEmpty()) && (text.contains(' ') || output.contains(' ')))
{
// If we have to do contex-matching with tabs, determine the correct tab-width so that the context
// can be matched correctly
Indentation indent = indentation(url);
if(indent.indentationTabWidth > 0)
tabWidth = indent.indentationTabWidth;
}
return KDevelop::extractFormattedTextFromContext(output, text, leftContext, rightContext, tabWidth);
}
QString CustomScriptPlugin::formatSource(const QString& text, const KUrl& url, const KMimeType::Ptr& mime, const QString& leftContext, const QString& rightContext)
{
return formatSourceWithStyle( KDevelop::ICore::self()->sourceFormatterController()->styleForMimeType( mime ), text, url, mime, leftContext, rightContext );
}
static QList<SourceFormatterStyle> stylesFromLanguagePlugins() {
QList<KDevelop::SourceFormatterStyle> styles;
for ( ILanguage* lang: ICore::self()->languageController()->loadedLanguages() ) {
SourceFormatterItemList languageStyles = lang->languageSupport()->sourceFormatterItems();
for ( const SourceFormatterStyleItem& item: languageStyles ) {
if ( item.engine == "customscript" ) {
styles << item.style;
}
}
}
return styles;
};
KDevelop::SourceFormatterStyle CustomScriptPlugin::predefinedStyle(const QString& name)
{
for ( auto langStyle: stylesFromLanguagePlugins() ) {
qDebug() << "looking at style from language with custom sample" << langStyle.description() << langStyle.overrideSample();
if ( langStyle.name() == name ) {
return langStyle;
}
}
SourceFormatterStyle result(name);
if (name == "GNU_indent_GNU")
{
result.setCaption(i18n("Gnu Indent: GNU"));
result.setContent("indent");
} else if (name == "GNU_indent_KR") {
result.setCaption(i18n("Gnu Indent: Kernighan & Ritchie"));
result.setContent("indent -kr");
} else if (name == "GNU_indent_orig") {
result.setCaption(i18n("Gnu Indent: Original Berkeley indent style"));
result.setContent("indent -orig");
} else if(name == "kdev_format_source") {
result.setCaption("KDevelop: kdev_format_source");
result.setContent("kdev_format_source $FILE $TMPFILE");
result.setUsePreview(false);
result.setDescription(i18n( "Description:<br />"
"<b>kdev_format_source</b> is a script bundled with KDevelop "
"which allows using fine-grained formatting rules by placing "
"meta-files called <b>format_sources</b> into the file-system.<br /><br />"
"Each line of the <b>format_sources</b> files defines a list of wildcards "
"followed by a colon and the used formatting-command.<br /><br />"
"The formatting-command should use <b>$TMPFILE</b> to reference the "
"temporary file to reformat.<br /><br />"
"Example:<br />"
"<b>*.cpp *.h : myformatter $TMPFILE</b><br />"
"This will reformat all files ending with <b>.cpp</b> or <b>.h</b> using "
"the custom formatting script <b>myformatter</b>.<br /><br />"
"Example: <br />"
"<b>subdir/* : uncrustify -l CPP -f $TMPFILE -c uncrustify.config -o $TMPFILE</b> <br />"
"This will reformat all files in subdirectory <b>subdir</b> using the <b>uncrustify</b> "
"tool with the config-file <b>uncrustify.config</b>." ));
}
result.setMimeTypes({
{"text/x-c++src", "C++"}, {"text/x-chdr", "C"},
{"text/x-c++hdr", "C++"}, {"text/x-csrc", "C"},
{"text/x-java", "Java"}, {"text/x-csharp", "C#"}
});
return result;
}
QList<KDevelop::SourceFormatterStyle> CustomScriptPlugin::predefinedStyles()
{
QList<KDevelop::SourceFormatterStyle> styles = stylesFromLanguagePlugins();
styles << predefinedStyle("kdev_format_source");
styles << predefinedStyle("GNU_indent_GNU");
styles << predefinedStyle("GNU_indent_KR");
styles << predefinedStyle("GNU_indent_orig");
return styles;
}
KDevelop::SettingsWidget* CustomScriptPlugin::editStyleWidget(const KMimeType::Ptr &mime)
{
Q_UNUSED(mime);
return new CustomScriptPreferences();
}
static QString formattingSample()
{
return
"// Formatting\n"
"void func(){\n"
"\tif(isFoo(a,b))\n"
"\tbar(a,b);\n"
"if(isFoo)\n"
"\ta=bar((b-c)*a,*d--);\n"
"if( isFoo( a,b ) )\n"
"\tbar(a, b);\n"
"if (isFoo) {isFoo=false;cat << isFoo <<endl;}\n"
"if(isFoo)DoBar();if (isFoo){\n"
"\tbar();\n"
"}\n"
"\telse if(isBar()){\n"
"\tannotherBar();\n"
"}\n"
"int var = 1;\n"
"int *ptr = &var;\n"
"int &ref = i;\n"
"\n"
"QList<int>::const_iterator it = list.begin();\n"
"}\n"
"namespace A {\n"
"namespace B {\n"
"void foo() {\n"
" if (true) {\n"
" func();\n"
" } else {\n"
" // bla\n"
" }\n"
"}\n"
"}\n"
"}\n";
}
static QString indentingSample()
{
return
"// Indentation\n"
"#define foobar(A)\\\n"
"{Foo();Bar();}\n"
"#define anotherFoo(B)\\\n"
"return Bar()\n"
"\n"
"namespace Bar\n"
"{\n"
"class Foo\n"
"{public:\n"
"Foo();\n"
"virtual ~Foo();\n"
"};\n"
"switch (foo)\n"
"{\n"
"case 1:\n"
"a+=1;\n"
"break;\n"
"case 2:\n"
"{\n"
"a += 2;\n"
" break;\n"
"}\n"
"}\n"
"if (isFoo)\n"
"{\n"
"bar();\n"
"}\n"
"else\n"
"{\n"
"anotherBar();\n"
"}\n"
"int foo()\n"
"\twhile(isFoo)\n"
"\t\t{\n"
"\t\t\t...\n"
"\t\t\tgoto error;\n"
"\t\t....\n"
"\t\terror:\n"
"\t\t\t...\n"
"\t\t}\n"
"\t}\n"
"fooArray[]={ red,\n"
"\tgreen,\n"
"\tdarkblue};\n"
"fooFunction(barArg1,\n"
"\tbarArg2,\n"
"\tbarArg3);\n";
}
QString CustomScriptPlugin::previewText(const SourceFormatterStyle& style, const KMimeType::Ptr& mime)
{
if ( ! style.overrideSample().isEmpty() ) {
return style.overrideSample();
}
return formattingSample() + "\n\n" + indentingSample();
}
QStringList CustomScriptPlugin::computeIndentationFromSample( const KUrl& url )
{
QStringList ret;
QList<ILanguage*> lang = ICore::self()->languageController()->languagesForUrl( url );
if( lang.isEmpty() )
return ret;
QString sample = lang[0]->languageSupport()->indentationSample();
QString formattedSample = formatSource( sample, url, KMimeType::findByUrl( url ), QString(), QString() );
QStringList lines = formattedSample.split( "\n" );
foreach( QString line, lines )
{
if( !line.isEmpty() && line[0].isSpace() )
{
QString indent;
foreach( QChar c, line )
{
if( c.isSpace() )
indent.push_back( c );
else
break;
}
if(!indent.isEmpty() && !ret.contains(indent))
ret.push_back(indent);
}
}
return ret;
}
CustomScriptPlugin::Indentation CustomScriptPlugin::indentation( const KUrl& url )
{
Indentation ret;
QStringList indent = computeIndentationFromSample( url );
if( indent.isEmpty() )
{
kDebug() << "failed extracting a valid indentation from sample for url" << url;
return ret; // No valid indentation could be extracted
}
if( indent[0].contains( ' ' ) )
ret.indentWidth = indent[0].count(' ');
if( !indent.join("").contains(' ') )
ret.indentationTabWidth = -1; // Tabs are not used for indentation
if( indent[0] == " " )
{
// The script indents with tabs-only
// The problem is that we don't know how
// wide a tab is supposed to be.
//
// We need indentation-width=tab-width
// to make the editor do tab-only formatting,
// so choose a random with of 4.
ret.indentWidth = 4;
ret.indentationTabWidth = 4;
}else if(ret.indentWidth) {
// Tabs are used for indentation, alongside with spaces
// Try finding out how many spaces one tab stands for.
// Do it by assuming a uniform indentation-step with each level.
for(int pos = 0; pos < indent.size(); ++pos)
{
if(indent[pos] == " " && pos >= 1)
{
// This line consists of only a tab.
int prevWidth = indent[pos-1].length();
int prevPrevWidth = (pos >= 2) ? indent[pos-2].length() : 0;
int step = prevWidth - prevPrevWidth;
kDebug() << "found in line " << pos << prevWidth << prevPrevWidth << step;
if(step > 0 && step <= prevWidth)
{
kDebug() << "Done";
ret.indentationTabWidth = prevWidth + step;
break;
}
}
}
}
kDebug() << "indent-sample" << "\"" + indent.join("\n") + "\"" << "extracted tab-width" << ret.indentationTabWidth << "extracted indentation width" << ret.indentWidth;
return ret;
}
void CustomScriptPreferences::updateTimeout()
{
const QString& text = indentPluginSingleton.data()->previewText ( m_style, KMimeType::Ptr() );
QString formatted = indentPluginSingleton.data()->formatSourceWithStyle ( m_style, text, KUrl(), KMimeType::Ptr() );
emit previewTextChanged ( formatted );
}
CustomScriptPreferences::CustomScriptPreferences()
{
m_updateTimer = new QTimer ( this );
m_updateTimer->setSingleShot ( true );
connect ( m_updateTimer, SIGNAL (timeout()), SLOT (updateTimeout()) );
m_vLayout = new QVBoxLayout ( this );
m_captionLabel = new QLabel;
m_vLayout->addWidget ( m_captionLabel );
m_vLayout->addSpacing ( 10 );
m_hLayout = new QHBoxLayout;
m_vLayout->addLayout ( m_hLayout );
m_commandLabel = new QLabel;
m_hLayout->addWidget ( m_commandLabel );
m_commandEdit = new QLineEdit;
m_hLayout->addWidget ( m_commandEdit );
m_commandLabel->setText ( i18n("Command: ") );
m_vLayout->addSpacing ( 10 );
m_bottomLabel = new QLabel;
m_vLayout->addWidget ( m_bottomLabel );
m_bottomLabel->setTextFormat ( Qt::RichText );
m_bottomLabel->setText (
i18n ( "<i>You can enter an arbitrary shell command.</i><br />"
"The unformatted source-code is reached to the command <br />"
"through the standard input, and the <br />"
"formatted result is read from the standard output.<br />"
"<br />"
"If you add <b>$TMPFILE</b> into the command, then <br />"
"a temporary file is used for transferring the code." ) );
connect ( m_commandEdit, SIGNAL (textEdited(QString)), SLOT (textEdited(QString)) );
m_vLayout->addSpacing ( 10 );
m_moreVariablesButton = new QPushButton( i18n("More Variables") );
connect( m_moreVariablesButton, SIGNAL(clicked(bool)), SLOT(moreVariablesClicked(bool)) );
m_vLayout->addWidget( m_moreVariablesButton );
}
void CustomScriptPreferences::load ( const KDevelop::SourceFormatterStyle& style )
{
m_style = style;
m_commandEdit->setText ( style.content() );
m_captionLabel->setText ( i18n ( "Style: %1", style.caption() ) );
updateTimeout();
}
QString CustomScriptPreferences::save()
{
return m_commandEdit->text();
}
void CustomScriptPreferences::moreVariablesClicked ( bool )
{
KMessageBox::information( ICore::self()->uiController()->activeMainWindow(),
i18n("<b>$TMPFILE</b> will be replaced with the path to a temporary file. <br />"
"The code will be written into the file, the temporary <br />"
"file will be substituted into that position, and the result <br />"
"will be read out of that file. <br />"
"<br />"
"<b>$FILE</b> will be replaced with the path of the original file. <br />"
"The contents of the file must not be modified, changes are allowed <br />"
"only in $TMPFILE.<br />"
"<br />"
"<b>${PROJECT_NAME}</b> will be replaced by the path of <br />"
"the currently open project with the matching name."
), i18n("Variable Replacements") );
}
#include "moc_customscript_plugin.cpp"
// kate: indent-mode cstyle; space-indent off; tab-width 4;