IDEA-108633 Gradle: Provide 'auto-import' feature

'Auto-import' api and infrastructure is provided
This commit is contained in:
Denis.Zhdanov
2013-06-08 12:58:38 +04:00
parent ab63359dea
commit f7e94c6bbc
8 changed files with 441 additions and 7 deletions
@@ -0,0 +1,50 @@
/*
* Copyright 2000-2013 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 com.intellij.openapi.externalSystem;
import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings;
import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* External system integration provides 'auto-import' feature, i.e. it listens for external system config files changes
* and automatically runs external project refresh and sync.
* <p/>
* Only {@link AbstractExternalSystemSettings#getLinkedProjectsSettings() registered project's}
* {@link ExternalProjectSettings#getExternalProjectPath() config files} are processed by default. However, there is a possible case
* that there are other auxiliary config files/directories which modification should trigger external project refresh. This interface
* is supposed to handle that situation, i.e. any {@link ExternalSystemManager external system implementation} which requires
* the functionality described above should implement this interface.
*
* @author Denis Zhdanov
* @since 6/7/13 6:44 PM
*/
public interface ExternalSystemAutoImportAware {
/**
* This method serves to check if particular file/dir change should trigger external project refresh.
* <p/>
* <b>Note:</b> there is no need to handle here {@link ExternalProjectSettings#getExternalProjectPath() external project config files}
* known to the ide as that functionality is built-in. Only auxiliary files should be processed.
*
* @param changedFileOrDirPath changed file/dir path
* @return <code>null</code> if target change should not trigger external project refresh;
* path to config file of an external project which should be refreshed
*/
@Nullable
String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath);
}
@@ -186,6 +186,10 @@ public class ExternalSystemApiUtil {
return MANAGERS.getValue().get(externalSystemId);
}
public static Collection<ExternalSystemManager<?, ?, ?, ?, ?>> getAllManagers() {
return MANAGERS.getValue().values();
}
@NotNull
public static Map<Key<?>, List<DataNode<?>>> group(@NotNull Collection<DataNode<?>> nodes) {
if (nodes.isEmpty()) {
@@ -34,9 +34,11 @@ public class ExternalSystemConstants {
@NonNls @NotNull public static final String TOOL_WINDOW_PLACE = "ExternalSystem.ToolWindow";
@NonNls @NotNull public static final String TREE_PLACE = "ExternalSystem.Tree";
public static final boolean VERBOSE_PROCESSING = SystemProperties.getBooleanProperty("external.system.verbose.processing", false);
public static final int RECENT_TASKS_NUMBER = SystemProperties.getIntProperty("external.system.recent.tasks.number", 5);
public static final char PATH_SEPARATOR = '/';
public static final boolean VERBOSE_PROCESSING = SystemProperties.getBooleanProperty("external.system.verbose.processing", false);
public static final int RECENT_TASKS_NUMBER = SystemProperties.getIntProperty("external.system.recent.tasks.number", 5);
public static final int AUTO_IMPORT_DELAY_MILLIS = SystemProperties.getIntProperty("external.system.auto.import.delay.ms", 3000);
public static final char PATH_SEPARATOR = '/';
// Order.
public static final int BUILTIN_SERVICE_ORDER = 42;
@@ -16,6 +16,7 @@
package com.intellij.openapi.externalSystem.service;
import com.intellij.openapi.externalSystem.ExternalSystemManager;
import com.intellij.openapi.externalSystem.service.project.autoimport.ExternalSystemAutoImporter;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.project.Project;
@@ -43,6 +44,7 @@ public class ExternalSystemStartupActivity implements StartupActivity {
ExternalSystemUtil.refreshProjects(project, manager.getSystemId());
}
}
ExternalSystemAutoImporter.letTheMagicBegin(project);
}
};
@@ -19,7 +19,7 @@ import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.model.settings.ExternalSystemExecutionSettings;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
import com.intellij.openapi.externalSystem.service.project.ExternalSystemProjectResolver;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -31,9 +31,9 @@ public interface ExternalProjectRefreshCallback {
/**
* Is expected to be called when
* {@link ExternalSystemProjectResolver#resolveProjectInfo(ExternalSystemTaskId, String, boolean, ExternalSystemExecutionSettings)}
* {@link ExternalSystemProjectResolver#resolveProjectInfo(ExternalSystemTaskId, String, boolean, ExternalSystemExecutionSettings, ExternalSystemTaskNotificationListener)}
* returns without exception.
*
*
* @param externalProject target external project (if available)
*/
void onSuccess(@Nullable DataNode<ProjectData> externalProject);
@@ -0,0 +1,344 @@
/*
* Copyright 2000-2013 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 com.intellij.openapi.externalSystem.service.project.autoimport;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware;
import com.intellij.openapi.externalSystem.ExternalSystemManager;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback;
import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataManager;
import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings;
import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.util.Alarm;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author Denis Zhdanov
* @since 6/7/13 6:38 PM
*/
public class ExternalSystemAutoImporter implements BulkFileListener, DocumentListener {
@NotNull private final ConcurrentMap<ProjectSystemId, Set<String /* external project path */>> myFilesToRefresh
= ContainerUtil.newConcurrentMap();
@NotNull private final Alarm myVfsAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
@NotNull private final ReadWriteLock myVfsLock = new ReentrantReadWriteLock();
@NotNull private final Set<Document> myDocumentsToSave = ContainerUtilRt.newHashSet();
@NotNull private final Alarm myDocumentAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
@NotNull private final ReadWriteLock myDocumentLock = new ReentrantReadWriteLock();
@NotNull private final Runnable myFilesRequest = new Runnable() {
@Override
public void run() {
refreshFilesIfNecessary();
}
};
@NotNull private final Runnable myDocumentsSaveRequest = new Runnable() {
@Override
public void run() {
saveDocumentsIfNecessary();
}
};
@NotNull private final ExternalProjectRefreshCallback myRefreshCallback = new ExternalProjectRefreshCallback() {
@Override
public void onSuccess(@Nullable DataNode<ProjectData> externalProject) {
if (externalProject != null) {
myProjectDataManager.importData(externalProject.getKey(), Collections.singleton(externalProject), myProject, false);
}
}
@Override
public void onFailure(@NotNull String errorMessage, @Nullable String errorDetails) {
// Do nothing.
}
};
@NotNull private final Project myProject;
@NotNull private final ProjectDataManager myProjectDataManager;
@NotNull private final MyEntry[] myAutoImportAware;
public ExternalSystemAutoImporter(@NotNull Project project,
@NotNull ProjectDataManager projectDataManager,
@NotNull MyEntry[] autoImportAware)
{
myProject = project;
myProjectDataManager = projectDataManager;
myAutoImportAware = autoImportAware;
}
@SuppressWarnings("unchecked")
public static void letTheMagicBegin(@NotNull Project project) {
List<MyEntry> autoImportAware = ContainerUtilRt.newArrayList();
Collection<ExternalSystemManager<?, ?, ?, ?, ?>> managers = ExternalSystemApiUtil.getAllManagers();
for (ExternalSystemManager<?, ?, ?, ?, ?> manager : managers) {
AbstractExternalSystemSettings<?, ?> systemSettings = manager.getSettingsProvider().fun(project);
ExternalSystemAutoImportAware defaultImportAware = createDefault(systemSettings);
final ExternalSystemAutoImportAware aware;
if (manager instanceof ExternalSystemAutoImportAware) {
aware = combine(defaultImportAware, (ExternalSystemAutoImportAware)manager);
}
else {
aware = defaultImportAware;
}
autoImportAware.add(new MyEntry(manager.getSystemId(), systemSettings, aware));
}
MyEntry[] entries = autoImportAware.toArray(new MyEntry[autoImportAware.size()]);
ExternalSystemAutoImporter autoImporter = new ExternalSystemAutoImporter(
project,
ServiceManager.getService(ProjectDataManager.class),
entries
);
final MessageBus messageBus = project.getMessageBus();
messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, autoImporter);
EditorFactory.getInstance().getEventMulticaster().addDocumentListener(autoImporter, project);
}
@NotNull
private static ExternalSystemAutoImportAware combine(@NotNull final ExternalSystemAutoImportAware aware1,
@NotNull final ExternalSystemAutoImportAware aware2)
{
return new ExternalSystemAutoImportAware() {
@Nullable
@Override
public String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath) {
String projectPath = aware1.getAffectedExternalProjectPath(changedFileOrDirPath);
return projectPath == null ? aware2.getAffectedExternalProjectPath(changedFileOrDirPath) : projectPath;
}
};
}
@NotNull
private static ExternalSystemAutoImportAware createDefault(@NotNull final AbstractExternalSystemSettings<?, ?> systemSettings) {
return new ExternalSystemAutoImportAware() {
@Nullable
@Override
public String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath) {
return systemSettings.getLinkedProjectSettings(changedFileOrDirPath) == null ? null : changedFileOrDirPath;
}
};
}
@Override
public void beforeDocumentChange(DocumentEvent event) {
}
@Override
public void documentChanged(DocumentEvent event) {
Document document = event.getDocument();
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
VirtualFile file = fileDocumentManager.getFile(document);
if (file == null) {
return;
}
String path = ExternalSystemApiUtil.getLocalFileSystemPath(file);
for (MyEntry entry : myAutoImportAware) {
if (entry.aware.getAffectedExternalProjectPath(path) != null) {
// Document save triggers VFS event but FileDocumentManager might be registered after the current listener, that's why
// call to 'saveDocument()' might not produce the desired effect. That's why we reschedule document save if necessary.
scheduleDocumentSave(document);
return;
}
}
}
private void scheduleDocumentSave(@NotNull Document document) {
Lock lock = myDocumentLock.readLock();
lock.lock();
try {
myDocumentsToSave.add(document);
if (myDocumentAlarm.getActiveRequestCount() <= 0) {
myDocumentAlarm.addRequest(myDocumentsSaveRequest, 100);
}
}
finally {
lock.unlock();
}
}
private void saveDocumentsIfNecessary() {
final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
Lock lock = myDocumentLock.writeLock();
Set<Document> toKeep = ContainerUtilRt.newHashSet();
final Set<Document> toSave = ContainerUtilRt.newHashSet();
lock.lock();
try {
myDocumentAlarm.cancelAllRequests();
for (Document document : myDocumentsToSave) {
if (fileDocumentManager.isDocumentUnsaved(document)) {
toSave.add(document);
}
else {
toKeep.add(document);
}
}
myDocumentsToSave.clear();
if (!toSave.isEmpty()) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
for (Document document : toSave) {
fileDocumentManager.saveDocument(document);
}
}
});
}
if (!toKeep.isEmpty()) {
myDocumentsToSave.addAll(toKeep);
myDocumentAlarm.addRequest(myDocumentsSaveRequest, 100);
}
}
finally {
lock.unlock();
}
}
@Override
public void before(@NotNull List<? extends VFileEvent> events) {
}
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
boolean scheduleRefresh = false;
for (VFileEvent event : events) {
String changedPath = event.getPath();
for (MyEntry entry : myAutoImportAware) {
String projectPath = entry.aware.getAffectedExternalProjectPath(changedPath);
if (projectPath == null) {
continue;
}
ExternalProjectSettings projectSettings = entry.systemSettings.getLinkedProjectSettings(projectPath);
if (projectSettings != null && projectSettings.isUseAutoImport()) {
addPath(entry.externalSystemId, projectPath);
scheduleRefresh = true;
break;
}
}
}
if (scheduleRefresh) {
myVfsAlarm.cancelAllRequests();
myVfsAlarm.addRequest(myFilesRequest, ExternalSystemConstants.AUTO_IMPORT_DELAY_MILLIS);
}
}
private void addPath(@NotNull ProjectSystemId externalSystemId, @NotNull String path) {
Lock lock = myVfsLock.readLock();
lock.lock();
try {
Set<String> paths = myFilesToRefresh.get(externalSystemId);
while (paths == null) {
myFilesToRefresh.putIfAbsent(externalSystemId, ContainerUtilRt.<String>newHashSet());
paths = myFilesToRefresh.get(externalSystemId);
}
paths.add(path);
}
finally {
lock.unlock();
}
}
private void refreshFilesIfNecessary() {
if (myFilesToRefresh.isEmpty()) {
return;
}
Map<ProjectSystemId, Set<String>> copy = ContainerUtilRt.newHashMap();
Lock fileLock = myVfsLock.writeLock();
fileLock.lock();
try {
copy.putAll(myFilesToRefresh);
myFilesToRefresh.clear();
}
finally {
fileLock.unlock();
}
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
LocalFileSystem fileSystem = LocalFileSystem.getInstance();
Lock documentLock = myDocumentLock.writeLock();
documentLock.lock();
try {
for (Set<String> paths : copy.values()) {
for (String path : paths) {
VirtualFile file = fileSystem.findFileByPath(path);
if (file != null) {
Document document = fileDocumentManager.getCachedDocument(file);
if (document != null) {
myDocumentsToSave.remove(document);
}
}
}
}
}
finally {
documentLock.unlock();
}
for (Map.Entry<ProjectSystemId, Set<String>> entry : copy.entrySet()) {
for (String path : entry.getValue()) {
ExternalSystemUtil.refreshProject(myProject, entry.getKey(), path, myRefreshCallback, true, false);
}
}
}
private static class MyEntry {
@NotNull public final ProjectSystemId externalSystemId;
@NotNull public final AbstractExternalSystemSettings<?, ?> systemSettings;
@NotNull public final ExternalSystemAutoImportAware aware;
MyEntry(@NotNull ProjectSystemId externalSystemId,
@NotNull AbstractExternalSystemSettings<?, ?> systemSettings,
@NotNull ExternalSystemAutoImportAware aware)
{
this.externalSystemId = externalSystemId;
this.systemSettings = systemSettings;
this.aware = aware;
}
}
}
@@ -33,6 +33,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.LibraryOrderEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.util.BooleanFunction;
@@ -133,12 +134,24 @@ public class LibraryDependencyDataService extends AbstractDependencyDataService<
myLibraryManager.importData(librariesToImport, module.getProject(), synchronous);
}
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
Map<String /* library name */, LibraryOrderEntry> toRemove = ContainerUtilRt.newHashMap();
for (OrderEntry entry : moduleRootManager.getOrderEntries()) {
if (entry instanceof LibraryOrderEntry) {
LibraryOrderEntry e = (LibraryOrderEntry)entry;
String libraryName = e.getLibraryName();
if (libraryName != null) {
toRemove.put(libraryName, e);
}
}
}
for (DataNode<LibraryDependencyData> dependencyNode : nodesToImport) {
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
final ModifiableRootModel moduleRootModel = moduleRootManager.getModifiableModel();
try {
libraryTable = myPlatformFacade.getProjectLibraryTable(module.getProject());
final LibraryDependencyData dependencyData = dependencyNode.getData();
toRemove.remove(dependencyData.getName());
final Library library = libraryTable.getLibraryByName(dependencyData.getName());
if (library == null) {
assert false;
@@ -161,6 +174,10 @@ public class LibraryDependencyDataService extends AbstractDependencyDataService<
moduleRootModel.commit();
}
}
if (!toRemove.isEmpty()) {
removeData(toRemove.values(), module, synchronous);
}
}
});
}
@@ -31,7 +31,9 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleOrderEntry;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.util.BooleanFunction;
import com.intellij.util.containers.ContainerUtilRt;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
@@ -92,10 +94,19 @@ public class ModuleDependencyDataService extends AbstractDependencyDataService<M
@Override
public void run() {
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
Map<String /* dependency module name */, ModuleOrderEntry> toRemove = ContainerUtilRt.newHashMap();
for (OrderEntry entry : moduleRootManager.getOrderEntries()) {
if (entry instanceof ModuleOrderEntry) {
ModuleOrderEntry e = (ModuleOrderEntry)entry;
toRemove.put(e.getModuleName(), e);
}
}
final ModifiableRootModel moduleRootModel = moduleRootManager.getModifiableModel();
try {
for (DataNode<ModuleDependencyData> dependencyNode : toImport) {
final ModuleDependencyData dependencyData = dependencyNode.getData();
toRemove.remove(dependencyData.getName());
final String moduleName = dependencyData.getName();
Module ideDependencyModule = myProjectStructureHelper.findIdeModule(moduleName, module.getProject());
if (ideDependencyModule == null) {
@@ -135,6 +146,10 @@ public class ModuleDependencyDataService extends AbstractDependencyDataService<M
finally {
moduleRootModel.commit();
}
if (!toRemove.isEmpty()) {
removeData(toRemove.values(), module, synchronous);
}
}
});
}