IDEA-238791: create tree visitor to update project view

GitOrigin-RevId: ce1619f07621087baf5cfdc8dd2086548eea4137
This commit is contained in:
Sergey Malenkov
2020-07-28 23:15:12 +00:00
committed by intellij-monorepo-bot
parent 4ccc9d206b
commit 3e40f1a28a
8 changed files with 170 additions and 111 deletions
@@ -30,7 +30,7 @@ internal class FileNode(parent: Node, val file: VirtualFile) : Node(parent) {
presentation.addText(" ${getLocationRelativeToUserHome(url)}", GRAYED_ATTRIBUTES)
}
val root = findAncestor(Root::class.java)
val count = root?.getProblemsCount(file) ?: 0
val count = root?.getFileProblemCount(file) ?: 0
if (count > 0) {
val text = ProblemsViewBundle.message("problems.view.file.problems", count)
presentation.addText(" $text", GRAYED_ATTRIBUTES)
@@ -2,15 +2,16 @@
package com.intellij.analysis.problemsView.toolWindow
import com.intellij.analysis.problemsView.Problem
import com.intellij.analysis.problemsView.ProblemsListener
import com.intellij.analysis.problemsView.ProblemsProvider
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFile
internal class HighlightingFileRoot(panel: ProblemsViewPanel, val file: VirtualFile)
: Root(panel, ProblemFilter(panel.state)), ProblemsListener {
internal class HighlightingFileRoot(panel: ProblemsViewPanel, val file: VirtualFile) : Root(panel) {
private val problems = mutableSetOf<HighlightingProblem>()
private val filter = ProblemFilter(panel.state)
private val provider = object : ProblemsProvider {
override val project = panel.project
@@ -23,24 +24,51 @@ internal class HighlightingFileRoot(panel: ProblemsViewPanel, val file: VirtualF
Disposer.register(provider, watcher)
}
fun findProblemNode(highlighter: RangeHighlighterEx): ProblemNode? {
val problem = watcher.findProblem(highlighter) ?: return null
return super.findProblemNode(file, problem)
fun findProblem(highlighter: RangeHighlighterEx) = watcher.findProblem(highlighter)
override fun getProblemCount() = synchronized(problems) { problems.count(filter) }
override fun getProblemFiles() = when (getProblemCount() > 0) {
true -> listOf(file)
else -> emptyList()
}
override fun getFileProblemCount(file: VirtualFile) = when (this.file == file) {
true -> getProblemCount()
else -> 0
}
override fun getFileProblems(file: VirtualFile) = when (this.file == file) {
true -> synchronized(problems) { problems.filter(filter) }
else -> emptyList()
}
override fun getOtherProblemCount() = 0
override fun getOtherProblems(): Collection<Problem> = emptyList()
override fun problemAppeared(problem: Problem) {
addProblems(file, problem)
if (!ProblemsView.isProjectErrorsEnabled()) return
if (problem is HighlightingProblem && problem.severity >= HighlightSeverity.ERROR.myVal) {
HighlightingErrorsProvider.getInstance(problem.provider.project).problemsAppeared(file)
}
if (problem !is HighlightingProblem || problem.file != file) return
notify(problem, synchronized(problems) { SetUpdateState.add(problem, problems) })
if (!ProblemsView.isProjectErrorsEnabled() || problem.severity < HighlightSeverity.ERROR.myVal) return
HighlightingErrorsProvider.getInstance(problem.provider.project).problemsAppeared(file)
}
override fun problemDisappeared(problem: Problem) {
removeProblems(file, problem)
if (problem !is HighlightingProblem || problem.file != file) return
notify(problem, synchronized(problems) { SetUpdateState.remove(problem, problems) })
}
override fun problemUpdated(problem: Problem) {
updateProblem(file, problem)
if (problem !is HighlightingProblem || problem.file != file) return
notify(problem, synchronized(problems) { SetUpdateState.update(problem, problems) })
}
private fun notify(problem: Problem, state: SetUpdateState) = when (state) {
SetUpdateState.ADDED -> super.problemAppeared(problem)
SetUpdateState.REMOVED -> super.problemDisappeared(problem)
SetUpdateState.UPDATED -> super.problemUpdated(problem)
SetUpdateState.IGNORED -> {
}
}
}
@@ -17,7 +17,7 @@ import com.intellij.openapi.fileEditor.*
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.SingleAlarm
import com.intellij.util.ui.tree.TreeUtil.promiseSelectFirstLeaf
import com.intellij.util.ui.tree.TreeUtil
import javax.swing.Icon
internal class HighlightingPanel(project: Project, state: ProblemsViewState)
@@ -28,6 +28,7 @@ internal class HighlightingPanel(project: Project, state: ProblemsViewState)
init {
tree.showsRootHandles = false
updateCurrentFile()
project.messageBus.connect(this)
.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this)
getApplication().messageBus.connect(this)
@@ -59,11 +60,6 @@ internal class HighlightingPanel(project: Project, state: ProblemsViewState)
if (selected) updateCurrentFile()
}
fun selectHighlighter(highlighter: RangeHighlighterEx) {
val root = treeModel.root as? HighlightingFileRoot
root?.findProblemNode(highlighter)?.let { select(it) }
}
override fun powerSaveStateChanged() {
statusUpdateAlarm.cancelAndRequest(forceRun = true)
updateToolWindowContent()
@@ -83,11 +79,17 @@ internal class HighlightingPanel(project: Project, state: ProblemsViewState)
else {
if (root != null && root.file == file) return
treeModel.root = HighlightingFileRoot(this, file)
promiseSelectFirstLeaf(tree)
TreeUtil.promiseSelectFirstLeaf(tree)
}
powerSaveStateChanged()
}
fun selectHighlighter(highlighter: RangeHighlighterEx) {
val root = treeModel.root as? HighlightingFileRoot ?: return
val problem = root.findProblem(highlighter) ?: return
TreeUtil.promiseSelect(tree, ProblemNodeFinder(problem))
}
private fun findCurrentFile(): VirtualFile? {
if (project.isDisposed) return null
val fileEditor = FileEditorManager.getInstance(project)?.selectedEditor ?: return null
@@ -0,0 +1,46 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.analysis.problemsView.toolWindow
import com.intellij.analysis.problemsView.FileProblem
import com.intellij.analysis.problemsView.Problem
import com.intellij.openapi.vfs.VfsUtil.isAncestor
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.tree.TreeVisitor
import com.intellij.util.ui.tree.TreeUtil
import javax.swing.tree.TreePath
internal interface ProblemsTreeVisitor : TreeVisitor {
override fun visit(path: TreePath) = when (val node = TreeUtil.getLastUserObject(path)) {
is Root -> visitRoot(node)
is FileNode -> visitFile(node)
is ProblemNode -> visitProblem(node)
else -> TreeVisitor.Action.SKIP_CHILDREN
}
fun visitRoot(root: Root) = TreeVisitor.Action.CONTINUE
fun visitFile(node: FileNode): TreeVisitor.Action
fun visitProblem(node: ProblemNode) = TreeVisitor.Action.SKIP_CHILDREN
}
internal class FileNodeFinder(private val file: VirtualFile) : ProblemsTreeVisitor {
override fun visitFile(node: FileNode) = when {
node.file == file -> TreeVisitor.Action.INTERRUPT
isAncestor(node.file, file, true) -> TreeVisitor.Action.CONTINUE
else -> TreeVisitor.Action.SKIP_CHILDREN
}
}
internal class ProblemNodeFinder(private val problem: Problem) : ProblemsTreeVisitor {
override fun visitFile(node: FileNode) = when {
problem !is FileProblem -> TreeVisitor.Action.SKIP_CHILDREN
isAncestor(node.file, problem.file, false) -> TreeVisitor.Action.CONTINUE
else -> TreeVisitor.Action.SKIP_CHILDREN
}
override fun visitProblem(node: ProblemNode) = when (node.problem) {
problem -> TreeVisitor.Action.INTERRUPT
else -> TreeVisitor.Action.SKIP_CHILDREN
}
}
@@ -23,7 +23,6 @@ import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import com.intellij.ui.tree.AsyncTreeModel;
import com.intellij.ui.tree.RestoreSelectionListener;
import com.intellij.ui.tree.TreeVisitor;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.EditSourceOnDoubleClickHandler;
import com.intellij.util.EditSourceOnEnterKeyHandler;
@@ -72,7 +71,7 @@ abstract class ProblemsViewPanel extends OnePixelSplitter implements Disposable,
if (content == null) return;
Root root = myTreeModel.getRoot();
int count = root == null ? 0 : root.getProblemsCount();
int count = root == null ? 0 : root.getProblemCount();
content.setDisplayName(getContentDisplayName(count));
Icon icon = getToolWindowIcon(count);
if (icon != null) window.setIcon(icon);
@@ -324,14 +323,6 @@ abstract class ProblemsViewPanel extends OnePixelSplitter implements Disposable,
getApplication().invokeLater(runnable, stateForComponent(this));
}
void select(@NotNull Node node) {
TreeUtil.promiseSelect(getTree(), createVisitor(node));
}
@NotNull TreeVisitor createVisitor(@NotNull Node node) {
return new TreeVisitor.ByTreePath<>(node.getPath(), o -> o);
}
@NotNull Comparator<Node> createComparator() {
return new NodeComparator(
isNullableOrSelected(getSortFoldersFirst()),
@@ -103,4 +103,6 @@ private class ProjectErrorsCollector(val project: Project) : ProblemsCollector {
?.contents
?.mapNotNull { it.component as? ProjectErrorsPanel }
?.firstOrNull()
?.treeModel
?.root
}
@@ -1,35 +1,34 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.analysis.problemsView.toolWindow
import com.intellij.analysis.problemsView.FileProblem
import com.intellij.analysis.problemsView.Problem
import com.intellij.analysis.problemsView.ProblemsListener
import com.intellij.analysis.problemsView.ProblemsCollector
import com.intellij.openapi.actionSystem.ToggleOptionAction.Option
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
internal class ProjectErrorsPanel(project: Project, state: ProblemsViewState)
: ProblemsViewPanel(project, state), ProblemsListener {
private val root = Root(this)
: ProblemsViewPanel(project, state) {
init {
treeModel.root = root
treeModel.root = ProjectErrorsRoot(this)
tree.emptyText.text = ProblemsViewBundle.message("problems.view.project.empty")
}
override fun getDisplayName() = ProblemsViewBundle.message("problems.view.project")
override fun getSortFoldersFirst(): Option? = null
override fun getSortBySeverity(): Option? = null
}
override fun problemAppeared(problem: Problem) {
if (problem is FileProblem) root.addProblems(problem.file, problem)
}
override fun problemDisappeared(problem: Problem) {
if (problem is FileProblem) root.removeProblems(problem.file, problem)
}
private class ProjectErrorsRoot(panel: ProblemsViewPanel) : Root(panel) {
private val collector = ProblemsCollector.getInstance(panel.project)
override fun problemUpdated(problem: Problem) {
if (problem is FileProblem) root.updateProblem(problem.file, problem)
}
override fun getProblemCount() = collector.getProblemCount()
override fun getProblemFiles() = collector.getProblemFiles()
override fun getFileProblemCount(file: VirtualFile) = collector.getFileProblemCount(file)
override fun getFileProblems(file: VirtualFile) = collector.getFileProblems(file)
override fun getOtherProblemCount() = collector.getOtherProblemCount()
override fun getOtherProblems() = collector.getOtherProblems()
}
@@ -1,20 +1,22 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.analysis.problemsView.toolWindow
import com.intellij.analysis.problemsView.FileProblem
import com.intellij.analysis.problemsView.Problem
import com.intellij.analysis.problemsView.ProblemsCollector
import com.intellij.ide.projectView.PresentationData
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES
import com.intellij.ui.tree.LeafState
import com.intellij.ui.tree.TreeVisitor.ByTreePath
import com.intellij.util.ui.tree.TreeUtil.promiseExpand
import com.intellij.util.ui.tree.TreeUtil
import javax.swing.tree.TreePath
internal open class Root(val panel: ProblemsViewPanel, private val filter: ProblemFilter? = null)
: Node(panel.project), Disposable {
internal abstract class Root(val panel: ProblemsViewPanel)
: Node(panel.project), ProblemsCollector, Disposable {
private val allProblems = mutableMapOf<VirtualFile, FileProblems>()
private val nodes = mutableMapOf<VirtualFile, FileNode>()
override fun dispose() = Unit
@@ -26,55 +28,68 @@ internal open class Root(val panel: ProblemsViewPanel, private val filter: Probl
presentation.addText(name, REGULAR_ATTRIBUTES)
}
override fun getChildren(): Collection<Node> = synchronized(allProblems) {
allProblems.values
.filter { it.count(filter) > 0 }
.map { it.getFileNode(getParentNode(it.file)) }
override fun getChildren(): Collection<Node> {
val children = mutableListOf<Node>()
val files = getProblemFiles()
synchronized(nodes) {
files.forEach { children += nodes.computeIfAbsent(it) { file -> FileNode(this, file) } }
}
return children
}
open fun getChildren(file: VirtualFile): Collection<Node> = synchronized(allProblems) {
allProblems[file]?.getProblemNodes(filter) ?: emptyList()
open fun getChildren(file: VirtualFile): Collection<Node> {
val node = synchronized(nodes) { nodes[file] } ?: return emptyList()
return getFileProblems(node.file).map { ProblemNode(node, it) }
}
open fun getProblemsCount(): Int = synchronized(allProblems) {
allProblems.values.sumBy { it.count(filter) }
}
open fun getProblemsCount(file: VirtualFile): Int = synchronized(allProblems) {
allProblems[file]?.count(filter) ?: 0
}
open fun addProblems(file: VirtualFile, vararg problems: Problem) {
val exist = synchronized(allProblems) { allProblems.contains(file) }
val node = add(file, problems) ?: return
onValidThread {
panel.updateToolWindowContent()
panel.treeModel.structureChanged(node.getPath())
if (!exist) synchronized(allProblems) {
allProblems[file]?.getFileNode(getParentNode(file))
}?.let {
promiseExpand(panel.tree, ByTreePath(it.getPath()) { any: Any? -> any })
override fun problemAppeared(problem: Problem) = when (problem) {
!is FileProblem -> structureChanged()
else -> {
val file = problem.file
when (1 == getFileProblemCount(file)) {
true -> fileAppeared(file)
else -> fileUpdated(file)
}
}
}
open fun removeProblems(file: VirtualFile, vararg problems: Problem) {
val node = remove(file, problems) ?: return
onValidThread {
panel.updateToolWindowContent()
panel.treeModel.structureChanged(node.getPath())
override fun problemDisappeared(problem: Problem) = when (problem) {
!is FileProblem -> structureChanged()
else -> {
val file = problem.file
when (0 == getFileProblemCount(file)) {
true -> fileDisappeared(file)
else -> fileUpdated(file)
}
}
}
open fun updateProblem(file: VirtualFile, problem: Problem) {
val node = findProblemNode(file, problem) ?: return
onValidThread {
if (node.update()) panel.treeModel.nodeChanged(node.getPath())
override fun problemUpdated(problem: Problem) {
TreeUtil.promiseVisit(panel.tree, ProblemNodeFinder(problem)).onSuccess { path ->
val node = TreeUtil.getLastUserObject(ProblemNode::class.java, path) ?: return@onSuccess
onValidThread { if (node.update()) panel.treeModel.nodeChanged(node.getPath()) }
}
}
fun findProblemNode(file: VirtualFile, problem: Problem) = synchronized(allProblems) {
allProblems[file]?.findProblemNode(problem)
private fun fileAppeared(file: VirtualFile) {
structureChanged()
TreeUtil.promiseExpand(panel.tree, FileNodeFinder(file))
}
private fun fileDisappeared(file: VirtualFile) {
synchronized(nodes) { nodes.remove(file) }
structureChanged()
}
private fun fileUpdated(file: VirtualFile) {
TreeUtil.promiseVisit(panel.tree, FileNodeFinder(file)).onSuccess { path ->
path?.let { structureChanged(it) }
}
}
private fun structureChanged(path: TreePath? = null) {
panel.updateToolWindowContent()
panel.treeModel.structureChanged(path)
}
private fun onValidThread(task: () -> Unit) {
@@ -82,28 +97,4 @@ internal open class Root(val panel: ProblemsViewPanel, private val filter: Probl
if (panel.treeModel.isRoot(this)) task()
}
}
private fun add(file: VirtualFile, problems: Array<out Problem>): Node? = synchronized(allProblems) {
val fileProblems = allProblems.computeIfAbsent(file) { FileProblems(it) }
val count = fileProblems.count()
problems.forEach { fileProblems.add(it) }
if (count == fileProblems.count()) return null
val parent = getParentNode(file)
if (count > 0) return fileProblems.getFileNode(parent)
return parent
}
private fun remove(file: VirtualFile, problems: Array<out Problem>): Node? = synchronized(allProblems) {
val fileProblems = allProblems[file] ?: return null
val count = fileProblems.count()
problems.forEach { fileProblems.remove(it) }
if (count == fileProblems.count()) return null
val parent = getParentNode(file)
if (fileProblems.count() > 0) return fileProblems.getFileNode(parent)
allProblems.remove(file)
return parent
}
@Suppress("UNUSED_PARAMETER") // TODO: support file hierarchy
private fun getParentNode(file: VirtualFile): Node = this
}