IDEA-156758 Disable non project files writing protection, when file is opened explicitly

IDEA-156759 Add an option to 'Non-project files' protection dialog to edit files in a folder
IDEA-156760 'Non-project files protection' dialog is had to use with a keyboard
This commit is contained in:
Anton Makeev
2016-05-28 11:52:57 +02:00
parent 6887a0e91b
commit 6ffa2055db
12 changed files with 323 additions and 209 deletions
@@ -30,6 +30,7 @@ import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.command.undo.UndoableAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessProvider;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
@@ -537,22 +538,20 @@ public abstract class BaseRefactoringProcessor implements Runnable {
public final void run() {
if (ApplicationManager.getApplication().isUnitTestMode()) {
ApplicationManager.getApplication().assertIsDispatchThread();
doRun();
NonProjectFileWritingAccessProvider.disableChecksDuring(this::doRun);
//noinspection TestOnlyProblems
UIUtil.dispatchAllInvocationEvents();
//noinspection TestOnlyProblems
UIUtil.dispatchAllInvocationEvents();
return;
}
if (ApplicationManager.getApplication().isWriteAccessAllowed()) {
LOG.error("Refactorings should not be started inside write action\n because they start progress inside and any read action from the progress task would cause the deadlock", new Exception());
DumbService.getInstance(myProject).smartInvokeLater(new Runnable() {
@Override
public void run() {
doRun();
}
});
DumbService.getInstance(myProject).smartInvokeLater(() -> NonProjectFileWritingAccessProvider.disableChecksDuring(this::doRun));
}
else {
doRun();
NonProjectFileWritingAccessProvider.disableChecksDuring(this::doRun);
}
}
@@ -142,7 +142,7 @@ public class PsiElementRenameHandler implements RenameHandler {
if (!PsiManager.getInstance(project).isInProject(element)) {
if (element.isPhysical()) {
VirtualFile virtualFile = PsiUtilCore.getVirtualFile(element);
if (!(virtualFile != null && NonProjectFileWritingAccessProvider.isWriteAccessAllowedExplicitly(virtualFile, project))) {
if (!(virtualFile != null && NonProjectFileWritingAccessProvider.isWriteAccessAllowed(virtualFile))) {
String message = RefactoringBundle.message("error.out.of.project.element", UsageViewUtil.getType(element));
return RefactoringBundle.getCannotRefactorMessage(message);
}
@@ -30,6 +30,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorProvider;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileEditor.ex.FileEditorProviderManager;
import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessProvider;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.ex.FileTypeChooser;
import com.intellij.openapi.project.DumbAware;
@@ -41,7 +42,6 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.impl.welcomeScreen.NewWelcomeScreen;
import com.intellij.platform.PlatformProjectOpenProcessor;
import com.intellij.projectImport.ProjectAttachProcessor;
import com.intellij.util.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -127,6 +127,8 @@ public class OpenFileAction extends AnAction implements DumbAware {
return;
}
NonProjectFileWritingAccessProvider.allowWriting(file);
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file);
FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
}
@@ -8,39 +8,48 @@
<properties/>
<border type="none"/>
<children>
<grid id="2f133" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="2f133" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="2" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="etched"/>
<border type="none"/>
<children>
<component id="e62df" class="javax.swing.JRadioButton" default-binding="true">
<component id="e62df" class="javax.swing.JRadioButton" binding="myUnlockOneButton">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="true"/>
<text value="I want to edit these files anyway"/>
<selected value="false"/>
<text value="unlock one"/>
</properties>
</component>
<component id="592c9" class="javax.swing.JRadioButton" binding="myUnlockAllButton" default-binding="true">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="unlock all"/>
</properties>
</component>
<component id="4ff89" class="javax.swing.JRadioButton" binding="myUnlockDirButton">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="I want to edit any non-project file in the current session"/>
<selected value="false"/>
<text value="unlock dir"/>
</properties>
</component>
</children>
</grid>
<component id="dbd63" class="javax.swing.JLabel">
<component id="dbd63" class="javax.swing.JLabel" binding="myListTitle">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="These files do not belong to the project"/>
<text value="title"/>
</properties>
</component>
<scrollpane id="6c540" class="com.intellij.ui.components.JBScrollPane">
@@ -64,6 +73,7 @@
<member id="e62df"/>
<member id="25953"/>
<member id="16741"/>
<member id="4ff89"/>
</group>
</buttonGroups>
</form>
@@ -17,33 +17,85 @@ package com.intellij.openapi.fileEditor.impl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.readOnlyHandler.FileListRenderer;
import com.intellij.openapi.vcs.readOnlyHandler.ReadOnlyStatusDialog;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.CollectionListModel;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.List;
public class NonProjectFileWritingAccessDialog extends DialogWrapper {
private JPanel myPanel;
private JLabel myListTitle;
private JList myFileList;
private JRadioButton myUnlockOneButton;
private JRadioButton myUnlockDirButton;
private JRadioButton myUnlockAllButton;
protected NonProjectFileWritingAccessDialog(@NotNull Project project, @NotNull List<VirtualFile> nonProjectFiles) {
public NonProjectFileWritingAccessDialog(@NotNull Project project, @NotNull List<VirtualFile> nonProjectFiles) {
this(project, nonProjectFiles, "Non-Project Files");
}
public NonProjectFileWritingAccessDialog(@NotNull Project project,
@NotNull List<VirtualFile> nonProjectFiles,
@NotNull String filesType) {
super(project);
setTitle("Non-Project Files Access");
setTitle(filesType + " Protection");
myFileList.setCellRenderer(new FileListRenderer());
myFileList.setModel(new CollectionListModel<VirtualFile>(nonProjectFiles));
String theseFilesMessage = ReadOnlyStatusDialog.getTheseFilesMessage(nonProjectFiles);
myListTitle.setText(StringUtil.capitalize(theseFilesMessage)
+ " " + (nonProjectFiles.size() > 1 ? "do" : "does")
+ " not belong to the project:");
myUnlockOneButton.setSelected(true);
setTextAndMnemonicAndListeners(myUnlockOneButton, "I want to edit " + theseFilesMessage + " anyway", "edit");
int dirs = ContainerUtil.map2Set(nonProjectFiles, VirtualFile::getParent).size();
setTextAndMnemonicAndListeners(myUnlockDirButton, "I want to edit all files in "
+ StringUtil.pluralize("this", dirs)
+ " " + StringUtil.pluralize("directory", dirs), "dir");
setTextAndMnemonicAndListeners(myUnlockAllButton, "I want to edit any non-project file in the current session", "any");
// disable default button to avoid accidental pressing, if user typed something, missed the dialog and pressed 'enter'.
getOKAction().putValue(DEFAULT_ACTION, null);
getCancelAction().putValue(DEFAULT_ACTION, true);
getCancelAction().putValue(DEFAULT_ACTION, null);
getRootPane().registerKeyboardAction(e -> doOKAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK),
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
getRootPane().registerKeyboardAction(e -> doOKAction(), KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.META_DOWN_MASK),
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
init();
}
private void setTextAndMnemonicAndListeners(JRadioButton button, String text, String mnemonic) {
button.setText(text);
button.setMnemonic(mnemonic.charAt(0));
button.setDisplayedMnemonicIndex(button.getText().indexOf(mnemonic));
// enabled OK button when user selects an option
button.addActionListener(e -> button.getRootPane().setDefaultButton(getButton(getOKAction())));
button.addItemListener(e -> button.getRootPane().setDefaultButton(getButton(getOKAction())));
}
@Nullable
@Override
public JComponent getPreferredFocusedComponent() {
return myUnlockOneButton;
}
@Nullable
@Override
@@ -53,8 +105,9 @@ public class NonProjectFileWritingAccessDialog extends DialogWrapper {
@NotNull
public NonProjectFileWritingAccessProvider.UnlockOption getUnlockOption() {
return myUnlockAllButton.isSelected() ? NonProjectFileWritingAccessProvider.UnlockOption.UNLOCK_ALL
: NonProjectFileWritingAccessProvider.UnlockOption.UNLOCK;
if (myUnlockAllButton.isSelected()) return NonProjectFileWritingAccessProvider.UnlockOption.UNLOCK_ALL;
if (myUnlockDirButton.isSelected()) return NonProjectFileWritingAccessProvider.UnlockOption.UNLOCK_DIR;
return NonProjectFileWritingAccessProvider.UnlockOption.UNLOCK;
}
protected String getHelpId() {
@@ -15,8 +15,8 @@
*/
package com.intellij.openapi.fileEditor.impl;
import com.intellij.ProjectTopics;
import com.intellij.ide.impl.ProjectUtil;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceKt;
import com.intellij.openapi.components.impl.stores.IProjectStore;
@@ -24,31 +24,35 @@ import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.roots.ModuleRootAdapter;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.NotNullLazyKey;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.*;
import com.intellij.util.NotNullFunction;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.util.NullableFunction;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NonProjectFileWritingAccessProvider extends WritingAccessProvider {
public enum AccessStatus {REQUESTED, ALLOWED}
private static final Key<Boolean> ENABLE_IN_TESTS = Key.create("NON_PROJECT_FILE_ACCESS_ENABLE_IN_TESTS");
private static final Key<Boolean> ALL_ACCESS_ALLOWED = Key.create("NON_PROJECT_FILE_ALL_ACCESS_STATUS");
private static final NotNullLazyKey<Map<VirtualFile, AccessStatus>, Project> ACCESS_STATUS
= NotNullLazyKey.create("NON_PROJECT_FILE_ACCESS_STATUS", project -> new HashMap<VirtualFile, AccessStatus>());
private static final NotNullLazyKey<AtomicInteger, UserDataHolder> ACCESS_ALLOWED
= NotNullLazyKey.create("NON_PROJECT_FILE_ACCESS", holder -> new AtomicInteger());
@NotNull private final Project myProject;
@Nullable private static NullableFunction<List<VirtualFile>, UnlockOption> ourCustomUnlocker;
@@ -58,26 +62,22 @@ public class NonProjectFileWritingAccessProvider extends WritingAccessProvider {
ourCustomUnlocker = unlocker;
}
public NonProjectFileWritingAccessProvider(@NotNull final Project project) {
public NonProjectFileWritingAccessProvider(@NotNull Project project) {
myProject = project;
VirtualFileManager.getInstance().addVirtualFileListener(new VirtualFileAdapter() {
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
getRegisteredFiles(project).remove(event.getFile());
public void fileCreated(@NotNull VirtualFileEvent event) {
unlock(event);
}
}, project);
myProject.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
@Override
public void rootsChanged(ModuleRootEvent event) {
Map<VirtualFile, AccessStatus> files = getRegisteredFiles(project);
// reset access status and notifications for files that became project files
for (VirtualFile each : new ArrayList<VirtualFile>(files.keySet())) {
if (isProjectFile(each, project)) {
files.remove(each);
}
}
public void fileCopied(@NotNull VirtualFileCopyEvent event) {
unlock(event);
}
private void unlock(@NotNull VirtualFileEvent event) {
if (!event.isFromRefresh() && !event.getFile().isDirectory()) allowWriting(event.getFile());
}
});
}
@@ -90,24 +90,9 @@ public class NonProjectFileWritingAccessProvider extends WritingAccessProvider {
@NotNull
@Override
public Collection<VirtualFile> requestWriting(VirtualFile... files) {
if (allAccessAllowed(myProject)) return Collections.emptyList();
List<VirtualFile> deniedFiles = new SmartList<VirtualFile>();
Map<VirtualFile, AccessStatus> statuses = getRegisteredFiles(myProject);
for (VirtualFile each : files) {
if (statuses.get(each) == AccessStatus.ALLOWED) continue;
if (!(each.getFileSystem() instanceof LocalFileSystem)) continue; // do not block e.g., HttpFileSystem, LightFileSystem etc.
if (isProjectFile(each, myProject)) {
statuses.remove(each);
continue;
}
statuses.put(each, AccessStatus.REQUESTED);
deniedFiles.add(each);
}
if (isAllAccessAllowed()) return Collections.emptyList();
List<VirtualFile> deniedFiles = Stream.of(files).filter(o -> !isWriteAccessAllowed(o)).collect(Collectors.toList());
if (deniedFiles.isEmpty()) return Collections.emptyList();
UnlockOption unlockOption = askToUnlock(deniedFiles);
@@ -116,12 +101,13 @@ public class NonProjectFileWritingAccessProvider extends WritingAccessProvider {
switch (unlockOption) {
case UNLOCK:
for (VirtualFile eachAllowed : deniedFiles) {
statuses.put(eachAllowed, AccessStatus.ALLOWED);
}
allowWriting(deniedFiles);
break;
case UNLOCK_DIR:
allowWriting(ContainerUtil.map(deniedFiles, VirtualFile::getParent));
break;
case UNLOCK_ALL:
myProject.putUserData(ALL_ACCESS_ALLOWED, Boolean.TRUE);
ACCESS_ALLOWED.getValue(getApp()).incrementAndGet();
break;
}
@@ -137,67 +123,98 @@ public class NonProjectFileWritingAccessProvider extends WritingAccessProvider {
return dialog.getUnlockOption();
}
public static boolean isWriteAccessAllowedExplicitly(@NotNull VirtualFile file, @NotNull Project project) {
if (!(file.getFileSystem() instanceof LocalFileSystem)) return false;
Map<VirtualFile, AccessStatus> statuses = getRegisteredFiles(project);
return statuses.get(file) == AccessStatus.ALLOWED ||
isProjectFile(file, project);
}
public static boolean isWriteAccessAllowed(@NotNull VirtualFile file) {
if (isAllAccessAllowed()) return true;
if (file.isDirectory()) return true;
private static boolean isProjectFile(@NotNull VirtualFile file, @NotNull Project project) {
ProjectFileIndex fileIndex = ProjectFileIndex.SERVICE.getInstance(project);
if (fileIndex.isInContent(file)) return true;
if (!Registry.is("ide.hide.excluded.files") && fileIndex.isExcluded(file) && !fileIndex.isUnderIgnored(file)) return true;
if (project instanceof ProjectEx && !project.isDefault()) {
if (ProjectUtil.isDirectoryBased(project)) {
VirtualFile baseDir = project.getBaseDir();
VirtualFile dotIdea = baseDir == null ? null : baseDir.findChild(Project.DIRECTORY_STORE_FOLDER);
if (dotIdea != null && VfsUtilCore.isAncestor(dotIdea, file, false)) return true;
}
IProjectStore store = (IProjectStore)ServiceKt.getStateStore(project);
String filePath = file.getPath();
if (FileUtil.namesEqual(filePath, store.getWorkspaceFilePath()) || FileUtil.namesEqual(filePath, store.getProjectFilePath())) {
return true;
}
for (Module module : ModuleManager.getInstance(project).getModules()) {
if (FileUtil.namesEqual(filePath, module.getModuleFilePath())) {
return true;
}
}
}
for (NonProjectFileWritingAccessExtension each : Extensions.getExtensions(NonProjectFileWritingAccessExtension.EP_NAME, project)) {
if(each.isWritable(file)) return true;
}
return false;
}
@TestOnly
public static void enableChecksInTests(@NotNull Project project, boolean enable) {
project.putUserData(ENABLE_IN_TESTS, enable ? Boolean.TRUE : null);
}
private static boolean allAccessAllowed(@NotNull Project project) {
// disable checks in tests, if not asked
if (ApplicationManager.getApplication().isUnitTestMode() && project.getUserData(ENABLE_IN_TESTS) != Boolean.TRUE) {
if (!(file.getFileSystem() instanceof LocalFileSystem)) return true;
if (file.getFileSystem() instanceof TempFileSystem) return true;
if (!getApp().isUnitTestMode()
&& FileUtil.isAncestor(new File(FileUtil.getTempDirectory()), VfsUtilCore.virtualToIoFile(file), true)) {
return true;
}
return project.getUserData(ALL_ACCESS_ALLOWED) == Boolean.TRUE;
VirtualFile each = file;
while (each != null) {
if (ACCESS_ALLOWED.getValue(each).get() > 0) return true;
each = each.getParent();
}
return isProjectFile(file);
}
@Nullable
public static AccessStatus getAccessStatus(@NotNull Project project, @NotNull VirtualFile file) {
return allAccessAllowed(project) ? AccessStatus.ALLOWED : getRegisteredFiles(project).get(file);
private static boolean isProjectFile(@NotNull VirtualFile file) {
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
ProjectFileIndex fileIndex = ProjectFileIndex.SERVICE.getInstance(project);
if (fileIndex.isInContent(file)) return true;
if (!Registry.is("ide.hide.excluded.files") && fileIndex.isExcluded(file) && !fileIndex.isUnderIgnored(file)) return true;
if (project instanceof ProjectEx && !project.isDefault()) {
if (ProjectUtil.isDirectoryBased(project)) {
VirtualFile baseDir = project.getBaseDir();
VirtualFile dotIdea = baseDir == null ? null : baseDir.findChild(Project.DIRECTORY_STORE_FOLDER);
if (dotIdea != null && VfsUtilCore.isAncestor(dotIdea, file, false)) return true;
}
IProjectStore store = (IProjectStore)ServiceKt.getStateStore(project);
String filePath = file.getPath();
if (FileUtil.namesEqual(filePath, store.getWorkspaceFilePath()) || FileUtil.namesEqual(filePath, store.getProjectFilePath())) {
return true;
}
for (Module module : ModuleManager.getInstance(project).getModules()) {
if (FileUtil.namesEqual(filePath, module.getModuleFilePath())) {
return true;
}
}
}
for (NonProjectFileWritingAccessExtension each : Extensions.getExtensions(NonProjectFileWritingAccessExtension.EP_NAME, project)) {
if(each.isWritable(file)) return true;
}
}
return false;
}
@NotNull
private static Map<VirtualFile, AccessStatus> getRegisteredFiles(@NotNull Project project) {
return ACCESS_STATUS.getValue(project);
public static void allowWriting(Collection<VirtualFile> allowedFiles) {
for (VirtualFile eachAllowed : allowedFiles) {
ACCESS_ALLOWED.getValue(eachAllowed).incrementAndGet();
}
}
public enum UnlockOption {UNLOCK, UNLOCK_ALL}
public static void allowWriting(VirtualFile... allowedFiles) {
allowWriting(Arrays.asList(allowedFiles));
}
public static void disableChecksDuring(@NotNull Runnable runnable) {
Application app = getApp();
ACCESS_ALLOWED.getValue(app).incrementAndGet();
try {
runnable.run();
}
finally {
ACCESS_ALLOWED.getValue(app).decrementAndGet();
}
}
@TestOnly
public static void enableChecksInTests(boolean enable) {
getApp().putUserData(ENABLE_IN_TESTS, enable ? Boolean.TRUE : null);
getApp().putUserData(ACCESS_ALLOWED, null);
}
private static boolean isAllAccessAllowed() {
Application app = getApp();
// disable checks in tests, if not asked
if (app.isUnitTestMode() && app.getUserData(ENABLE_IN_TESTS) != Boolean.TRUE) {
return true;
}
return ACCESS_ALLOWED.getValue(app).get() > 0;
}
private static Application getApp() {
return ApplicationManager.getApplication();
}
public enum UnlockOption {UNLOCK, UNLOCK_DIR, UNLOCK_ALL}
}
@@ -25,6 +25,7 @@ import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.fileEditor.impl.EditorWindow;
import com.intellij.openapi.fileEditor.impl.EditorWithProviderComposite;
import com.intellij.openapi.fileEditor.impl.NonProjectFileWritingAccessProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
@@ -75,6 +76,8 @@ public class FileDropHandler implements EditorDropHandler {
final VirtualFile vFile = fileSystem.refreshAndFindFileByIoFile(file);
final FileEditorManagerEx fileEditorManager = (FileEditorManagerEx) FileEditorManager.getInstance(project);
if (vFile != null) {
NonProjectFileWritingAccessProvider.allowWriting(vFile);
if (editorWindow != null) {
fileEditorManager.openFileWithProviders(vFile, true, editorWindow);
}
@@ -21,17 +21,19 @@ import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.CollectionComboBoxModel;
import com.intellij.ui.ColoredListCellRendererWrapper;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.Function;
import com.intellij.util.ui.OptionsDialog;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -186,4 +188,19 @@ public class ReadOnlyStatusDialog extends OptionsDialog {
final JRootPane pane = getRootPane();
return pane != null ? pane.getDefaultButton() : null;
}
}
@NotNull
public static String getTheseFilesMessage(Collection<VirtualFile> files) {
boolean dirsOnly = true;
for (VirtualFile each : files) {
if (!each.isDirectory()) {
dirsOnly = false;
break;
}
}
int size = files.size();
return StringUtil.pluralize("this", size) + " " + StringUtil.pluralize((dirsOnly ? "directory" : "file"), size);
}
}
@@ -54,4 +54,5 @@ com.fedot.idea.requirejs 0.24
com.widerwille.quicklook 0.8
com.phpuaca 1.3.1 1.2.1
coffeescript-idea 0.1-1-gae5a
RESTClient 1.2
RESTClient 1.2
krasa.nonProjectFilesUnlocker 1.0 1.1
@@ -17,6 +17,7 @@ package com.intellij.openapi.fileEditor;
import com.intellij.openapi.actionSystem.DataConstants;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.WriteCommandAction;
@@ -31,7 +32,6 @@ import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.io.FileUtil;
@@ -39,7 +39,6 @@ import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.WritingAccessProvider;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.ui.EditorNotifications;
import com.intellij.ui.EditorNotificationsImpl;
import com.intellij.util.NullableFunction;
@@ -47,18 +46,21 @@ import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
private Set<VirtualFile> myOpenedFiles = new THashSet<>();
private Set<VirtualFile> myCreatedFiles = new THashSet<>();
@Override
public void setUp() throws Exception {
super.setUp();
EditorNotifications notifications = new EditorNotificationsImpl(getProject());
((ComponentManagerImpl)getProject()).registerComponentInstance(EditorNotifications.class, notifications);
NonProjectFileWritingAccessProvider.enableChecksInTests(getProject(), true);
NonProjectFileWritingAccessProvider.enableChecksInTests(true);
ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges();
}
@@ -66,11 +68,21 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
protected void tearDown() throws Exception {
try {
NonProjectFileWritingAccessProvider.setCustomUnlocker(null);
NonProjectFileWritingAccessProvider.enableChecksInTests(getProject(), false);
NonProjectFileWritingAccessProvider.enableChecksInTests(false);
FileEditorManager editorManager = FileEditorManager.getInstance(getProject());
for (VirtualFile file : myOpenedFiles) {
editorManager.closeFile(file);
}
ApplicationManager.getApplication().runWriteAction(() -> {
for (VirtualFile each : myCreatedFiles) {
try {
each.delete(this);
}
catch (IOException e) {
e.printStackTrace();
}
}
});
}
finally {
super.tearDown();
@@ -91,14 +103,32 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
typeAndCheck(nonProjectFile, null, true); // still allowed after previous Unlock
}
public void testDoNotLockCreatedAndCopiedFiles() throws Exception {
VirtualFile nonProjectFile = createNonProjectFile();
typeAndCheck(nonProjectFile, false);
final VirtualFile[] createdNonProject = new VirtualFile[1];
final VirtualFile[] copiedNonProject = new VirtualFile[1];
new WriteAction<Object>() {
@Override
protected void run(@NotNull Result<Object> result) throws Throwable {
createdNonProject[0] = nonProjectFile.getParent().createChildData(this, "createdNonProject.txt");
copiedNonProject[0] = nonProjectFile.copy(this, nonProjectFile.getParent(), "copiedNonProject.txt");
myCreatedFiles.add(createdNonProject[0]);
myCreatedFiles.add(copiedNonProject[0]);
}
}.execute();
typeAndCheck(createdNonProject[0], true);
typeAndCheck(copiedNonProject[0], true);
typeAndCheck(nonProjectFile, false); // original is still locked
}
public void testAccessToProjectSystemFiles() throws Exception {
PlatformTestUtil.saveProject(getProject());
VirtualFile fileUnderProjectDir = new WriteAction<VirtualFile>() {
@Override
protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
result.setResult(getProject().getBaseDir().createChildData(this, "fileUnderProjectDir.txt"));
}
}.execute().getResultObject();
VirtualFile fileUnderProjectDir = createFileExternally(new File(getProject().getBaseDir().getPath()));
assertFalse(ProjectFileIndex.SERVICE.getInstance(getProject()).isInContent(fileUnderProjectDir));
@@ -128,18 +158,14 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
}.execute().getResultObject();
PlatformTestUtil.saveProject(getProject());
VirtualFile fileUnderModuleDir = new WriteAction<VirtualFile>() {
@Override
protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
result.setResult(moduleWithoutContentRoot.getModuleFile().getParent().createChildData(this, "fileUnderModuleDir.txt"));
}
}.execute().getResultObject();
VirtualFile fileUnderNonProjectModuleDir
= createFileExternally(new File(moduleWithoutContentRoot.getModuleFile().getParent().getPath()));
assertFalse(ProjectFileIndex.SERVICE.getInstance(getProject()).isInContent(fileUnderModuleDir));
assertFalse(ProjectFileIndex.SERVICE.getInstance(getProject()).isInContent(fileUnderNonProjectModuleDir));
typeAndCheck(moduleWithoutContentRoot.getModuleFile(), true);
typeAndCheck(myModule.getModuleFile(), true);
typeAndCheck(fileUnderModuleDir, false);
typeAndCheck(fileUnderNonProjectModuleDir, false);
}
public void testAllowEditingInOneFileOnly() throws Exception {
@@ -157,6 +183,31 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
typeAndCheck(nonProjectFile2, null, true);
}
public void testAllowEditingFileInDirectory() throws Exception {
VirtualFile nonProjectFileDir11 = createNonProjectFile();
File dir = new File(nonProjectFileDir11.getParent().getPath());
VirtualFile nonProjectFileDir12 = createFileExternally(dir);
File subDir = new File(dir, "subdir");
subDir.mkdirs();
VirtualFile nonProjectFileDirSubdir1 = createFileExternally(subDir);
VirtualFile nonProjectFileDir2 = createNonProjectFile();
typeAndCheck(nonProjectFileDir11, false);
typeAndCheck(nonProjectFileDir12, false);
typeAndCheck(nonProjectFileDir2, false);
typeAndCheck(nonProjectFileDir11, NonProjectFileWritingAccessProvider.UnlockOption.UNLOCK_DIR, true);
// affects other files in dir
typeAndCheck(nonProjectFileDir12, true);
typeAndCheck(nonProjectFileDirSubdir1, true);
// doesn't affect files in other dirs
typeAndCheck(nonProjectFileDir2, false);
}
public void testAllowEditingInAllFiles() throws Exception {
VirtualFile nonProjectFile1 = createNonProjectFile();
VirtualFile nonProjectFile2 = createNonProjectFile();
@@ -201,57 +252,7 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
false); // can't write since denied by another write-access provider
typeAndCheck(nonProjectFile2, true);
}
public void testClearingInfoForDeletedFiles() throws Exception {
final VirtualFile nonProjectFile1 = createNonProjectFile();
final VirtualFile nonProjectFile2 = createNonProjectFile();
typeAndCheck(nonProjectFile1, false);
typeAndCheck(nonProjectFile2, false);
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile1));
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile2));
new WriteAction<Object>() {
@Override
protected void run(@NotNull Result<Object> result) throws Throwable {
nonProjectFile1.delete(this);
}
}.execute();
assertNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile1));
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile2));
}
public void testUpdatingNotificationsOnRootChanges() throws Exception {
VirtualFile nonProjectFile1 = createNonProjectFile();
VirtualFile nonProjectFile2 = createNonProjectFile();
typeAndCheck(nonProjectFile1, false);
typeAndCheck(nonProjectFile2, false);
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile1));
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile2));
ContentEntry contextRoot = PsiTestUtil.addContentRoot(myModule, nonProjectFile2.getParent());
// removing notification panel for newly added files
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile1));
assertNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile2));
PsiTestUtil.removeContentEntry(myModule, contextRoot.getFile());
// do not add notification panel until access is requested
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile1));
assertNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile2));
// but files are still not writable
typeAndCheck(nonProjectFile1, false);
typeAndCheck(nonProjectFile2, false);
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile1));
assertNotNull(NonProjectFileWritingAccessProvider.getAccessStatus(getProject(), nonProjectFile2));
}
public void testCheckingExtensions() throws Exception {
VirtualFile nonProjectFile1 = createNonProjectFile();
VirtualFile nonProjectFile2 = createNonProjectFile();
@@ -312,23 +313,27 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase {
@NotNull
private VirtualFile createProjectFile() {
return new WriteAction<VirtualFile>() {
@Override
protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
result.setResult(ModuleRootManager.getInstance(myModule).getContentRoots()[0].createChildData(this, "projectFile.txt"));
}
}.execute().getResultObject();
return createFileExternally(new File(ModuleRootManager.getInstance(myModule).getContentRoots()[0].getPath()));
}
@NotNull
private VirtualFile createNonProjectFile() {
return new WriteAction<VirtualFile>() {
private VirtualFile createNonProjectFile() throws IOException {
return createFileExternally(FileUtil.createTempDirectory("tmp", null));
}
@NotNull
private VirtualFile createFileExternally(File dir) {
VirtualFile result = new WriteAction<VirtualFile>() {
@Override
protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
VirtualFile tmp = LocalFileSystem.getInstance().findFileByIoFile(FileUtil.createTempDirectory("tmp", null));
result.setResult(tmp.createChildData(this, "nonProjectFile.txt"));
// create externally, since files created via VFS are marked for editing automatically
File file = new File(dir, FileUtil.createSequentFileName(dir, "file", "txt"));
file.createNewFile();
result.setResult(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file));
}
}.execute().getResultObject();
myCreatedFiles.add(result);
return result;
}
private void typeAndCheck(VirtualFile file, boolean fileHasBeenChanged) {
@@ -14,7 +14,7 @@
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="2" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="etched"/>
<border type="none"/>
<children>
<component id="e62df" class="javax.swing.JRadioButton" binding="myShelveChangesRadioButton" default-binding="true">
<constraints>
@@ -51,15 +51,15 @@
</component>
</children>
</grid>
<component id="dbd63" class="javax.swing.JLabel">
<component id="dbd63" class="javax.swing.JLabel" binding="myListTitle">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="&lt;html&gt;These files do not belong to active changelist:&lt;/html&gt;"/>
<text value="list title"/>
</properties>
</component>
<scrollpane class="com.intellij.ui.components.JBScrollPane" id="6c540">
<scrollpane id="6c540" class="com.intellij.ui.components.JBScrollPane">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
@@ -18,11 +18,13 @@ package com.intellij.openapi.vcs.changes.conflicts;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.changes.ChangeList;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ChangeListManagerImpl;
import com.intellij.openapi.vcs.readOnlyHandler.FileListRenderer;
import com.intellij.openapi.vcs.readOnlyHandler.ReadOnlyStatusDialog;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.CollectionListModel;
import org.jetbrains.annotations.NotNull;
@@ -42,6 +44,7 @@ public class ChangelistConflictDialog extends DialogWrapper {
private JRadioButton myMoveChangesToActiveRadioButton;
private JRadioButton mySwitchToChangelistRadioButton;
private JRadioButton myIgnoreRadioButton;
private JLabel myListTitle;
private JList myFileList;
private final Project myProject;
@@ -51,6 +54,10 @@ public class ChangelistConflictDialog extends DialogWrapper {
myProject = project;
setTitle("Resolve Changelist Conflict");
myListTitle.setText(StringUtil.capitalize(ReadOnlyStatusDialog.getTheseFilesMessage(conflicts))
+ " " + (conflicts.size() > 1 ? "do" : "does")
+ " not belong to the active changelist:");
myFileList.setCellRenderer(new FileListRenderer());
myFileList.setModel(new CollectionListModel(conflicts));