From 6ffa2055db9581425c3184ba53908ce4e873b2ba Mon Sep 17 00:00:00 2001 From: Anton Makeev Date: Sat, 28 May 2016 11:51:31 +0200 Subject: [PATCH] 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 --- .../refactoring/BaseRefactoringProcessor.java | 15 +- .../rename/PsiElementRenameHandler.java | 2 +- .../intellij/ide/actions/OpenFileAction.java | 4 +- .../NonProjectFileWritingAccessDialog.form | 26 ++- .../NonProjectFileWritingAccessDialog.java | 65 +++++- .../NonProjectFileWritingAccessProvider.java | 215 ++++++++++-------- .../fileEditor/impl/text/FileDropHandler.java | 3 + .../readOnlyHandler/ReadOnlyStatusDialog.java | 21 +- .../platform-resources/src/brokenPlugins.txt | 3 +- .../fileEditor/NonProjectFileAccessTest.java | 163 ++++++------- .../conflicts/ChangelistConflictDialog.form | 8 +- .../conflicts/ChangelistConflictDialog.java | 7 + 12 files changed, 323 insertions(+), 209 deletions(-) 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..efcce0243dfb 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))) { 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 5e0be6d90835..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; @@ -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); } 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..6562b6bd4627 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,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 nonProjectFiles) { + 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("Non-Project Files Access"); + setTitle(filesType + " Protection"); 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 +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() { 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 80fd36ffae83..c7c862519c3c 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,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 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()); @NotNull private final Project myProject; @Nullable private static NullableFunction, 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 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); - } - } + 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 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)).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 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 getRegisteredFiles(@NotNull Project project) { - return ACCESS_STATUS.getValue(project); + public static void allowWriting(Collection 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} } 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 5eacb84780f2..09671be35552 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,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; } -} \ No newline at end of file + + @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); + } +} diff --git a/platform/platform-resources/src/brokenPlugins.txt b/platform/platform-resources/src/brokenPlugins.txt index 81f52b7e31a0..8160bea433df 100644 --- a/platform/platform-resources/src/brokenPlugins.txt +++ b/platform/platform-resources/src/brokenPlugins.txt @@ -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 \ No newline at end of file +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..0c008e9bd5cb 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(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() { + @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 +158,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 +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() { - @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 { VirtualFile nonProjectFile1 = createNonProjectFile(); VirtualFile nonProjectFile2 = createNonProjectFile(); @@ -312,23 +313,27 @@ public class NonProjectFileAccessTest extends HeavyFileEditorManagerTestCase { @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));