[java] IDEA-210466 Suggest to move Java file to the source root

GitOrigin-RevId: f225ebbb2bcb6084b36ca504aae4e5463c074fc5
This commit is contained in:
Tagir Valeev
2023-03-06 15:05:37 +00:00
committed by intellij-monorepo-bot
parent b5808d2c51
commit f5effaaeb3
6 changed files with 149 additions and 17 deletions
@@ -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
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}
@@ -320,5 +320,6 @@
<lang.jvm.annotationPackageSupport implementation="com.intellij.codeInsight.annoPackages.CheckerFrameworkSupport"/>
<lang.jvm.annotationPackageSupport implementation="com.intellij.codeInsight.annoPackages.EclipseAnnotationSupport"/>
<lang.jvm.annotationPackageSupport implementation="com.intellij.codeInsight.annoPackages.JSpecifyAnnotationSupport"/>
<notificationGroup id="FileSystemIssue" displayType="BALLOON" bundle="messages.JavaAnalysisBundle" key="notification.file.system.issue"/>
</extensions>
</idea-plugin>
@@ -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
@@ -2398,6 +2398,7 @@
<dataflowIRProvider language="JAVA" implementationClass="com.intellij.codeInspection.dataFlow.java.JavaDataFlowIRProvider"/>
<java.effectively.final.fixer implementation="com.intellij.codeInsight.daemon.impl.quickfix.makefinal.MoveInitializerToIfBranchFixer"/>
<java.effectively.final.fixer implementation="com.intellij.codeInspection.streamMigration.ConvertToStreamFixer"/>
<postStartupActivity implementation="com.intellij.ide.FileNotInSourceRootChecker"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains">
@@ -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<RuntimeException> {
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)
}
}
}
}
@@ -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