diff --git a/platform/lang-impl/src/com/intellij/refactoring/BaseRefactoringProcessor.java b/platform/lang-impl/src/com/intellij/refactoring/BaseRefactoringProcessor.java index 60b05dae04a7..aa1a1a98f93a 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/BaseRefactoringProcessor.java +++ b/platform/lang-impl/src/com/intellij/refactoring/BaseRefactoringProcessor.java @@ -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); } } diff --git a/platform/lang-impl/src/com/intellij/refactoring/rename/PsiElementRenameHandler.java b/platform/lang-impl/src/com/intellij/refactoring/rename/PsiElementRenameHandler.java index ede322cb6ef4..2b1f9761b7c2 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/rename/PsiElementRenameHandler.java +++ b/platform/lang-impl/src/com/intellij/refactoring/rename/PsiElementRenameHandler.java @@ -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, project))) { String message = RefactoringBundle.message("error.out.of.project.element", UsageViewUtil.getType(element)); return RefactoringBundle.getCannotRefactorMessage(message); } diff --git a/platform/platform-impl/src/com/intellij/ide/actions/OpenFileAction.java b/platform/platform-impl/src/com/intellij/ide/actions/OpenFileAction.java index 542fb8d3f780..06bd381c3f96 100644 --- a/platform/platform-impl/src/com/intellij/ide/actions/OpenFileAction.java +++ b/platform/platform-impl/src/com/intellij/ide/actions/OpenFileAction.java @@ -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; @@ -126,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); } diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.form b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.form index 01173794cbeb..edb8c4e4be4e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.form +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.form @@ -8,39 +8,48 @@ - + - + - + - - + + + + + + + + + + - + + - + - + @@ -64,6 +73,7 @@ + diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.java index 397ff73da184..79965dd52221 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessDialog.java @@ -17,33 +17,87 @@ 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 nonProjectFiles) { - super(project); - setTitle("Non-Project Files Access"); + public NonProjectFileWritingAccessDialog(@NotNull Project project, @NotNull List nonProjectFiles) { + this(project, nonProjectFiles, "Non-Project Files"); + } + public NonProjectFileWritingAccessDialog(@NotNull Project project, + @NotNull List nonProjectFiles, + @NotNull String filesType) { + super(project); + setTitle(filesType + " Protection"); + + myFileList.setPreferredSize(ReadOnlyStatusDialog.getDialogPreferredSize()); + myFileList.setCellRenderer(new FileListRenderer()); myFileList.setModel(new CollectionListModel(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 +107,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() { diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessExtension.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessExtension.java index 3955916923d4..ffc0c59dc877 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessExtension.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessExtension.java @@ -23,5 +23,17 @@ public interface NonProjectFileWritingAccessExtension { ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.nonProjectFileWritingAccessExtension"); - boolean isWritable(@NotNull VirtualFile file); + /** + * @return true if the file should not be protected from accidental writing. false to use default logic. + */ + default boolean isWritable(@NotNull VirtualFile file) { + return false; + } + + /** + * @return true if the file should be protected from accidental writing. false to use default logic. + */ + default boolean isNotWritable(@NotNull VirtualFile file) { + return false; + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessProvider.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessProvider.java index 0b5992e80796..5b8913c91258 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessProvider.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/NonProjectFileWritingAccessProvider.java @@ -15,8 +15,9 @@ */ package com.intellij.openapi.fileEditor.impl; -import com.intellij.ProjectTopics; import com.intellij.ide.impl.ProjectUtil; +import com.intellij.openapi.Disposable; +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; @@ -25,29 +26,37 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; 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.Disposer; 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.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.AtomicBoolean; +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 ENABLE_IN_TESTS = Key.create("NON_PROJECT_FILE_ACCESS_ENABLE_IN_TESTS"); - private static final Key ALL_ACCESS_ALLOWED = Key.create("NON_PROJECT_FILE_ALL_ACCESS_STATUS"); - private static final NotNullLazyKey, Project> ACCESS_STATUS - = NotNullLazyKey.create("NON_PROJECT_FILE_ACCESS_STATUS", project -> new HashMap()); + private static final NotNullLazyKey ACCESS_ALLOWED + = NotNullLazyKey.create("NON_PROJECT_FILE_ACCESS", holder -> new AtomicInteger()); + + private static final AtomicBoolean myInitialized = new AtomicBoolean(); @NotNull private final Project myProject; @Nullable private static NullableFunction, UnlockOption> ourCustomUnlocker; @@ -57,28 +66,12 @@ 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()); - } - }, project); - - myProject.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() { - @Override - public void rootsChanged(ModuleRootEvent event) { - Map files = getRegisteredFiles(project); - - // reset access status and notifications for files that became project files - for (VirtualFile each : new ArrayList(files.keySet())) { - if (isProjectFile(each, project)) { - files.remove(each); - } - } - } - }); + + if (myInitialized.compareAndSet(false, true)) { + VirtualFileManager.getInstance().addVirtualFileListener(new OurVirtualFileAdapter()); + } } @Override @@ -89,24 +82,9 @@ public class NonProjectFileWritingAccessProvider extends WritingAccessProvider { @NotNull @Override public Collection requestWriting(VirtualFile... files) { - if (allAccessAllowed(myProject)) return Collections.emptyList(); - - List deniedFiles = new SmartList(); - - Map 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 deniedFiles = Stream.of(files).filter(o -> !isWriteAccessAllowed(o, myProject)).collect(Collectors.toList()); if (deniedFiles.isEmpty()) return Collections.emptyList(); UnlockOption unlockOption = askToUnlock(deniedFiles); @@ -115,12 +93,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; } @@ -136,14 +115,33 @@ 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 statuses = getRegisteredFiles(project); - return statuses.get(file) == AccessStatus.ALLOWED || - isProjectFile(file, project); + public static boolean isWriteAccessAllowed(@NotNull VirtualFile file, @NotNull Project project) { + if (isAllAccessAllowed()) return true; + if (file.isDirectory()) return true; + + if (!(file.getFileSystem() instanceof LocalFileSystem)) return true; // do not block e.g., HttpFileSystem, LightFileSystem etc. + if (file.getFileSystem() instanceof TempFileSystem) return true; + + if (!getApp().isUnitTestMode() + && FileUtil.isAncestor(new File(FileUtil.getTempDirectory()), VfsUtilCore.virtualToIoFile(file), true)) { + return true; + } + + VirtualFile each = file; + while (each != null) { + if (ACCESS_ALLOWED.getValue(each).get() > 0) return true; + each = each.getParent(); + } + + return isProjectFile(file, project); } private static boolean isProjectFile(@NotNull VirtualFile file, @NotNull Project project) { + for (NonProjectFileWritingAccessExtension each : Extensions.getExtensions(NonProjectFileWritingAccessExtension.EP_NAME, project)) { + if(each.isWritable(file)) return true; + if(each.isNotWritable(file)) return false; + } + 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; @@ -166,37 +164,70 @@ public class NonProjectFileWritingAccessProvider extends WritingAccessProvider { } } } - - 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); - } + public static void allowWriting(VirtualFile... allowedFiles) { + allowWriting(Arrays.asList(allowedFiles)); + } - private static boolean allAccessAllowed(@NotNull Project project) { + public static void allowWriting(Iterable allowedFiles) { + for (VirtualFile eachAllowed : allowedFiles) { + ACCESS_ALLOWED.getValue(eachAllowed).incrementAndGet(); + } + } + + 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(@NotNull Disposable disposable) { + getApp().putUserData(ENABLE_IN_TESTS, Boolean.TRUE); + getApp().putUserData(ACCESS_ALLOWED, null); + + Disposer.register(disposable, () -> { + getApp().putUserData(ENABLE_IN_TESTS, null); + getApp().putUserData(ACCESS_ALLOWED, null); + }); + } + + private static boolean isAllAccessAllowed() { + Application app = getApp(); + // disable checks in tests, if not asked - if (ApplicationManager.getApplication().isUnitTestMode() && project.getUserData(ENABLE_IN_TESTS) != Boolean.TRUE) { + if (app.isUnitTestMode() && app.getUserData(ENABLE_IN_TESTS) != Boolean.TRUE) { return true; } - - return project.getUserData(ALL_ACCESS_ALLOWED) == Boolean.TRUE; + return ACCESS_ALLOWED.getValue(app).get() > 0; } - @Nullable - public static AccessStatus getAccessStatus(@NotNull Project project, @NotNull VirtualFile file) { - return allAccessAllowed(project) ? AccessStatus.ALLOWED : getRegisteredFiles(project).get(file); + private static Application getApp() { + return ApplicationManager.getApplication(); } - @NotNull - private static Map getRegisteredFiles(@NotNull Project project) { - return ACCESS_STATUS.getValue(project); - } + public enum UnlockOption {UNLOCK, UNLOCK_DIR, UNLOCK_ALL} - public enum UnlockOption {UNLOCK, UNLOCK_ALL} + private static class OurVirtualFileAdapter extends VirtualFileAdapter { + @Override + public void fileCreated(@NotNull VirtualFileEvent event) { + unlock(event); + } + + @Override + public void fileCopied(@NotNull VirtualFileCopyEvent event) { + unlock(event); + } + + private static void unlock(@NotNull VirtualFileEvent event) { + if (!event.isFromRefresh() && !event.getFile().isDirectory()) allowWriting(event.getFile()); + } + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/FileDropHandler.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/FileDropHandler.java index bd0045b21e4d..f5a5b84a4870 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/FileDropHandler.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/FileDropHandler.java @@ -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); } diff --git a/platform/platform-impl/src/com/intellij/openapi/vcs/readOnlyHandler/ReadOnlyStatusDialog.java b/platform/platform-impl/src/com/intellij/openapi/vcs/readOnlyHandler/ReadOnlyStatusDialog.java index 2bd2b7146972..e946f60e9df5 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vcs/readOnlyHandler/ReadOnlyStatusDialog.java +++ b/platform/platform-impl/src/com/intellij/openapi/vcs/readOnlyHandler/ReadOnlyStatusDialog.java @@ -21,16 +21,20 @@ 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.ui.OptionsDialog; import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; import javax.swing.*; +import java.awt.*; 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; @@ -54,8 +58,9 @@ public class ReadOnlyStatusDialog extends OptionsDialog { super(project); setTitle(VcsBundle.message("dialog.title.clear.read.only.file.status")); myFiles = files; + myFileList.setPreferredSize(getDialogPreferredSize()); initFileList(); - + ActionListener listener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -185,4 +190,23 @@ public class ReadOnlyStatusDialog extends OptionsDialog { final JRootPane pane = getRootPane(); return pane != null ? pane.getDefaultButton() : null; } + + public static Dimension getDialogPreferredSize() { + return new Dimension(500, 400); + } + + @NotNull + public static String getTheseFilesMessage(Collection 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); + } } \ No newline at end of file diff --git a/platform/platform-resources/src/brokenPlugins.txt b/platform/platform-resources/src/brokenPlugins.txt index 7216877a3002..8160bea433df 100644 --- a/platform/platform-resources/src/brokenPlugins.txt +++ b/platform/platform-resources/src/brokenPlugins.txt @@ -55,3 +55,4 @@ com.widerwille.quicklook 0.8 com.phpuaca 1.3.1 1.2.1 coffeescript-idea 0.1-1-gae5a RESTClient 1.2 +krasa.nonProjectFilesUnlocker 1.0 1.1 diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/fileEditor/NonProjectFileAccessTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/fileEditor/NonProjectFileAccessTest.java index bab06fd044b3..d1e9b6666b24 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/fileEditor/NonProjectFileAccessTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/fileEditor/NonProjectFileAccessTest.java @@ -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 myOpenedFiles = new THashSet<>(); + private Set 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(myTestRootDisposable); ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges(); } @@ -66,11 +68,20 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase { protected void tearDown() throws Exception { try { NonProjectFileWritingAccessProvider.setCustomUnlocker(null); - NonProjectFileWritingAccessProvider.enableChecksInTests(getProject(), 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 +102,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() { + @Override + protected void run(@NotNull Result 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() { - @Override - protected void run(@NotNull Result 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 +157,14 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase { }.execute().getResultObject(); PlatformTestUtil.saveProject(getProject()); - VirtualFile fileUnderModuleDir = new WriteAction() { - @Override - protected void run(@NotNull Result 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 +182,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,58 +251,8 @@ 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() { - @Override - protected void run(@NotNull Result 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 { + + public void testCheckingExtensionsForWritableFiles() throws Exception { VirtualFile nonProjectFile1 = createNonProjectFile(); VirtualFile nonProjectFile2 = createNonProjectFile(); @@ -260,7 +260,7 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase { typeAndCheck(nonProjectFile2, false); List allowed = new ArrayList(); - registerAccessCheckExtension(allowed); + registerAccessCheckExtension(allowed, Collections.emptyList()); typeAndCheck(nonProjectFile1, false); typeAndCheck(nonProjectFile2, false); @@ -278,6 +278,33 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase { typeAndCheck(nonProjectFile1, false); typeAndCheck(nonProjectFile2, false); } + + public void testCheckingExtensionsForNonWritableFiles() throws Exception { + VirtualFile nonProjectFile1 = createProjectFile(); + VirtualFile nonProjectFile2 = createProjectFile(); + + typeAndCheck(nonProjectFile1, true); + typeAndCheck(nonProjectFile2, true); + + List denied = new ArrayList(); + registerAccessCheckExtension(Collections.emptyList(), denied); + + typeAndCheck(nonProjectFile1, true); + typeAndCheck(nonProjectFile2, true); + + denied.add(nonProjectFile1); + typeAndCheck(nonProjectFile1, false); + typeAndCheck(nonProjectFile2, true); + + denied.clear(); + denied.add(nonProjectFile2); + typeAndCheck(nonProjectFile1, true); + typeAndCheck(nonProjectFile2, false); + + denied.clear(); + typeAndCheck(nonProjectFile1, true); + typeAndCheck(nonProjectFile2, true); + } private Set registerWriteAccessProvider(final VirtualFile... filesToDeny) { final Set requested = new LinkedHashSet(); @@ -299,36 +326,45 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase { return requested; } - private void registerAccessCheckExtension(final Collection filesToAllow) { + private void registerAccessCheckExtension(Collection filesToAllow, Collection filesToDeny) { PlatformTestUtil.registerExtension(Extensions.getArea(getProject()), NonProjectFileWritingAccessExtension.EP_NAME, new NonProjectFileWritingAccessExtension() { @Override public boolean isWritable(@NotNull VirtualFile file) { return filesToAllow.contains(file); } + + @Override + public boolean isNotWritable(@NotNull VirtualFile file) { + return filesToDeny.contains(file); + } }, myTestRootDisposable); } @NotNull private VirtualFile createProjectFile() { - return new WriteAction() { - @Override - protected void run(@NotNull Result 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() { + private VirtualFile createNonProjectFile() throws IOException { + return createFileExternally(FileUtil.createTempDirectory("tmp", null)); + } + + @NotNull + private VirtualFile createFileExternally(File dir) { + VirtualFile result = new WriteAction() { @Override protected void run(@NotNull Result 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) { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.form b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.form index 951dab5e6f7d..7b26a15b238b 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.form +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.form @@ -14,7 +14,7 @@ - + @@ -51,15 +51,15 @@ - + - + - + diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.java index cba973307ca5..e8d9b2a96312 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/conflicts/ChangelistConflictDialog.java @@ -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)); diff --git a/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXRenameTest.java b/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXRenameTest.java index 08d8a137a00a..00ab88ec6225 100644 --- a/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXRenameTest.java +++ b/plugins/javaFX/javaFX-CE/testSrc/org/jetbrains/plugins/javaFX/fxml/JavaFXRenameTest.java @@ -21,6 +21,7 @@ import com.intellij.openapi.application.PluginPathManager; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiMethod; +import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.RenameRefactoring; import com.intellij.refactoring.openapi.impl.JavaRenameRefactoringImpl; import com.intellij.refactoring.rename.PsiElementRenameHandler; @@ -181,8 +182,7 @@ public class JavaFXRenameTest extends AbstractJavaFXRenameTest { } public void testStaticPropertyFromLibrary() throws Exception { - doTestErrorHint("Foo", "Cannot perform refactoring.\n" + - "Selected method is not located inside the project"); + doTestErrorHint("Foo", RefactoringBundle.getCannotRefactorMessage(RefactoringBundle.message("error.cannot.be.renamed"))); } public void testControllerMethod() throws Exception {