diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/FileNode.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/FileNode.kt index 5b68ce7c920a..207b5e60704d 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/FileNode.kt +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/FileNode.kt @@ -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) diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingFileRoot.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingFileRoot.kt index 1edfbb050ad7..79816da3fc7e 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingFileRoot.kt +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingFileRoot.kt @@ -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() + 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 = 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 -> { + } } } diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingPanel.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingPanel.kt index 15df53bfd8ad..3ff7e38f6860 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingPanel.kt +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/HighlightingPanel.kt @@ -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 diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsTreeVisitor.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsTreeVisitor.kt new file mode 100644 index 000000000000..601d9ac9aecc --- /dev/null +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsTreeVisitor.kt @@ -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 + } +} diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsViewPanel.java b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsViewPanel.java index 640c84663970..fe1816b12005 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsViewPanel.java +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProblemsViewPanel.java @@ -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 createComparator() { return new NodeComparator( isNullableOrSelected(getSortFoldersFirst()), diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsCollector.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsCollector.kt index 38a9d47cf2ac..7003bfd6ea6f 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsCollector.kt +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsCollector.kt @@ -103,4 +103,6 @@ private class ProjectErrorsCollector(val project: Project) : ProblemsCollector { ?.contents ?.mapNotNull { it.component as? ProjectErrorsPanel } ?.firstOrNull() + ?.treeModel + ?.root } diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsPanel.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsPanel.kt index 318f77b09d2d..7ec8e1cfc0ee 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsPanel.kt +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/ProjectErrorsPanel.kt @@ -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() } diff --git a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/Root.kt b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/Root.kt index 1bf3a2d82d4c..c45db3251dd6 100644 --- a/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/Root.kt +++ b/platform/lang-impl/src/com/intellij/analysis/problemsView/toolWindow/Root.kt @@ -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() + private val nodes = mutableMapOf() 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 = synchronized(allProblems) { - allProblems.values - .filter { it.count(filter) > 0 } - .map { it.getFileNode(getParentNode(it.file)) } + override fun getChildren(): Collection { + val children = mutableListOf() + val files = getProblemFiles() + synchronized(nodes) { + files.forEach { children += nodes.computeIfAbsent(it) { file -> FileNode(this, file) } } + } + return children } - open fun getChildren(file: VirtualFile): Collection = synchronized(allProblems) { - allProblems[file]?.getProblemNodes(filter) ?: emptyList() + open fun getChildren(file: VirtualFile): Collection { + 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): 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): 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 }