Category Archives: Developers

Kate Scripting: Indentation

Kate Part in KDE4 supports the ECMAScript (JavaScript) language by using kjs. In KDE3 we had several hard-coded indenters in C++, the idea is to let scripts do all the indentation in KDE4.
How does it work? It is similar to vim: You simply create a script in the directory $KDEDIR/share/apps/katepart/jscript. An indentation script has to follow several rules:

  1. it must have a valid script header (the first line must include the string kate-script and indentation scripts must have the type: indentation)
  2. it must define some variables and functions

Whenever the user types a character, the flow in Kate Part works like this

  1. check the indentation script’s trigger characters, i.e. whether the script wants to indent code for the typed character
  2. if yes, call the indentation function
  3. the return value of the indentation function is an integer value representing the new indentation depth in spaces.

In the 3rd step there are 2 special cases for the return value:

  1. return value = -1: Kate keeps the indentation, that is it searches for the last non-empty line and uses its indentation for the current line
  2. return value < -1 tells Kate to do nothing

So how does a script look like exactly?
The name does not really matter, so let’s call it foobar.js:

/* kate-script
 * name: MyLanguage Indenter
 * license: LGPL
 * author: Foo Bar
 * version: 1
 * kate-version: 3.0
 * type: indentation
 *
 * optional bla bla here
 */

// specifies the characters which should trigger indent() beside the default '\n'
triggerCharacters = "{}";

// called for the triggerCharacters {} and
function indent(line, indentWidth, typedChar)
{
  // do calculations here
  // if typedChar is an empty string, the user hit enter/return

  // todo: Implement your indentation algorithms here.
  return -1; // keep indentation
}

More details on the header:

  • name [required]: the name will appear in Kate’s menus
  • license [optional]: not visible in gui, but should be specified in js-files. it is always better to have a defined license
  • author [optional]: name
  • version [optional]: recommended. an integer
  • kate-version [required]: the minimum required kate-version (e.g. for api changes)
  • type [required]: must be set to indentation

The only missing part is the API which Kate Part exports to access the document and the view. Right now, there is no API documentation, so you have to look at the code:

You will find, that the current document exports functions like

  • document.fileName()
  • document.isModified()
  • document.charAt(line, column)
  • etc…

The view exports functions like

  • view.cursorPosition()
  • view.hasSelection()
  • view.clearSelection()

That’s the boring part of this blog. The interesting one is unfortunately shorter: we are looking for contributors who want to write scripts or help in the C++ implementation :)

Writing a Kate Plugin

Introduction

First at all, why writing plugins for an editor ? Good question, and I hope I have a good answer: Because we want Kate to be small and all extended features not all users need should go into plugins (like CVS suppport, project managment, coffee cooking ;) Therefore Kate provides a quite full-featured plugin interface and interfaces to all important stuff in the Kate application (the documents, views, windows, sidebar …).

This tutorial is for people knowing the Qt/KDE libraries and will not describe how to compile, make … a kde/qt application/library in detail.

The “helloworld” plugin which is here described can be found in the “kdesdk” package of kde (located in kdesdk/kate/plugins/helloworld). A detailed description of the Kate API is available here..

Coding example: The Konsole plugin

This is one of standard plugins shipped with Kate which embeds the awesome KonsolePart into it to have a nice terminal emulator around.

Needed files for a plugin

Each plugin consists of at least three files which are needed:

  • a desktop file with some information about the plugin (correct syntax described later – named like kate”pluginname”.desktop
  • a XmlGui rc file named ui.rc
  • a library containing the plugin (named like kate”pluginname”plugin)

About the desktop file

The desktop file needs the syntax shown here and must be located in kde services directory with the name kate”yourpluginname”.desktop,

Example: katekonsoleplugin.desktop

[Desktop Entry]
Type=Service
ServiceTypes=Kate/Plugin
X-KDE-Library=katekonsoleplugin
X-Kate-Version=2.8
Name=Terminal tool view
Comment=Toolview embedding a terminal widget
X-Kate-MajorProfiles=Kate
X-Kate-MinorProfiles=*

The “X-Kate-Version=2.8″ line is important! Kate for KDE 4 and up won’t load your plugin unless the property is similar to the version with which you want to use the plugin!

To install this desktop file to KDE’s services directory, we’ll later add the following line to the CMakeLists.txt file:

install( FILES katekonsoleplugin.desktop  DESTINATION  ${SERVICES_INSTALL_DIR} )

About the xmlGUI rc file

The xmlgui resource file describes how the menu and toolbar actions appear in the Kate application. For more detailed examples look into other plugins. The ui.rc file for the katekonsoleplugin looks like this:

<!DOCTYPE kpartgui>
<gui name="katekonsole" library="katekonsoleplugin" version="3">
<MenuBar>
 <Menu name="tools"><text>&amp;Tools</text>
 <Action name="katekonsole_tools_pipe_to_terminal" group="tools_operations"/>
 <Action name="katekonsole_tools_sync" group="tools_operations"/>
 <Action name="katekonsole_tools_toggle_focus" group="tools_operations"/>
 </Menu>
</MenuBar>
</gui>

To automatically install the ui.rc file into the right directory later we add the following line to CMakeLists.txt:

install( FILES ui.rc  DESTINATION  ${DATA_INSTALL_DIR}/kate/plugins/katekonsole )

About plugin lib

Each Kate plugin is a library which Kate will dynamically load on demand via KLibLoader. The name of the library is kate”yourpluginname”plugin.so and should be installed into KDE’s plugin directory.

The complete result for the file CMakeLists.txt looks like this:

########### compile and link and install plugin library ###############
set(katekonsoleplugin_PART_SRCS kateconsole.cpp )
kde4_add_plugin(katekonsoleplugin ${katekonsoleplugin_PART_SRCS})
target_link_libraries(katekonsoleplugin  ${KDE4_KDEUI_LIBS} ${KDE4_KPARTS_LIBS} kateinterfaces )
install(TARGETS katekonsoleplugin  DESTINATION ${PLUGIN_INSTALL_DIR} )

########### install support files ###############
install( FILES ui.rc  DESTINATION  ${DATA_INSTALL_DIR}/kate/plugins/katekonsole )
install( FILES katekonsoleplugin.desktop  DESTINATION  ${SERVICES_INSTALL_DIR} )

Start the real coding ;)

Now we can start with the real coding, The Konsole plugin consists of 2 files, kateconsole.cpp and .h.

The plugin header file – kateconsole.h

#ifndef __KATE_CONSOLE_H__
#define __KATE_CONSOLE_H__

#include <kate/plugin.h>
#include <kate/mainwindow.h>
#include <kate/pluginconfigpageinterface.h>
#include <kurl.h>
#include <kxmlguiclient.h>

#include <kvbox.h>
#include <QList>

class QShowEvent;

namespace KParts
{
 class ReadOnlyPart;
}

namespace KateMDI
{
 }

class KateConsole;
class KateKonsolePluginView;

class KateKonsolePlugin: public Kate::Plugin, public Kate::PluginConfigPageInterface
{
 Q_OBJECT
 Q_INTERFACES(Kate::PluginConfigPageInterface)
 
 friend class KateKonsolePluginView;
 
 public:
 explicit KateKonsolePlugin( QObject* parent = 0, const QList<QVariant>& = QList<QVariant>() );
 virtual ~KateKonsolePlugin();

 Kate::PluginView *createView (Kate::MainWindow *mainWindow);

 // PluginConfigPageInterface
 uint configPages() const { return 1; };
 Kate::PluginConfigPage *configPage (uint number = 0, QWidget *parent = 0, const char *name = 0);
 QString configPageName (uint number = 0) const;
 QString configPageFullName (uint number = 0) const;
 KIcon configPageIcon (uint number = 0) const;

 void readConfig();

 QByteArray previousEditorEnv() {return m_previousEditorEnv;}
 
 private:
 QList<KateKonsolePluginView*> mViews;
 QByteArray m_previousEditorEnv;
};

class KateKonsolePluginView : public Kate::PluginView
{
 Q_OBJECT

 public:
 /**
 * Constructor.
 */
 KateKonsolePluginView (KateKonsolePlugin* plugin, Kate::MainWindow *mainWindow);

 /**
 * Virtual destructor.
 */
 ~KateKonsolePluginView ();

 void readConfig();

 private:
 KateKonsolePlugin *m_plugin;
 KateConsole *m_console;
};

/**
 * KateConsole
 * This class is used for the internal terminal emulator
 * It uses internally the konsole part, thx to konsole devs :)
 */
class KateConsole : public KVBox, public Kate::XMLGUIClient
{
 Q_OBJECT

 public:
 /**
 * construct us
 * @param mw main window
 * @param parent toolview
 */
 KateConsole (KateKonsolePlugin* plugin, Kate::MainWindow *mw, QWidget* parent);

 /**
 * destruct us
 */
 ~KateConsole ();

 void readConfig();

 /**
 * cd to dir
 * @param url given dir
 */
 void cd (const KUrl &url);

 /**
 * send given text to console
 * @param text commands for console
 */
 void sendInput( const QString& text );

 Kate::MainWindow *mainWindow()
 {
 return m_mw;
 }

 public Q_SLOTS:
 /**
 * pipe current document to console
 */
 void slotPipeToConsole ();

 /**
 * synchronize the konsole with the current document (cd to the directory)
 */
 void slotSync();
 /**
 * When syncing is done by the user, also show the terminal if it is hidden
 */
 void slotManualSync();

 private Q_SLOTS:
 /**
 * the konsole exited ;)
 * handle that, hide the dock
 */
 void slotDestroyed ();

 /**
 * construct console if needed
 */
 void loadConsoleIfNeeded();

 /**
 * set or clear focus as appropriate.
 */
 void slotToggleFocus();

 protected:
 /**
 * the konsole get shown
 * @param ev show event
 */
 void showEvent(QShowEvent *ev);

 private:
 /**
 * console part
 */
 KParts::ReadOnlyPart *m_part;

 /**
 * main window of this console
 */
 Kate::MainWindow *m_mw;

 /**
 * toolview for this console
 */
 QWidget *m_toolView;

 KateKonsolePlugin *m_plugin;
};

class KateKonsoleConfigPage : public Kate::PluginConfigPage {
 Q_OBJECT
 public:
 explicit KateKonsoleConfigPage( QWidget* parent = 0, KateKonsolePlugin *plugin = 0 );
 virtual ~KateKonsoleConfigPage()
 {}

 virtual void apply();
 virtual void reset();
 virtual void defaults()
 {}
 private:
 class QCheckBox *cbAutoSyncronize;
 class QCheckBox *cbSetEditor;
 KateKonsolePlugin *mPlugin;
};
#endif

// kate: space-indent on; indent-width 2; replace-tabs on;

The plugin source file – kateconsole.cpp

#include "kateconsole.h"
#include "kateconsole.moc"

#include <kicon.h>
#include <kiconloader.h>
#include <ktexteditor/document.h>
#include <ktexteditor/view.h>

#include <kde_terminal_interface.h>
#include <kshell.h>
#include <kparts/part.h>
#include <kaction.h>
#include <kactioncollection.h>
#include <KDialog>

#include <kurl.h>
#include <klibloader.h>
#include <klocale.h>
#include <kdebug.h>
#include <kmessagebox.h>
//Added by qt3to4:
#include <QShowEvent>
#include <QLabel>

#include <QCheckBox>
#include <QVBoxLayout>

#include <kpluginloader.h>
#include <kaboutdata.h>
#include <kpluginfactory.h>
#include <kauthorized.h>

K_PLUGIN_FACTORY(KateKonsoleFactory, registerPlugin<KateKonsolePlugin>();)
K_EXPORT_PLUGIN(KateKonsoleFactory(KAboutData("katekonsole","katekonsoleplugin",ki18n("Konsole"), "0.1", ki18n("Embedded Konsole"), KAboutData::License_LGPL_V2)) )

KateKonsolePlugin::KateKonsolePlugin( QObject* parent, const QList<QVariant>& ):
 Kate::Plugin ( (Kate::Application*)parent )
{
 m_previousEditorEnv=qgetenv("EDITOR");
 if (!KAuthorized::authorizeKAction("shell_access"))
 {
 KMessageBox::sorry(0, i18n ("You do not have enough karma to access a shell or terminal emulation"));
 }
}

KateKonsolePlugin::~KateKonsolePlugin()
{
 ::setenv( "EDITOR", m_previousEditorEnv.data(), 1 );
}

Kate::PluginView *KateKonsolePlugin::createView (Kate::MainWindow *mainWindow)
{
 KateKonsolePluginView *view = new KateKonsolePluginView (this, mainWindow);
 return view;
}

Kate::PluginConfigPage *KateKonsolePlugin::configPage (uint number, QWidget *parent, const char *name)
{
 Q_UNUSED(name)
 if (number != 0)
 return 0;
 return new KateKonsoleConfigPage(parent, this);
}

QString KateKonsolePlugin::configPageName (uint number) const
{
 if (number != 0) return QString();
 return i18n("Terminal");
}

QString KateKonsolePlugin::configPageFullName (uint number) const
{
 if (number != 0) return QString();
 return i18n("Terminal Settings");
}

KIcon KateKonsolePlugin::configPageIcon (uint number) const
{
 if (number != 0) return KIcon();
 return KIcon("utilities-terminal");
}

void KateKonsolePlugin::readConfig()
{
 foreach ( KateKonsolePluginView *view, mViews )
 view->readConfig();
}

KateKonsolePluginView::KateKonsolePluginView (KateKonsolePlugin* plugin, Kate::MainWindow *mainWindow)
 : Kate::PluginView (mainWindow),m_plugin(plugin)
{
 // init console
 QWidget *toolview = mainWindow->createToolView ("kate_private_plugin_katekonsoleplugin", Kate::MainWindow::Bottom, SmallIcon("utilities-terminal"), i18n("Terminal"));
 m_console = new KateConsole(m_plugin, mainWindow, toolview);
 
 // register this view
 m_plugin->mViews.append ( this );
}

KateKonsolePluginView::~KateKonsolePluginView ()
{
 // unregister this view
 m_plugin->mViews.removeAll (this);
 
 // cleanup, kill toolview + console
 QWidget *toolview = m_console->parentWidget();
 delete m_console;
 delete toolview;
}

void KateKonsolePluginView::readConfig()
{
 m_console->readConfig();
}

KateConsole::KateConsole (KateKonsolePlugin* plugin, Kate::MainWindow *mw, QWidget *parent)
 : KVBox (parent), Kate::XMLGUIClient(KateKonsoleFactory::componentData())
 , m_part (0)
 , m_mw (mw)
 , m_toolView (parent)
 , m_plugin(plugin)
{
 QAction* a = actionCollection()->addAction("katekonsole_tools_pipe_to_terminal");
 a->setIcon(KIcon("utilities-terminal"));
 a->setText(i18nc("@action", "&Pipe to Terminal"));
 connect(a, SIGNAL(triggered()), this, SLOT(slotPipeToConsole()));

 a = actionCollection()->addAction("katekonsole_tools_sync");
 a->setText(i18nc("@action", "S&ynchronize Terminal with Current Document"));
 connect(a, SIGNAL(triggered()), this, SLOT(slotManualSync()));

 a = actionCollection()->addAction("katekonsole_tools_toggle_focus");
 a->setIcon(KIcon("utilities-terminal"));
 a->setText(i18nc("@action", "&Focus Terminal"));
 connect(a, SIGNAL(triggered()), this, SLOT(slotToggleFocus()));

 m_mw->guiFactory()->addClient (this);

 readConfig();
}

KateConsole::~KateConsole ()
{ 
 m_mw->guiFactory()->removeClient (this);
 if (m_part)
 disconnect ( m_part, SIGNAL(destroyed()), this, SLOT(slotDestroyed()) );
}

void KateConsole::loadConsoleIfNeeded()
{
 if (m_part) return;

 if (!window() || !parentWidget()) return;
 if (!window() || !isVisibleTo(window())) return;

 KPluginFactory *factory = KPluginLoader("libkonsolepart").factory();

 if (!factory) return;

 m_part = static_cast<KParts::ReadOnlyPart *>(factory->create<QObject>(this, this));

 if (!m_part) return;

 // start the terminal
 qobject_cast<TerminalInterface*>(m_part)->showShellInDir( QString() );

 KGlobal::locale()->insertCatalog("konsole");

 setFocusProxy(m_part->widget());
 m_part->widget()->show();

 connect ( m_part, SIGNAL(destroyed()), this, SLOT(slotDestroyed()) );

 slotSync();
}

void KateConsole::slotDestroyed ()
{
 m_part = 0;

 // hide the dockwidget
 if (parentWidget())
 {
 m_mw->hideToolView (m_toolView);
 m_mw->centralWidget()->setFocus ();
 }
}

void KateConsole::showEvent(QShowEvent *)
{
 if (m_part) return;

 loadConsoleIfNeeded();
}

void KateConsole::cd (const KUrl &url)
{
 sendInput("cd " + KShell::quoteArg(url.path()) + '\n');
}

void KateConsole::sendInput( const QString& text )
{
 loadConsoleIfNeeded();

 if (!m_part) return;

 TerminalInterface *t = qobject_cast<TerminalInterface *>(m_part);

 if (!t) return;

 t->sendInput (text);
}

void KateConsole::slotPipeToConsole ()
{
 if (KMessageBox::warningContinueCancel
 (m_mw->window()
 , i18n ("Do you really want to pipe the text to the console? This will execute any contained commands with your user rights.")
 , i18n ("Pipe to Terminal?")
 , KGuiItem(i18n("Pipe to Terminal")), KStandardGuiItem::cancel(), "Pipe To Terminal Warning") != KMessageBox::Continue)
 return;

 KTextEditor::View *v = m_mw->activeView();

 if (!v)
 return;

 if (v->selection())
 sendInput (v->selectionText());
 else
 sendInput (v->document()->text());
}

void KateConsole::slotSync()
{
 if (m_mw->activeView() ) {
 if ( m_mw->activeView()->document()->url().isValid()
 && m_mw->activeView()->document()->url().isLocalFile() ) {
 cd(KUrl( m_mw->activeView()->document()->url().directory() ));
 } else if ( !m_mw->activeView()->document()->url().isEmpty() ) {
 sendInput( "### " + i18n("Sorry, can not cd into '%1'", m_mw->activeView()->document()->url().directory() ) + '\n' );
 }
 }
}

void KateConsole::slotManualSync()
{
 slotSync();
 if ( ! m_part || ! m_part->widget()->isVisible() )
 m_mw->showToolView( parentWidget() );
}
void KateConsole::slotToggleFocus()
{
 QAction *action = actionCollection()->action("katekonsole_tools_toggle_focus");
 if ( ! m_part ) {
 m_mw->showToolView( parentWidget() );
 action->setText( i18n("Defocus Terminal") );
 return; // this shows and focuses the konsole
 }

 if ( ! m_part ) return;

 if (m_part->widget()->hasFocus()) {
 if (m_mw->activeView())
 m_mw->activeView()->setFocus();
 action->setText( i18n("Focus Terminal") );
 } else {
 // show the view if it is hidden
 if (parentWidget()->isHidden())
 m_mw->showToolView( parentWidget() );
 else // should focus the widget too!
 m_part->widget()->setFocus( Qt::OtherFocusReason );
 action->setText( i18n("Defocus Terminal") );
 }
}

void KateConsole::readConfig()
{
 disconnect( m_mw, SIGNAL(viewChanged()), this, SLOT(slotSync()) );
 if ( KConfigGroup(KGlobal::config(), "Konsole").readEntry("AutoSyncronize", false) )
 connect( m_mw, SIGNAL(viewChanged()), SLOT(slotSync()) );
 
 
 if ( KConfigGroup(KGlobal::config(), "Konsole").readEntry("SetEditor", false) )
 ::setenv( "EDITOR", "kate -b",1);
 else
 ::setenv( "EDITOR", m_plugin->previousEditorEnv().data(), 1 );
}

KateKonsoleConfigPage::KateKonsoleConfigPage( QWidget* parent, KateKonsolePlugin *plugin )
 : Kate::PluginConfigPage( parent )
 , mPlugin( plugin )
{
 QVBoxLayout *lo = new QVBoxLayout( this );
 lo->setSpacing( KDialog::spacingHint() );

 cbAutoSyncronize = new QCheckBox( i18n("&Automatically synchronize the terminal with the current document when possible"), this );
 lo->addWidget( cbAutoSyncronize );
 cbSetEditor = new QCheckBox( i18n("Set &EDITOR environment variable to 'kate -b'"), this );
 lo->addWidget( cbSetEditor );
 QLabel *tmp = new QLabel(this);
 tmp->setText(i18n("Important: The document has to be closed to make the console application continue"));
 lo->addWidget(tmp);
 reset();
 lo->addStretch();
 connect( cbAutoSyncronize, SIGNAL(stateChanged(int)), SIGNAL(changed()) );
 connect( cbSetEditor, SIGNAL(stateChanged(int)), SIGNAL(changed()) );
}

void KateKonsoleConfigPage::apply()
{
 KConfigGroup config(KGlobal::config(), "Konsole");
 config.writeEntry("AutoSyncronize", cbAutoSyncronize->isChecked());
 config.writeEntry("SetEditor", cbSetEditor->isChecked());
 config.sync();
 mPlugin->readConfig();
}

void KateKonsoleConfigPage::reset()
{
 KConfigGroup config(KGlobal::config(), "Konsole");
 cbAutoSyncronize->setChecked(config.readEntry("AutoSyncronize", false));
 cbSetEditor->setChecked(config.readEntry("SetEditor", false));
}

// kate: space-indent on; indent-width 2; replace-tabs on;

How does the Plugin look & work after install ?

You first have to go to the Settings -> Configure Kate -> Kate -> Plugins in Kate and activate the Konsole  plugin.

More information ? Advanced plugin stuff ?

To get more information about the plugin interface and its advanced functions please look at the Kate API documentation and the KTextEditor API documentation or contact us on our mailing list!