core/project/source/jetbrains/mps/project/ProjectBase.java (207 lines of code) (raw):

/* * Copyright 2003-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.project; import jetbrains.mps.components.ComponentHost; import jetbrains.mps.components.CoreComponent; import jetbrains.mps.extapi.module.SRepositoryExt; import jetbrains.mps.extapi.module.SRepositoryRegistry; import jetbrains.mps.logging.Logger; import jetbrains.mps.project.ProjectModuleLoader.Update; import jetbrains.mps.project.structure.modules.GeneratorDescriptor; import jetbrains.mps.project.structure.project.ModulePath; import jetbrains.mps.project.structure.project.ProjectDescriptor; import jetbrains.mps.smodel.Generator; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.MPSModuleRepository; import jetbrains.mps.util.Pair; import jetbrains.mps.util.Reference; import jetbrains.mps.vfs.IFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.annotations.ImmutableReturn; import org.jetbrains.mps.annotations.Internal; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.BiConsumer; import java.util.stream.Stream; /** * MPS Project basic implementation. * Stores a set of modules. * Set of modules coincide with the modules in the underlying repository. * Supported always by a {@link ProjectDescriptor} which stores paths to the module descriptors * Doesn't manage lifecycle of a module descriptors other than "{@linkplain #update() update} 'em all" on demand. * Check {@code ModuleFileChangeListener} of [mps-platform] for change tracking. * However, tracks module renames (albeit in a bit weird way) to keep inner structures fit. * * This project is tied to MPS platform and gives access to MPS core platform and components it comprises. * * FIXME * poor architecture results in the intertwined control flow between ProjectBase, ModuleLoader and ProjectDescriptor * I guess that removing virtual folders solves all the problem * * @see ProjectDescriptor */ public abstract class ProjectBase extends Project { private static final Logger LOG = Logger.getLogger(ProjectBase.class); protected final ProjectManager myProjectManager; protected final ComponentHost myPlatform; private final ProjectModuleLoader myModuleLoader; protected ProjectBase(String name, @NotNull ComponentHost mpsPlatform) { this(name, mpsPlatform, false); ProjectRepository r = new ProjectRepository(this, mpsPlatform.findComponent(MPSModuleRepository.class), mpsPlatform.findComponent(SRepositoryRegistry.class)); r.init(); initRepository(r); } // FIXME refactor other subclasses and pass boolean initDefaultRepo == true|false protected ProjectBase(String name, @NotNull ComponentHost mpsPlatform, boolean unusedJustIndicatorOfNoRepository) { super(name); myModuleLoader = new ProjectModuleLoader(); // fixme: avoid myPlatform = mpsPlatform; // the only reason I keep the field is to manifest we register/unregister project instance into the same PM instance myProjectManager = mpsPlatform.findComponent(ProjectManager.class); } @NotNull public String getErrors() { return myModuleLoader.getErrors(); } @Nullable @Deprecated /*package*/ final ModulePath getPath(@NotNull SModuleReference mRef) { return myModuleLoader.getPath(mRef); } /*package*/ IFile getModuleDescriptorFile(SModuleReference ref) { return myModuleLoader.getDescriptorFile(ref); } /*package*/ String getModuleVirtualFolder(SModuleReference ref) { return myModuleLoader.getVirtualFolder(ref); } @Deprecated protected final Stream<ModulePath> allModulePaths() { return myModuleLoader.allPaths(); } protected final void forEachModuleEntry(@NotNull BiConsumer<IFile, String> consumer) { myModuleLoader.allFiles().forEach(p -> consumer.accept(p.o1, p.o2)); } // all project modules, including language-hosted generators, are registered with a project as owner. /*package*/ void associateWithProjectRepo(SModule module) { SRepositoryExt repository = (SRepositoryExt) getRepository(); // generally, module is already registered with a repo, as the primary mechanism to create a module instance, ModuleRepositoryFacade#instantiateModule, // automatically registers a module as well. // FIXME ^^^ this is likely no longer true repository.registerModule(module, this); } /*package*/ void dissociateFromProjectRepo(final SModule module, final boolean checkProjectIsOwner) { SRepositoryExt repository = (SRepositoryExt) getRepository(); if (checkProjectIsOwner && !repository.getOwners(module).contains(this)) { LOG.warning("Module has not been registered in the project: " + module); return; } repository.unregisterModule(module, this); } /** * Locks: at the moment, method grabs model write on the project repository once it registers module in there. * It's up to client to grab model write on a project repo in case he needs to batch addition of multiple modules. * The reason I decided to keep code to grab model write inside the method is that I plan to make module instantiation * without registration mainstream (unlike what's currently happens in {@code MRF.instantiateModule()}, which instantiates and registers module * right away, therefore clients usually possess model lock already). */ @Override public final void addModule(@NotNull SModule module) { IFile descriptorFile = module instanceof AbstractModule ? ((AbstractModule) module).getDescriptorFile() : null; if (descriptorFile != null) { final IFile existing = getModuleDescriptorFile(module.getModuleReference()); if (existing != null) { // throw new IllegalArgumentException(module + " is already in the " + this); todo enable after MPS-24400 LOG.warning(String.format("Project %s already tracks module %s under %s; provided %s ignored", this, module.getModuleReference(), existing, descriptorFile)); return; } // FIXME investigate why MP was not recorded for Language-owned Generators. // Other than notification dispatch in removeModule(), is there any trouble? // Beware, ProjectModuleFileChangeListener may need attention. // It seems to work with MP being announced for a Generator (seen live), but thorough check won't hurt. myModuleLoader.attachModule(module, descriptorFile, null); // project repository listeners may consult project.isProjectModule(moduleAdded), treat module being added as one from the project associateWithProjectRepo(module); } else { // there are modules like JpsSolutionIdea that got no file, but we still need to register them with a project repo, and it's better // to do it here rather than expose 'owner' knowledge outside of a project. // FIXME I don't register them in a project as there's no ModulePath to associate them with, but perhaps we shall use some default MP for them, // (e.g. associated with a project root). // XXX perhaps, shall keep record of modules added this way, e.g. to report them from Project.getProjectModules() associateWithProjectRepo(module); } } /** * force removal of the module from the project */ @Override public final void removeModule(@NotNull SModule module) { removeModule0(module, (file, folder) -> // client code can ask us to forget Generator module owned by a Language. We don't keep ModulePath for these myModuleLoader.detachModule(module, file) ); } /** * Method which intent is to update only the module <-> virtual path map * and remove the module from the repository but not to touch the project descriptor * */ @Internal @Nullable @Deprecated /*package*/ final ModulePath removeModule0(@NotNull SModule module) { // modulePath could be null for Generator modules sharing mpl descriptor file with their Language final Reference<ModulePath> modulePath = new Reference<>(null); removeModule0(module, (file, folder) -> modulePath.set(new ModulePath(file, folder))); return modulePath.get(); } /*package*/ final void removeModule0(@NotNull SModule module, @Nullable BiConsumer<IFile, String> continuation) { Pair<IFile, String> fileToFolder = myModuleLoader.dropIfAttached(module.getModuleReference()); if (module instanceof Language) { // Project tracks Generator modules by denoting itself as 'owner' of the module in a repository. // E.g. ProjectModulesFiller tells project to addModuleEntry(Generator), and it eventually gets down to associateWithProjectRepo(). // Though a great deal has been done to let Generator modules to live without their Language module present, I still keep this code // to unregister Language-owned generators along with the language as I'm too afraid to make the change and to dissociate supplied module only. Collection<Generator> ownedGenerators = ((Language) module).getOwnedGenerators(); for (Generator g : ownedGenerators) { myModuleLoader.dropIfAttached(g.getModuleReference()); dissociateFromProjectRepo(g, false); } } // fileToFolder object can be null in case the module had already been invalidated _before_ this invocation // in that case, the guard that checks if the module is owned by the project repo prevents a nasty exception // how clever is that... dissociateFromProjectRepo(module, fileToFolder == null); if (fileToFolder != null && continuation != null) { continuation.accept(fileToFolder.o1, fileToFolder.o2); } } @NotNull @ImmutableReturn public final List<SModule> getProjectModules() { List<SModule> result = new ArrayList<>(); SRepository repository = getRepository(); repository.getModelAccess().runReadAction(() -> { for (SModuleReference mRef : myModuleLoader.activeModules()) { SModule resolved = mRef.resolve(repository); if (resolved != null) { if (resolved instanceof Generator && !((Generator) resolved).getModuleDescriptor().isStandaloneModule()) { // openapi.Project.getProjectModules states it gives 'top-level' modules only, without language-owned generators // FIXME shall deprecate this method and stick to a new one, that gives all modules, including generators continue; } result.add(resolved); } else { LOG.error("Module " + mRef + " is not found in the project repository", new Throwable()); } } }); return Collections.unmodifiableList(result); } /** * Effective way to tell if a module is part of the project * Note, Generator modules owned by a Language from the project are deemed project modules, too. */ @Override public boolean isProjectModule(@NotNull SModule module) { if (getModuleDescriptorFile(module.getModuleReference()) != null) { return true; } // FIXME now myModuleLoader keeps ModulePath for each module, including Generator one, next code is no longer necessary if (module instanceof Generator) { // could be a generator owned by a language. Standalone generators from project would be discovered by getPath(). final GeneratorDescriptor gmd = ((Generator) module).getModuleDescriptor(); return !gmd.isStandaloneModule() && getModuleDescriptorFile(gmd.getSourceLanguage()) != null; } return false; } /** * persists the state of the project to the disk */ public abstract void save(); /** * tells a project that has external source of modules that it needs to refresh its set of modules * no-op for {@code ProjectBase}, subclasses shall override if needed */ protected void update() { } /** * AP todo : this logic must be redone alongside with filling the SLibraries with modules. * filling libraries and projects with modules externally seems to me the best solution * Requires model write */ // @Hack // @Deprecated // protected final void loadModules(@NotNull Collection<ModulePath> modulePaths) { // // FIXME present approach is unfortunate, as it's impossible to split module discovery (ModulesMiner for a path, and even up to SModule instantiation) // // from its registration in a project/its repo. First step could be initiated in parallel with project startup and done in non-UI thread. Even // // actual registration of the modules could be done in a project repo write w/o EDT access. It's only UI update that MAY (not necessarily SHALL) // // require EDT (with new project model, perhaps, even this might be no longer a requirement). // myModuleLoader.updatePathsInProject(modulePaths); // } protected final void reloadProject(@NotNull ProjectDescriptor projectDescriptor) { Update update = myModuleLoader.reloadProjectModules(this, projectDescriptor); update.doUpdate(); } /** * these are our own project opened/closed events. * in the case of idea platform presence they are triggered from the corresponding idea project opened/closed events. * in the other case they are triggered at the init/dispose methods */ public void projectOpened() { LOG.info("Project '" + getName() + "' is opened"); update(); myProjectManager.projectOpened(this); } public void projectClosed() { checkNotDisposed(); LOG.info("Project '" + getName() + "' is closing"); myProjectManager.projectClosed(this); } @Override public boolean isOpened() { return myProjectManager.getOpenedProjects().contains(this); } /** * Access components that constitute core of MPS platform. */ public final @NotNull ComponentHost getPlatform() { return myPlatform; } @Override public <T> T getComponent(Class<T> cls) { if (CoreComponent.class.isAssignableFrom(cls)) { return cls.cast(myPlatform.findComponent(cls.asSubclass(CoreComponent.class))); } return null; } /** * Optional operations, project may but not necessarily does grouping of modules. * @return virtual grouping for the module, empty string if none set or module doesn't belong to the project. */ @NotNull public String getVirtualFolder(@NotNull SModule module) { final String folder = getModuleVirtualFolder(module.getModuleReference()); return folder == null ? "" : folder; } /** * Optional operation to assign a grouping for a project module. Optional operation, projects may opt to * ignore module grouping */ public void setVirtualFolder(@NotNull SModule module, @Nullable String newFolder) { // Used to live in StandaloneMPSProject. I don't see why it's restricted to that one, provided any // ProjectBase derivative knows about ModulePath and its virtual folder. final SModuleReference moduleReference = module.getModuleReference(); IFile descriptorFile = getModuleDescriptorFile(moduleReference); if (descriptorFile != null) { myModuleLoader.setVirtualFolder(moduleReference, newFolder); } else { LOG.warning(String.format("Could not set virtual folder for the module %s, module could not be found", module)); } } public final void addListener(@NotNull ProjectModuleLoadingListener listener) { myModuleLoader.addListener(listener); } public final void removeListener(@NotNull ProjectModuleLoadingListener listener) { myModuleLoader.removeListener(listener); } }