diff --git a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties index 05e4701795fa..5113c7f3593d 100644 --- a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties +++ b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties @@ -638,4 +638,6 @@ inspection.extract.method.message=Extract method returning ''{0}'' inspection.extract.method.option.min.length=Minimum length of code to extract:|symbols inspection.extract.method.option.max.parameters=Maximum number of parameters: inspection.extract.method.dont.suggest.parameters=Don''t suggest to extract method with {0} or more parameters -inspection.extract.method.dont.suggest.length=Don\u2019t suggest to extract methods as short as this \ No newline at end of file +inspection.extract.method.dont.suggest.length=Don\u2019t suggest to extract methods as short as this +notification.file.system.issue=File Operation Issue +notification.content.cannot.move.file=Cannot move ''{0}'' into ''{1}'': {2} diff --git a/java/java-analysis-impl/src/META-INF/JavaAnalysisPlugin.xml b/java/java-analysis-impl/src/META-INF/JavaAnalysisPlugin.xml index f73c88a6cdb7..2b594f9f9201 100644 --- a/java/java-analysis-impl/src/META-INF/JavaAnalysisPlugin.xml +++ b/java/java-analysis-impl/src/META-INF/JavaAnalysisPlugin.xml @@ -320,5 +320,6 @@ + diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveFileFix.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveFileFix.java index 6a4d12de62f5..0a15c403df50 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveFileFix.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/MoveFileFix.java @@ -1,24 +1,16 @@ -/* - * Copyright 2000-2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInsight.daemon.impl.quickfix; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo; import com.intellij.codeInspection.util.IntentionName; +import com.intellij.java.analysis.JavaAnalysisBundle; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationGroup; +import com.intellij.notification.NotificationType; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; @@ -63,14 +55,26 @@ public class MoveFileFix implements IntentionAction { @Override public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + int offset = -1; + if (editor.getVirtualFile().equals(myFile)) { + offset = editor.getCaretModel().getOffset(); + } if (myFile.isValid() && myTarget.isValid()) { try { myFile.move(this, myTarget); } catch (IOException e) { - throw new IncorrectOperationException("Cannot move '" + myFile.getPath() + "' into '" + myTarget.getPath() + "'", (Throwable)e); + new Notification("FileSystemIssue", + JavaAnalysisBundle.message("notification.content.cannot.move.file", myFile.getPath(), myTarget.getPath(), e.getMessage()), + NotificationType.ERROR) + .notify(project); } } + FileEditorManager manager = FileEditorManager.getInstance(project); + manager.closeFile(myFile); + if (offset >= 0) { + manager.openTextEditor(new OpenFileDescriptor(project, myFile, offset), true); + } } @Override diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 13f532f0b135..6f02e240f283 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -2398,6 +2398,7 @@ + diff --git a/java/java-impl/src/com/intellij/ide/FileNotInSourceRootChecker.kt b/java/java-impl/src/com/intellij/ide/FileNotInSourceRootChecker.kt new file mode 100644 index 000000000000..8c272b7f4120 --- /dev/null +++ b/java/java-impl/src/com/intellij/ide/FileNotInSourceRootChecker.kt @@ -0,0 +1,120 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ide + +import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx +import com.intellij.codeInsight.daemon.impl.HighlightInfo +import com.intellij.codeInsight.daemon.impl.HighlightInfoType +import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil +import com.intellij.codeInsight.daemon.impl.quickfix.MoveFileFix +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.ide.util.PropertiesComponent +import com.intellij.java.JavaBundle +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.roots.ui.configuration.DefaultModulesProvider +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaFile + +class FileNotInSourceRootChecker : ProjectActivity { + override suspend fun execute(project: Project) { + project.getService(FileNotInSourceRootService::class.java).init() + } +} + +private const val GROUP : Int = 1234 +private const val JAVA_DONT_CHECK_OUT_OF_SOURCE_FILES : String = "com.intellij.ide.FileNotInSourceRootChecker.no.check" + +@Service(Service.Level.PROJECT) +class FileNotInSourceRootService(val project: Project) : Disposable { + + fun init() { + if (PropertiesComponent.getInstance(project).getBoolean(JAVA_DONT_CHECK_OUT_OF_SOURCE_FILES, false)) return + val editorFactory = EditorFactory.getInstance() + editorFactory.addEditorFactoryListener(object : EditorFactoryListener { + override fun editorCreated(event: EditorFactoryEvent) { + checkEditor(event.editor) + } + }, this) + editorFactory.allEditors.forEach { editor -> checkEditor(editor) } + } + + private fun checkEditor(editor: Editor) { + if (editor.project !== project) return + if (PropertiesComponent.getInstance(project).getBoolean(JAVA_DONT_CHECK_OUT_OF_SOURCE_FILES, false)) return + val virtualFile = editor.virtualFile + if (virtualFile == null) return + ReadAction.run { + val fileIndex = ProjectFileIndex.getInstance(project) + if (fileIndex.isInSource(virtualFile) || fileIndex.isExcluded(virtualFile) || fileIndex.isUnderIgnored(virtualFile)) return@run + if (!fileIndex.getOrderEntriesForFile(virtualFile).isEmpty()) return@run + val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) as? PsiJavaFile ?: return@run + val packageName = psiFile.packageName + val module = fileIndex.getModuleForFile(virtualFile) ?: return@run + val rootModel = DefaultModulesProvider.createForProject(project).getRootModel(module) + val roots = rootModel.sourceRoots + if (roots.isEmpty()) return@run + var root = roots[0] + if (packageName.isNotEmpty()) { + root = VfsUtil.findRelativeFile(root, *packageName.split('.').toTypedArray()) ?: root + } + if (root.findChild(virtualFile.name) != null) return@run + val moveFileFix = MoveFileFix(virtualFile, root, JavaBundle.message("fix.move.to.source.root")) + val info = HighlightInfo.newHighlightInfo(HighlightInfoType.WARNING) + .range(psiFile) + .description(JavaBundle.message("warning.java.file.outside.source.root")) + .fileLevelAnnotation() + .group(GROUP) + .registerFix(moveFileFix, listOf(DismissFix(), IgnoreForThisProjectFix()), null, null, null) + ApplicationManager.getApplication().invokeLater { + UpdateHighlightersUtil.setHighlightersToSingleEditor(project, editor, 0, editor.document.textLength, + listOf(info.create()), null, GROUP) + } + } + } + + override fun dispose() { + } + + class DismissFix: IntentionAction { + override fun startInWriteAction(): Boolean = false + override fun getText(): String = familyName + override fun getFamilyName(): String = JavaBundle.message("intention.family.name.dismiss") + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = true + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + file ?: return + DaemonCodeAnalyzerEx.getInstanceEx(project).cleanFileLevelHighlights(GROUP, file) + } + } + + class IgnoreForThisProjectFix: IntentionAction { + override fun startInWriteAction(): Boolean = false + override fun getText(): String = familyName + override fun getFamilyName(): String = JavaBundle.message("intention.family.name.ignore.project") + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = true + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + file ?: return + val codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project) + PropertiesComponent.getInstance(project).setValue(JAVA_DONT_CHECK_OUT_OF_SOURCE_FILES, true) + codeAnalyzer.cleanFileLevelHighlights(GROUP, file) + for (ed in EditorFactory.getInstance().allEditors) { + if (ed.project != project) continue + val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(ed.document) as? PsiJavaFile ?: continue + if (psiFile == file) continue + codeAnalyzer.cleanFileLevelHighlights(GROUP, psiFile) + } + } + } +} diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 1e2e016fcfc0..03e961c4770f 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -1813,3 +1813,7 @@ hint.text.not.valid.java.identifier=Not a valid Java identifier command.name.replace.type=Replace Type java.completion.tag=Tag: java.completion.tags=Tags: +intention.family.name.dismiss=Dismiss +intention.family.name.ignore.project=Do not show again in this project +fix.move.to.source.root=Move to source root +warning.java.file.outside.source.root=Java file is located outside of the module source root, so it won't be compiled