Community • 14. August 2024

Plugins entwickeln für PhpStorm am Beispiel von Mapbender

In diesem Beitrag widmen wir uns beispielhaft der Entwicklung eines Plugins für die integrierte Entwicklungsumgebung (IDE) PhpStorm, auf das Entwickler*innen des Mapbender zurückgreifen können, wenn zusätzliche Funktionen benötigt werden.

 

Voraussetzungen

Der Artikel beschreibt die Erstellung eines Plugins mit Kotlin. Die Kenntnis dieser Programmiersprache sowie des Gradle-Buildsystems ist daher hilfreich. Die Setup-Schritte orientieren sich an der offiziellen Dokumentation von JetBrains.

 

Zum Hintergrund

Die IDE (Integrierte Entwicklungsumgebung) ist die Anwendung, die Entwickler*innen in ihrem Arbeitsalltag täglich einsetzen. Eine IDE ist deutlich mehr als ein simpler Editor. Sie ist gleichzeitig Dateiverwaltung, Terminal, Fehleranalyse-Tool, Datenbankverwaltung und vieles mehr. Trotz der zahlreichen mitgelieferten Funktionen und bereits bestehenden Plugins, auf die Entwickler*innen zurückgreifen können, ergeben sich in Projekten immer wieder Fragestellungen, bei denen zur Arbeitserleichterung eine weitere Funktionalität benötigt wird. Für solche Fälle sind alle IDEs, die die WhereGroup einsetzt, erweiterbar.

Die gängigsten IDEs der WhereGroup sind Visual Studio Code und die Produkte von JetBrains, wie PyCharm für die Python-Entwicklung und PhpStorm für die PHP-Entwicklung.

 

Anforderungen an das PhpStorm-Plugin

Bei der Entwicklung des PhpStorm-Plugins soll eine hartkodierte Zeichenkette ausgewählt werden und nach dem Betätigen einer Tastenkombination in eine Übersetzungsdatei ausgelagert werden. Die Übersetzungen sollen in einem Dialogfenster eingegeben werden können.

 

1. Vorbereitung der Entwicklungsumgebung

Installieren Sie zunächst die Jetbrains-eigene Entwicklungsumgebung für Java IntelliJ IDEA, denn alle Jetbrains-Anwendungen sind in Java geschrieben und so müssen auch Plugins in Java oder einer JVM-Sprache wie Scala oder Kotlin erstellt werden. Somit kann das Plugin direkt aus IntelliJ heraus getestet werden. Die freie Community-Variante ist für die Plugin-Entwicklung ausreichend.

In einem nächsten Schritt ist die Installation der Erweiterung "Plugin Devkit" aus dem offiziellen Plugin-Verzeichnis erforderlich.

 

2. Neues Plugin-Projekt erstellen

Erzeugen Sie via File - New - New Project ein neues, leeres Projekt mit dem Typ "IDE Plugin", sobald IntelliJ IDEA vorbereitet ist.

Bereits beim Anlegen des Plugins sind einige Dateien vorkonfiguriert, einige davon müssen aber für das konkrete Projekt angepasst werden.

Bearbeiten Sie zunächst die Datei build.gradle.kts. In dieser Datei ist ein Block mit dem Namen intellij enthalten. In ihm kann konfiguriert werden, für welche IDEs das Plugin verfügbar sein soll. Für unser PhpStorm-Plugin tragen Sie dort bitte das Kürzel PS ein. Da Mapbender-Übersetzungen in yaml-Dateien gespeichert werden, benötigt das Plugin außerdem auch Unterstützung für dieses Dateiformat. Dies kann der IDE über einen entsprechenden Eintrag im Block intellij mitgeteilt werden.

intellij {
  version.set("2023.2.6")
  type.set("PS") // Target IDE Platform

  plugins.set(listOf("yaml"))
}

Im Bereich tasks kann außerdem der Pfad zu einer bestehenden PhpStorm-Installation hinzugefügt werden. Dadurch kann das neue Plugin mit gewohntem Erscheinungsbild und Konfiguration getestet werden.

tasks {
    runIde {
        // Absolute path to the installed targetIDE to use as IDE Development Instance
        ideDir.set(file("/home/thack/Applications/PhpStorm-222.4345.15"))
    }

    ...
}

Außerdem ist es hilfreich, in resources/META-INF/plugin.xml einige Meta-Informationen (wie z.B. eine Beschreibung sowie eine Kontaktadresse) hinzuzufügen.

Die Grundkonfiguration ist damit abgeschlossen.

Über die Gradle Task "Run Plugin" kann das Plugin nun gestartet werden. Es öffnet sich eine PhpStorm-Installation. Auch wenn das Plugin noch nicht arbeitet, empfehlen wir über die Einstellungen zu überprüfen, ob das Plugin tatsächlich installiert ist, bevor Sie für das Plugin eine neue Aktion registrieren (siehe Schritt 3).

 

3. Eine neue Aktion registrieren und einrichten

Ein Plugin kann grundsätzlich verschiedene Aufgaben durchführen. Zum Beispiel:

  • Syntax-Highlighting für eine eigene Programmiersprache bereitstellen
  • die Benutzeroberfläche der IDE modifizieren oder
  • Aktionen hinzufügen. Eine Aktion wird entweder durch ihren Aufruf in einem Menü oder durch ein Tastenkürzel ausgelöst.

Um eine weitere Aktion hinzuzufügen, wie in unserem Fall eine Zeichenkette in eine Übersetzungsdatei auszulagern, muss zunächst innerhalb des src/main/kotlin-Ordners eine neue Klasse angelegt werden, die von AnAction erbt:

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent

class ExtractTranslationAction : AnAction() {
    override fun update(event: AnActionEvent) {
        // Using the event, evaluate the context,
        // and enable or disable the action.
    }

    override fun actionPerformed(event: AnActionEvent) {
        // Using the event, implement an action.
        // For example, create and show a dialog.
    }

    override fun getActionUpdateThread(): ActionUpdateThread {
        return ActionUpdateThread.EDT
    }
}

Es erscheint zunächst eine Warnung, dass die Aktion noch nicht registriert wurde. Betätigen Sie die Tastenkombination Alt-Enter und es erscheint ein Fenster, in dem der Aktion ein Name und eine Tastenkombination zugewiesen werden kann. IntelliJ erweitert dann automatisch die Datei plugin.xml, in welcher auch nachträglich Änderungen gemacht werden können.

Die actionPerformed-Method ist im Code die relevanteste. Sie wird von der IDE aufgerufen, sobald die Aktion ausgelöst wird. Aus dem übergebenen Argument event kann der aktuelle Kontext ausgelesen werden – zum Beispiel das aktuell geöffnete Projekt, die Datei, die Programmiersprache (deren Syntax-Highlighting gerade aktiv ist) oder der Editor. Um den aktuell ausgewählten Text auszulesen kann der folgende Code benutzt werden:

override fun actionPerformed(event: AnActionEvent) {
    val editor = event.getData(CommonDataKeys.EDITOR) ?: return
    val primaryCaret = editor.caretModel.primaryCaret
    val selection = primaryCaret.selectedText.toString()

    println(selection)
}

Im Falle der Übersetzungen soll ein Dialog angezeigt werden, in dem der Übersetzungskey sowie die Übersetzung in den verschiedenen, vom Mapbender unterstützten, Sprachen eingegeben werden kann. In diesem Artikel wird eine stark vereinfachte Version dargestellt.

Dialoge werden in Jetbrains-Plugins über Javas Swing Framework erzeugt. Dafür muss zunächst eine neue Klasse erstellt werden, in der der Dialog konfiguriert wird.

In der vereinfachten Version enthält der Dialog ein Label, darunter ein Textfeld für den Übersetzungskey, und unter einem Trennstrich je ein Label sowie ein Textfeld pro Sprache. Der Einfachheit halber werden hier nur englisch und deutsch verwendet.

Die Buttons für "Ok" und "Abbrechen" werden vom DialogWrapper automatisch hinzugefügt. Man kann allerdings auf die Buttons zugreifen und einen Klick abfangen. Hier wird beispielsweise der Inhalt des Übersetzungskeys in der Konsole geloggt, wenn der Button geklickt wird.

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.panels.HorizontalBox
import com.intellij.ui.components.panels.VerticalLayout
import java.awt.Dimension
import javax.swing.*

class ExtractTranslationDialog(
    project: Project,
    private val selection: String,
) : DialogWrapper(project, true) {

    private val tfKey = JTextField("")

    init {
        init()

        getButton(okAction)?.addActionListener {
            println(tfKey.text)
        }
    }

    override fun getPreferredFocusedComponent(): JComponent {
        return tfKey
    }

    override fun createCenterPanel(): JComponent {
        val panel = JPanel(VerticalLayout(10))

        panel.add(JLabel("Übersetzungskey").apply {
            preferredSize = Dimension(100, 20)
            labelFor = tfKey
        })
        panel.add(tfKey)
        panel.add(JSeparator())

        arrayOf("en", "de").forEach { key ->
            val layout = HorizontalBox()
            val jLabel = JLabel(key)
            val value = JTextField(if (key == "de") selection else "")
            jLabel.labelFor = value
            jLabel.preferredSize = Dimension(50, 20)
            layout.add(jLabel)
            layout.add(value)
            panel.add(layout)
        }

        return panel
    }
}

Der Dialog muss in der Action jetzt nur noch instantiiert werden:

val project = event.getData(CommonDataKeys.PROJECT) ?: return
ExtractTranslationDialog(project, selection).show()

 

Export als installierbare Datei

Die Entwicklung des neuen PhpStorm-Plugins ist damit abgeschlossen. Wenn Sie es für andere Entwickler*innen als installierbare Datei bereitstellen möchten, muss noch der folgende Befehl ausgeführt werden:

./gradlew buildPlugin

 

Sofern keine Fehler auftreten, wird eine JAR-Datei im Verzeichnis build/libs gespeichert. Diese kann dann versendet werden und in PHPStorm via File - Settings - Plugins - Settings Icon - Install from disc (auch von anderen Entwicklern) in deren IDE installiert werden.

 

Fertigstellung

Damit das Plugin im Alltag nützlich ist, muss die Funktionalität noch erweitert werden. Das fertige Plugin, wie es innerhalb der WhereGroup eingesetzt wird, liest aus für welche Sprachen Übersetzungsdateien vorliegen, und listet diese im Dialog auf.

Nach Klick auf Enter werden die Übersetzungen in die entsprechenden Übersetzungs-YAML-Dateien automatisch hinzugefügt. Das spart viel Zeit, gerade beim Mapbender, der aktuell in 10 Sprachen lokalisiert ist.

Der Code des fertigen Plugins ist auf Github verfügbar. Dort ist auch ein Link zu einer JAR-Datei hinterlegt, die sich Entwickler*innen, die am Mapbender entwickeln, als Plugin installieren können. Selbstverständlich kann der Code aber auch als Basis für ein eigenes Plugin genutzt werden!

Weitere Beiträge, die Dich interessieren könnten:

Thorsten Hack

Thorsten Hack ist seit 2022 am Freiburger Standort der WhereGroup GmbH als Softwareentwickler beschäftigt. Er ist hauptverantwortlicher Entwickler für das OpenSource-Projekt Mapbender. Außerdem arbeitet er in diversen Kundenprojekten unter anderem mit PHP, JavaScript und Python.

Artikel teilen: