Développement de plugins IntelliJ : Création d'un échafaudage de projet personnalisé

Pour améliorer le flux de travail lors de la création de nouveaux projets dans IntelliJ IDEA, il est possible de définir des échafaudages personnalisés. Cet article explique comment implémenter un module de création de projet sur mesure en utilisant l'API de plugins IntelliJ.

Approche d'implémentation

La méthode consiste à étendre la classe ModuleBuilder pour personnaliser le processus de création de projet. Cela inclut la définition d'un type de module, de panneaux de configuration et de fichiers modèles.

Création d'un constructeur de module personnalisé

Commencez par créer une classe qui hérite de ModuleBuilder. Voici un exemple refactorisé :

class MonModuleBuilder : ModuleBuilder() {
    private val configuration = ConfigurationProjet.getInstance()

    override fun getModuleType() = TypeModulePersonnalise()

    override fun setupRootModel(rootModel: ModifiableRootModel) {
        val chemin = configuration.cheminProjet
        val dossierSrc = File(chemin, "src")
        dossierSrc.mkdirs()
        val vfSource = LocalFileSystem.getInstance().refreshAndFindFileByPath(chemin)
        rootModel.addContentEntry(vfSource!!)

        val modele = FileTemplateManager.getInstance(rootModel.project)
            .getJ2eeTemplate(FabriqueModeleFichier.FICHIER_PLUGIN)
        val proprietes = Properties().apply {
            setProperty("nomProjet", configuration.nomProjet)
            setProperty("groupe", configuration.groupe)
            setProperty("artefact", configuration.artefact)
        }
        val contenu = modele.getText(proprietes)
        val fichierSortie = File(dossierSrc, FabriqueModeleFichier.FICHIER_PLUGIN)
        FileUtil.writeToFile(fichierSortie, contenu)

        val dossierResources = File(chemin, "resources")
        dossierResources.mkdir()
        val vfResources = LocalFileSystem.getInstance()
            .refreshAndFindFileByPath(dossierResources.absolutePath)!!
        rootModel.contentEntries.forEach { entry ->
            entry.addSourceFolder(vfResources, false)
        }
    }

    override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep? {
        settingsStep.context.projectName = configuration.nomProjet
        return super.modifyProjectTypeStep(settingsStep)
    }

    override fun createProject(name: String?, path: String?) =
        super.createProject(configuration.nomProjet, configuration.cheminProjet)

    override fun createWizardSteps(wizardContext: WizardContext, modulesProvider: ModulesProvider) =
        arrayOf(EtapeConfigurationSecondaire())

    override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?) =
        EtapeConfigurationPrincipale()

    override fun getIgnoredSteps(): MutableList<class modulewizardstep="">> =
        mutableListOf(ProjectSettingsStep::class.java)
}
</class>

Enregistrez cette classe dans le fichier plugin.xml :

<extensions defaultExtensionNs="com.intellij">
    <moduleBuilder builderClass="com.example.MonModuleBuilder"/>
</extensions>

Définition du type de module

Créez une classe pour spécifier le type de module, comme TypeModulePersonnalise :

class TypeModulePersonnalise(id: String = "MODULE_PERSONNALISE") : ModuleType<MonModuleBuilder>(id) {
    override fun createModuleBuilder() = MonModuleBuilder()
    override fun getName() = "Mon Module"
    override fun getDescription() = "Module pour projet personnalisé"
    override fun getNodeIcon(isOpened: Boolean) = IconesPlugin.ICON_MODULE
}

Configuration des panneaux de paramètres

Les étapes de configuration sont gérées par des classes héritant de ModuleWizardStep. Voici un exemple pour le premier panneau :

class EtapeConfigurationPrincipale : ModuleWizardStep() {
    private val donnees = DonneesSaisie()
    private val etatPersistant = ConfigurationProjet.getInstance()

    private val panneau = panel {
        indent {
            row("Nom du projet : ") {
                textField().bindText(donnees::nomProjet)
            }
            row("Chemin : ") {
                textFieldWithBrowseButton(
                    "",
                    ProjectManager.getInstance().defaultProject,
                    FileChooserDescriptorFactory.createSingleFolderDescriptor()
                ).bindText(donnees::chemin)
            }
            row("Groupe : ") {
                textField().bindText(donnees::groupe)
            }
            row("Artefact : ") {
                textField().bindText(donnees::artefact)
            }
        }
    }

    override fun getComponent() = panneau
    override fun updateDataModel() {
        panneau.apply()
        etatPersistant.nomProjet = donnees.nomProjet
        etatPersistant.cheminProjet = donnees.chemin
        etatPersistant.groupe = donnees.groupe
        etatPersistant.artefact = donnees.artefact
    }

    data class DonneesSaisie(
        var nomProjet: String = "",
        var chemin: String = "",
        var groupe: String = "",
        var artefact: String = ""
    )
}

Pour la persistance des données, utilisez un service d'application :

@Service
@State(name = "ConfigurationProjet", storages = [Storage("configuration-projet.xml")])
class ConfigurationProjet : PersistentStateComponent<ConfigurationProjet> {
    var nomProjet = ""
    var cheminProjet = ""
    var groupe = ""
    var artefact = ""
    var description = ""
    var auteur = ""
    var email = ""

    override fun getState() = this
    override fun loadState(etat: ConfigurationProjet) {
        XmlSerializerUtil.copyBean(etat, this)
    }

    companion object {
        fun getInstance(): ConfigurationProjet =
            ApplicationManager.getApplication().getService(ConfigurationProjet::class.java)
    }
}

Enregistrez-le dans plugin.xml :

<extensions defaultExtensionNs="com.intellij">
    <applicationService serviceImplementation="com.example.ConfigurationProjet"/>
</extensions>

Intégration des fichiers modèles

Pour gérer les fichiers modèles, créez une fabrique implémentant FileTemplateGroupDescriptorFactory :

class FabriqueModeleFichier : FileTemplateGroupDescriptorFactory {
    companion object {
        const val FICHIER_PLUGIN = "modele-plugin.xml"
    }

    override fun getFileTemplatesDescriptor(): FileTemplateGroupDescriptor {
        val groupe = FileTemplateGroupDescriptor("Mes Modèles", IconesPlugin.ICON_MODELE)
        groupe.addTemplate(FileTemplateDescriptor(FICHIER_PLUGIN, IconesPlugin.ICON_MODELE))
        return groupe
    }
}

Ajoutez le fichier modèle dans les ressources sous fileTemplates/j2ee/modele-plugin.xml.ft, puis déclarez la fabrqiue :

<extensions defaultExtensionNs="com.intellij">
    <fileTemplateGroup implementation="com.example.FabriqueModeleFichier"/>
</extensions>

Génération de la structure du projet

Dans setupRootModel, le constructeur de module crée les répertoires nécessaires, lit le modèle en utilisant les propriétés définies, et écrit le fichier résultant. Les dossiers source sont ensuite marqués explicitement via l'API du modèle de racine.

Personnalisation des étapes de l'assistant

La méthode getIgnoredSteps permet de supprimer les étapes par défaut de l'assistant de création de projet. Les étapes personnalisées sont ajoutées via createWizardSteps et getCustomOptionsStep.

Étiquettes: IntelliJ-IDEA kotlin ModuleBuilder FileTemplate PluginDevelopment

Publié le 18 juin à 17h52