Mutate project in NewProjectWizardStep#setupProject(Project)

Answered

Hey,

For my plugin I'm also creating a template-based project generator using the moduleType and moduleBuilder entrypoints.

In the final step of my AbstractNewProjectWizardBuilder I'm delegating to my ModuleBuilder (which is not necessarily required, as I'm just calling commitModule - but that should be changed later):

@NotNull
    @Override
    protected NewProjectWizardStep createStep(@NotNull WizardContext wizardContext) {
        return UIWizardUtil.chain(
                new RootNewProjectWizardStep(wizardContext),
                LabyModNewProjectWizardInfoWizwardStep::new,
                NewProjectWizardBaseStepKt::newProjectWizardBaseStepWithoutGap,
                LabyModNewProjectWizardAddonWizardStep::new,
                s -> new AbstractNewProjectWizardStep(s) {
                    @Override
                    public void setupProject(@NotNull Project project) {
                        new LabyModModuleBuilder(s).commitModule(project, null);
                    }
                }
        );
    }

That LabyModModuleBuilder#commitModule(Project, ModifiableModuleModel) attempts to download a project template from GitHub using the tarball, extract that template and after that just modifies the build.gradle.kts and settings.gradle.kts with the user-provided input from the wizard. At the end, as the project is a Gradle project, I'm just trying to trigger an import of that module (as that does not happen automatically).

By using the debugger I can definitely tell, that the mutators and stuff are indeed called. The changes are not written into the files itself, and the import is party broken - intelliJ IDEA attempts to import the gradle project but fails after a while and ends in a kinda-important-kinda-broken state. The error com.intellij.serviceContainer.AlreadyDisposedException: Already disposed: Module: 'IdeaProjects.test-addon' (disposed) is thrown quite a bit, so i guess that's the culprit in that case - tho i can't reconstruct based on what events the module is / was disposed.

My ModuleBuilder with all the named logic:


public class LabyModModuleBuilder extends ModuleBuilder {

    private final LabyModNewProjectWizardBuilder.LabyModNewProjectWizardAddonWizardStep finalizingStep;

    public LabyModModuleBuilder(LabyModNewProjectWizardBuilder.LabyModNewProjectWizardAddonWizardStep finalizingStep) {
        this.finalizingStep = finalizingStep;
        setModuleFilePath(finalizingStep.getPath() + "/" + finalizingStep.getName());
        setContentEntryPath(finalizingStep.getPath() + "/" + finalizingStep.getName());
        setName(finalizingStep.getName());
    }


    @Override
    public ModuleType<?> getModuleType() {
        return LabyModModule.INSTANCE;
    }

    @Override
    public boolean isTemplateBased() {
        return true;
    }

    @Override
    public @Nullable Module commitModule(@NotNull Project project, @Nullable ModifiableModuleModel model) {
        Module module = super.commitModule(project, model);
        if (module == null) {
            return null;
        }

        File zipCache = GithubDownloadUtil.findCacheFile("LabyMod", "addon-template", "template.zip");
        try {
            GithubDownloadUtil.downloadContentToFileWithProgressSynchronously(
                    project,
                    "https://api.github.com/repos/LabyMod/addon-template/zipball",
                    "Downloading addon template",
                    zipCache,
                    "LabyMod",
                    "addon-template",
                    true
            );
        } catch (GeneratorException e) {
            Messages.showErrorDialog(project,
                    "Failed to download addon template to local fileystem: " + e.getMessage(),
                    "Error While Setting up Addon Project"
            );
            FileUtil.delete(zipCache);
            return module;
        }

        Path baseDir = Path.of(Objects.requireNonNull(getContentEntryPath()));

        try {
            ZipUtil.unzipWithProgressSynchronously(
                    project,
                    "Extracting addon template",
                    zipCache,
                    baseDir.toFile(),
                    null,
                    true
            );
        } catch (GeneratorException e) {
            Messages.showErrorDialog(project,
                    "Failed to extract addon template: " + e.getMessage(),
                    "Error While Setting up Addon Project"
            );
            return module;
        }

        ApplicationManager.getApplication().runWriteAction(() ->
                Objects.requireNonNull(VfsUtil.findFile(baseDir, true)).refresh(true, true));

        PsiManager psiManager = PsiManager.getInstance(project);
        PsiFile buildGradleKits = psiManager.findFile(Objects.requireNonNull(VfsUtil.findFile(baseDir.resolve("build.gradle.kts"), true)));
        PsiFile settingsGradleKits = psiManager.findFile(Objects.requireNonNull(VfsUtil.findFile(baseDir.resolve("settings.gradle.kts"), true)));

        // Mutate template build.gradle.kt & settings.gradle.kts based on user input in wizard
        WriteCommandAction.writeCommandAction(project)
                .withName("Adjusting Addon Properties")
                .withUndoConfirmationPolicy(UndoConfirmationPolicy.REQUEST_CONFIRMATION).run(() -> {
                    new LabyBuildGradleMutator(project, Objects.requireNonNull(buildGradleKits))
                            .mutate(finalizingStep.group().get(), finalizingStep.namespace().get(), finalizingStep.displayName().get(), finalizingStep.author().get(), "*");
                    new LabySettingsGradleMutator(project, settingsGradleKits)
                            .mutate(finalizingStep.namespace().get());
                });

        // And import the gradle project manually, as that's not happening automatically when using a template based approach
        Objects.requireNonNull(ProjectOpenProcessor.getImportProvider(Objects.requireNonNull(buildGradleKits).getVirtualFile()))
                .importProjectAfterwards(project, buildGradleKits.getVirtualFile());
        return module;
    }
}

 

The full log of the IDEA sandbox used to create the project using my wizard is uploaded under the upload id: 2023_09_08_26SmEv3mVXZJpQco7DVp7H (file: idea.log)

  

Cheers!

0
1 comment

Hello!

> The changes are not written into the files itself

It happens because you change an in-memory file model (VirtualFile, Document, PsiFile). You need to save the project when you finish all mutations if you want to see the result on disk before a Gradle reload.
PS. IDEA does that before Gradle reloads.


> I'm delegating to my ModuleBuilder
> The error com.intellij.serviceContainer.AlreadyDisposedException: Already disposed: Module: 'IdeaProjects.test-addon' (disposed) is thrown quite a bit

Your module builder creates an additional module on `commitModule` and this module isn't connected to the build system. In that case, Gradle can have unpredicted behavior when it tries to create a Gradle-specific module in an already taken location by a non-Gradle module.
You can just copy and paste your code from the commitModule function into the setupProject function.


Could you use org.jetbrains.plugins.gradle.service.project.open.GradleProjectImportUtil#linkAndRefreshGradleProject instead of the code below?

// And import the gradle project manually, as that's not happening automatically when using a template based approach
Objects.requireNonNull(ProjectOpenProcessor.getImportProvider(Objects.requireNonNull(buildGradleKits).getVirtualFile()))
    .importProjectAfterwards(project, buildGradleKits.getVirtualFile());
1

Please sign in to leave a comment.