workbench/mps-platform/source/jetbrains/mps/project/MPSProject.java (144 lines of code) (raw):
/*
* Copyright 2003-2025 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 com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import jetbrains.mps.core.platform.Platform;
import jetbrains.mps.extapi.module.SRepositoryRegistry;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.ide.vfs.IdeaFileSystem;
import jetbrains.mps.ide.vfs.ProjectRootListenerComponent;
import jetbrains.mps.nodefs.FileSystemProjectBridge;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.smodel.MPSModuleRepository;
import jetbrains.mps.smodel.WorkbenchModelAccess;
import jetbrains.mps.util.annotation.AccessAsPlatformService;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.vfs.VFSManager;
import jetbrains.mps.vfs.tracking.ConflictResolverImpl;
import kotlin.Unit;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.module.SRepository;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Represents a project based on the idea platform project
* <p>
* fixme introduce a project<->library relation on this particular level (AP)
*/
public abstract class MPSProject extends ProjectBase implements FileBasedProject, Disposable {
private final com.intellij.openapi.project.Project myProject;
private final IdeaFileSystem myProjectFileSystem;
private FileSystemProjectBridge myFileSystemBridge;
// WorkbenchModelAccess is provisional argument. Now it provides implementation of executeCommand method
// with respect to shared model lock object from its smodel.ModelAccess superclass. Once each MA has own
// model lock object and executeCommand* implementations, we won't need this WMA parameter
public MPSProject(@NotNull com.intellij.openapi.project.Project project) {
super(project.getName(), MPSCoreComponents.getInstance().getPlatform(), false);
myProject = project;
myProjectFileSystem = IdeaFileSystem.getInstance();
project.getService(ProjectRootListenerComponent.class).boostProjectRead(myProjectFileSystem);
Platform platform = MPSCoreComponents.getInstance().getPlatform();
final MPSModuleRepository extRepo = platform.findComponent(MPSModuleRepository.class);
final SRepositoryRegistry registry = platform.findComponent(SRepositoryRegistry.class);
final ModelAccess projectMA = WorkbenchModelAccess.getInstance().createForProject(MPSProject.this);
final ProjectRepository repo = new ProjectRepository(this, extRepo, registry, projectMA);
repo.init();
initRepository(repo);
repo.setConflictResolver(new ConflictResolverImpl(this, platform.findComponent(PersistenceRegistry.class), platform.findComponent(VFSManager.class)));
}
@Override
public void projectOpened() {
if (myFileSystemBridge == null) {
// can't override projectOpened(), go with initComponent() now; have to fix ether of these anyway once get to ProjectComponent here
myFileSystemBridge = new FileSystemProjectBridge(this);
// FWIW, there's OnReloadingUndoCleaner (at least) that depends on this bridge present for a project
myFileSystemBridge.projectOpened();
}
super.projectOpened();
}
@Override
public void projectClosed() {
super.projectClosed();
}
@Override
public void dispose() {
super.dispose();
}
@Override
protected synchronized void destroy() {
if (isDisposed()) {
return;
}
myFileSystemBridge.projectClosed();
myFileSystemBridge = null;
((ProjectRepository) getRepository()).setConflictResolver(null);
super.destroy();
}
@NotNull
@Override
public File getProjectFile() {
String presentableUrl = myProject.getPresentableUrl();
if (presentableUrl == null) {
assert myProject.isDefault() : "Broken contract : url is null whenever the project is default!";
throw new IllegalArgumentException("The project url is null (default project?)");
}
return new File(presentableUrl);
}
/**
* @return the backing idea project
*/
@NotNull
public com.intellij.openapi.project.Project getProject() {
return myProject;
}
@NotNull
@Override
public String getName() {
// have to keep method here to avoid broken references in mbeddr
return super.getName();
}
@Override
public void save() {
getProject().save();
}
public static MPSProject open(@NotNull String projectPath) throws InvalidDataException, IOException, JDOMException {
com.intellij.openapi.project.Project project = ProjectManagerEx.getInstanceEx().loadAndOpenProject(projectPath);
if (project == null) {
return null;
}
// FIXME avoid calling ComponentManger.getComponent
return project.getComponent(MPSProject.class);
}
@Override
public <T> T getComponent(Class<T> clazz) {
if (isDisposed()) {
return null;
}
T rv;
if (clazz.getAnnotation(AccessAsPlatformService.class) != null) {
rv = getProject().getService(clazz);
} else {
//noinspection UnstableApiUsage
// FIXME avoid calling ComponentManger.getComponent
rv = getProject().getComponent(clazz);
}
// though would be great to support both components and services, I didn't find a reliable
// mechanism to detect whether supplied class is component or a service. Supplied interface may
// not be assignable to BaseComponent, only its implementation implements respective component
// interface (see EditorExtensionRegistry), and we may end up with getService for a component,
// which is not what IDEA tolerates (throws an exception, check
// logPluginError call in ComponentManagerImpl.doGetService).
if (rv == null) {
return super.getComponent(clazz);
}
return rv;
}
/**
* XXX the method might be worth exposing from {@link FileBasedProject} (with a more generic return type, of course), so that other Project clients has
* a chance to access project's FS without need to use global singleton
* @return fs one may use to resolve string paths of a project
*/
public final IdeaFileSystem getFileSystem() {
return myProjectFileSystem;
}
@Override
public void reconcileProjectFiles(@Nullable Iterable<IFile> files) {
// XXX perhaps, shall pass ProgressMonitor in here?
if (files == null) {
return;
}
// original fix was for MPS-14247, refactored now to use IDEA services and to update VCS explicitly
ArrayList<VirtualFile> ideaFiles = new ArrayList<>();
for (IFile f : files) {
final VirtualFile vf = myProjectFileSystem.asVirtualFile(f);
if (vf == null) {
continue;
}
ideaFiles.add(vf);
}
// want to schedule VCS update *after* IDEA get a chance to find out about new files, hence invokeLater and synchronous refresh
// I don't need write nor write intent here (as invokeLater provides), markDirtyAndRefresh is ok with read, just didn't find invokeReadLater().
ApplicationManager.getApplication().invokeLater(() -> {
// VfsUtil.markDirtyAndRefresh relies on LocalFileSystem (eventually delegates to RefreshQueue), while our
// BaseIdeaFileSystem.refresh uses IDEA's RefreshQueue directly. No idea what's right here.
VfsUtil.markDirtyAndRefresh(false, true, true, ideaFiles.toArray(new VirtualFile[0]));
// we used to rely on ChangeListManager.scheduleUpdate() of uncertain origin (uses of the method trace back to 1ca3d72f),
// but according to Aleksey Pivovarov, it's no-op, and we'd rather stick to VcsDirtyScopeManager
// VcsDirtyScopeManager doesn't need read/write or a specific thread, but as long as I want it to run
// *after* vfs refresh, I keep it here, inside invokeLater(). XXX Perhaps, have to change invokeLater to some
// async job scheduler, just too afraid to do it with 22.2 next door.
// FIXME In fact, Aleksey Pivovarov suggests VCS has to pick up VFS changes automatically, perhaps, we could just
// use async markDirtyAndRefresh() then?
VcsDirtyScopeManager.getInstance(myProject).filesDirty(Collections.emptyList(), ideaFiles);
});
}
}