diff options
author | Charlie Tsai <chartsai@google.com> | 2022-07-19 16:14:17 +0100 |
---|---|---|
committer | Charlie Tsai <chartsai@google.com> | 2022-07-27 10:18:19 +0000 |
commit | c99ef931a43e38609b09dc60aaed1c43f50b8e6d (patch) | |
tree | ada8caefe6c34eb1b501cb77bdac153fdb8c2cc0 /designer | |
parent | b9335be13376a829c91d3dd9e95ee9424101454c (diff) | |
download | idea-c99ef931a43e38609b09dc60aaed1c43f50b8e6d.tar.gz |
Make VisualLintService have its own IssueModel
This also makes VisualizationForm have its own IssueProvider to avoid
the potential race condition about clearing the issues of
IssueProvider.
Bug: N/A
Test: updated and covered
Change-Id: I1e25cdd2eb8d91581d2cf81f8b6d3ba82d853dc1
Diffstat (limited to 'designer')
8 files changed, 103 insertions, 76 deletions
diff --git a/designer/src/com/android/tools/idea/common/error/DesignerCommonIssueProvider.kt b/designer/src/com/android/tools/idea/common/error/DesignerCommonIssueProvider.kt index 594e10620cd..8893512e585 100644 --- a/designer/src/com/android/tools/idea/common/error/DesignerCommonIssueProvider.kt +++ b/designer/src/com/android/tools/idea/common/error/DesignerCommonIssueProvider.kt @@ -50,26 +50,15 @@ class DesignToolsIssueProvider(project: Project) : DesignerCommonIssueProvider<A fileEditorManager = FileEditorManager.getInstance(project) messageBusConnection.subscribe(IssueProviderListener.TOPIC, object : IssueProviderListener { override fun issueUpdated(source: Any, issues: List<Issue>) { - var filteredIssues = filterSelectedOrNoFileIssues(issues) - - // Background lint update the issue asynchronized. For example, it is possible that a layout issue is reported after switching to - // Kotlin file. For now we only display the visual lint issue when the linted file is selected. - // TODO: Remove when visual lint always running in the background regardless the current selected file. - val selectedFiles = fileEditorManager.selectedFiles - filteredIssues = filteredIssues.filter { - if (it is VisualLintRenderIssue) { - it.source.models.map { model -> model.virtualFile }.any { file -> selectedFiles.contains(file) } - } - else true + val selectedFiles = fileEditorManager.selectedFiles.toList() + var changed = false + if (issues != sourceToIssueMap[source]) { + changed = true + sourceToIssueMap[source] = issues } - - if (filteredIssues.isEmpty()) { - sourceToIssueMap.remove(source) - } - else { - sourceToIssueMap[source] = filteredIssues + if (cleanUpFileIssues(selectedFiles) || changed) { + listeners.forEach { it.run() } } - listeners.forEach { it.run() } } }) @@ -78,30 +67,41 @@ class DesignToolsIssueProvider(project: Project) : DesignerCommonIssueProvider<A // TODO(b/222110455): Make [DesignSurface] deactivate the IssueModel when it is no longer visible. messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { override fun fileClosed(source: FileEditorManager, file: VirtualFile) { - if (!source.hasOpenFiles()) { - cleanUpFileIssues() + if (!source.hasOpenFiles() && sourceToIssueMap.isNotEmpty()) { + sourceToIssueMap.clear() + listeners.forEach { it.run() } } } override fun selectionChanged(event: FileEditorManagerEvent) { - cleanUpFileIssues() + if (cleanUpFileIssues(listOfNotNull(event.newFile))) { + listeners.forEach { it.run() } + } } + }) + } - /** - * Remove the file issues if the file is not visible. - */ - private fun cleanUpFileIssues() { - for ((source, issues) in sourceToIssueMap.toMap()) { - val filteredIssues = filterSelectedOrNoFileIssues(issues) - if (filteredIssues.isEmpty()) { - sourceToIssueMap.remove(source) - } - else { - sourceToIssueMap[source] = filteredIssues - } + /** + * Remove the file issues or visual lint issue if the associated file is not visible(selected). + * Return true if [sourceToIssueMap] is changed, false otherwise. + */ + private fun cleanUpFileIssues(selectedFiles: List<VirtualFile>): Boolean { + var changed = false + for ((source, issues) in sourceToIssueMap.toMap()) { + val filteredIssues = filterSelectedOrNoFileIssues(issues).filterVisualLintIssues(selectedFiles) + if (filteredIssues.isEmpty()) { + sourceToIssueMap.remove(source) + changed = true + } + else { + if (filteredIssues != issues) { + sourceToIssueMap[source] = filteredIssues + changed = true } } - }) + } + return changed + } /** @@ -121,6 +121,18 @@ class DesignToolsIssueProvider(project: Project) : DesignerCommonIssueProvider<A return ret } + /** + * Remove the [VisualLintRenderIssue]s which are not related to the given [files] + */ + private fun List<Issue>.filterVisualLintIssues(files: List<VirtualFile>): List<Issue> { + return this.filter { + if (it is VisualLintRenderIssue) { + it.source.models.map { model -> model.virtualFile }.any { file -> files.contains(file) } + } + else true + } + } + override fun getFilteredIssues(): List<Issue> = sourceToIssueMap.values.flatten() .filterNot { (it as? VisualLintRenderIssue)?.isSuppressed() ?: false } .filter(filter) diff --git a/designer/src/com/android/tools/idea/uibuilder/surface/NlDesignSurface.java b/designer/src/com/android/tools/idea/uibuilder/surface/NlDesignSurface.java index 060f0039c1d..e33a3cd8407 100644 --- a/designer/src/com/android/tools/idea/uibuilder/surface/NlDesignSurface.java +++ b/designer/src/com/android/tools/idea/uibuilder/surface/NlDesignSurface.java @@ -870,7 +870,7 @@ public class NlDesignSurface extends DesignSurface<LayoutlibSceneManager> }); if (myShouldRunVisualLintService && !VisualizationToolWindowFactory.hasVisibleValidationWindow(project)) { - VisualLintService.getInstance(project).runVisualLintAnalysis(getModels(), NlDesignSurface.this); + VisualLintService.getInstance(project).runVisualLintAnalysis(getModels()); } } diff --git a/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationForm.kt b/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationForm.kt index 2af5f7ead70..8ea12547fa2 100644 --- a/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationForm.kt +++ b/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationForm.kt @@ -576,7 +576,6 @@ class VisualizationForm(private val project: Project, parentDisposable: Disposab myCancelRenderingTaskLock.unlock() } var renderFuture = CompletableFuture.completedFuture<Void?>(null) - visualLintHandler.clearIssueProviderAndBaseConfigurationIssue() // This render the added components. diff --git a/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationFormVisualLintHandler.kt b/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationFormVisualLintHandler.kt index 05196fe4995..f56492a414d 100644 --- a/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationFormVisualLintHandler.kt +++ b/designer/src/com/android/tools/idea/uibuilder/visual/VisualizationFormVisualLintHandler.kt @@ -23,6 +23,7 @@ import com.android.tools.idea.uibuilder.scene.LayoutlibSceneManager import com.android.tools.idea.uibuilder.scene.RenderListener import com.android.tools.idea.uibuilder.surface.NlDesignSurface import com.android.tools.idea.uibuilder.visual.visuallint.VisualLintBaseConfigIssues +import com.android.tools.idea.uibuilder.visual.visuallint.VisualLintIssueProvider import com.android.tools.idea.uibuilder.visual.visuallint.VisualLintService import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileEditor.FileEditorManager @@ -34,14 +35,20 @@ import com.intellij.openapi.project.Project class VisualizationFormVisualLintHandler(private val project: Project, private val issueModel: IssueModel) { private val myBaseConfigIssues = VisualLintBaseConfigIssues() - val lintIssueProvider = VisualLintService.getInstance(project).issueProvider + val lintIssueProvider = VisualLintIssueProvider(project) + + init { + issueModel.addIssueProvider(lintIssueProvider) + } fun clearIssueProvider() { lintIssueProvider.clear() + issueModel.updateErrorsList() } fun clearIssueProviderAndBaseConfigurationIssue() { lintIssueProvider.clear() + issueModel.updateErrorsList() myBaseConfigIssues.clear() } @@ -52,9 +59,10 @@ class VisualizationFormVisualLintHandler(private val project: Project, private v val result = manager.renderResult if (result != null) { ApplicationManager.getApplication().executeOnPooledThread { - VisualLintService.getInstance(project).analyzeAfterModelUpdate(result, model, myBaseConfigIssues) + VisualLintService.getInstance(project).analyzeAfterModelUpdate(lintIssueProvider, result, model, myBaseConfigIssues) if (StudioFlags.NELE_SHOW_VISUAL_LINT_ISSUE_IN_COMMON_PROBLEMS_PANEL.get()) { CommonLintUserDataHandler.updateVisualLintIssues(model.file, lintIssueProvider) + issueModel.updateErrorsList() } } } @@ -72,14 +80,18 @@ class VisualizationFormVisualLintHandler(private val project: Project, private v } fun onActivate() { + // Clean up the visual lint issue from Layout Editor + VisualLintService.getInstance(project).removeIssues() + issueModel.addIssueProvider(lintIssueProvider) - FileEditorManager.getInstance(project).selectedEditor?.getDesignSurface()?.let { - VisualLintService.getInstance(project).removeIssues(it) - } + issueModel.updateErrorsList() } fun onDeactivate() { issueModel.removeIssueProvider(lintIssueProvider) + issueModel.updateErrorsList() + + // Trigger Layout Editor Visual Lint (FileEditorManager.getInstance(project).selectedEditor?.getDesignSurface() as? NlDesignSurface)?.updateErrorDisplay() } }
\ No newline at end of file diff --git a/designer/src/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintService.kt b/designer/src/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintService.kt index c266f714c95..88d91e24273 100644 --- a/designer/src/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintService.kt +++ b/designer/src/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintService.kt @@ -19,7 +19,6 @@ import com.android.ide.common.rendering.HardwareConfigHelper import com.android.tools.idea.common.error.IssueModel import com.android.tools.idea.common.model.ModelListener import com.android.tools.idea.common.model.NlModel -import com.android.tools.idea.common.surface.DesignSurface import com.android.tools.idea.flags.StudioFlags import com.android.tools.idea.rendering.RenderAsyncActionExecutor import com.android.tools.idea.rendering.RenderResult @@ -43,6 +42,7 @@ import com.google.common.annotations.VisibleForTesting import com.intellij.codeInsight.daemon.HighlightDisplayKey import com.intellij.codeInspection.InspectionProfile import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer @@ -66,7 +66,7 @@ private val visualLintAnalyzerExecutorService = AppExecutorUtil.createBoundedApp * Service that runs visual lints */ @Service -class VisualLintService(val project: Project) { +class VisualLintService(val project: Project): Disposable { companion object { @JvmStatic @@ -75,8 +75,10 @@ class VisualLintService(val project: Project) { } } + val issueModel: IssueModel = IssueModel(this, project) + /** Default issue provider for Visual Lint Service. */ - val issueProvider = VisualLintIssueProvider(project) + val issueProvider: VisualLintIssueProvider = VisualLintIssueProvider(this) private val basicAnalyzers = listOf(BoundsAnalyzer, OverlapAnalyzer, AtfAnalyzer) private val adaptiveAnalyzers = listOf(BottomNavAnalyzer, BottomAppBarAnalyzer, TextFieldSizeAnalyzer, @@ -86,6 +88,7 @@ class VisualLintService(val project: Project) { private val ignoredTypes: MutableList<VisualLintErrorType> init { + issueModel.addIssueProvider(issueProvider, false) val connection = project.messageBus.connect() ignoredTypes = mutableListOf() getIgnoredTypesFromProfile(InspectionProfileManager.getInstance(project).currentProfile) @@ -113,29 +116,28 @@ class VisualLintService(val project: Project) { } } - fun removeIssues(surface: DesignSurface<*>) { - surface.issueModel.removeIssueProvider(issueProvider) + fun removeIssues() { + issueProvider.clear() + issueModel.updateErrorsList() } /** * Runs visual lint analysis in a pooled thread for configurations based on the model provided, * and adds the issues found to the [IssueModel] */ - fun runVisualLintAnalysis(models: List<NlModel>, surface: DesignSurface<*>) { - runVisualLintAnalysis(models, surface, visualLintExecutorService) + fun runVisualLintAnalysis(models: List<NlModel>) { + runVisualLintAnalysis(models, visualLintExecutorService) } @VisibleForTesting - fun runVisualLintAnalysis(models: List<NlModel>, surface: DesignSurface<*>, executorService: ExecutorService) { + fun runVisualLintAnalysis(models: List<NlModel>, executorService: ExecutorService) { CompletableFuture.runAsync({ - val issueModel = surface.issueModel - issueModel.removeIssueProvider(issueProvider) issueProvider.clear() + issueModel.updateErrorsList() if (models.isEmpty()) { return@runAsync } - issueModel.addIssueProvider(issueProvider, false) val displayingModel = models[0] val listener = object : ModelListener { override fun modelChanged(model: NlModel) { @@ -160,7 +162,7 @@ class VisualLintService(val project: Project) { createRenderResult(model, requireRender).handleAsync({ result, _ -> if (result != null) { updateHierarchy(result, model) - analyzeAfterModelUpdate(result, model, visualLintBaseConfigIssues, true) + analyzeAfterModelUpdate(issueProvider, result, model, visualLintBaseConfigIssues, true) } Disposer.dispose(model) latch.countDown() @@ -177,30 +179,32 @@ class VisualLintService(val project: Project) { /** * Collects in [issueProvider] all the [RenderErrorModel.Issue] found when analyzing the given [RenderResult] after model is updated. */ - fun analyzeAfterModelUpdate(result: RenderResult, + fun analyzeAfterModelUpdate(targetIssueProvider: VisualLintIssueProvider, + result: RenderResult, model: NlModel, baseConfigIssues: VisualLintBaseConfigIssues, runningInBackground: Boolean = false) { - runAnalyzers(basicAnalyzers, result, model, runningInBackground) + runAnalyzers(targetIssueProvider, basicAnalyzers, result, model, runningInBackground) if (HardwareConfigHelper.isWear(model.configuration.device)) { - runAnalyzers(wearAnalyzers, result, model, runningInBackground) + runAnalyzers(targetIssueProvider, wearAnalyzers, result, model, runningInBackground) } else { - runAnalyzers(adaptiveAnalyzers, result, model, runningInBackground) + runAnalyzers(targetIssueProvider, adaptiveAnalyzers, result, model, runningInBackground) if (VisualLintErrorType.LOCALE_TEXT !in ignoredTypes) { LocaleAnalyzer(baseConfigIssues).let { - issueProvider.addAllIssues(it.type, it.analyze(result, model, getSeverity(it.type), runningInBackground)) + targetIssueProvider.addAllIssues(it.type, it.analyze(result, model, getSeverity(it.type), runningInBackground)) } } } } - private fun runAnalyzers(analyzers: List<VisualLintAnalyzer>, + private fun runAnalyzers(targetIssueProvider: VisualLintIssueProvider, + analyzers: List<VisualLintAnalyzer>, result: RenderResult, model: NlModel, runningInBackground: Boolean) { analyzers.filter { !ignoredTypes.contains(it.type) }.forEach { val issues = it.analyze(result, model, getSeverity(it.type), runningInBackground) - issueProvider.addAllIssues(it.type, issues) + targetIssueProvider.addAllIssues(it.type, issues) } } @@ -208,6 +212,9 @@ class VisualLintService(val project: Project) { val key = HighlightDisplayKey.find(type.shortName) return key?.let { InspectionProfileManager.getInstance(project).currentProfile.getErrorLevel(it, null).severity } ?: HighlightSeverity.WARNING + } + + override fun dispose() { } } diff --git a/designer/testSrc/com/android/tools/idea/common/error/DesignToolsIssueProviderTest.kt b/designer/testSrc/com/android/tools/idea/common/error/DesignToolsIssueProviderTest.kt index 947aa746b9e..ebbdafec1db 100644 --- a/designer/testSrc/com/android/tools/idea/common/error/DesignToolsIssueProviderTest.kt +++ b/designer/testSrc/com/android/tools/idea/common/error/DesignToolsIssueProviderTest.kt @@ -21,6 +21,7 @@ import com.android.tools.idea.common.model.NlModel import com.android.tools.idea.testing.AndroidProjectRule import com.android.tools.idea.testing.EdtAndroidProjectRule import com.android.tools.idea.uibuilder.visual.visuallint.VisualLintErrorType +import com.android.tools.idea.uibuilder.visual.visuallint.VisualLintService import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.testFramework.runInEdtAndGet import com.intellij.testFramework.runInEdtAndWait @@ -127,7 +128,7 @@ class DesignToolsIssueProviderTest { val visualLintIssues = listOf(createTestVisualLintRenderIssue(VisualLintErrorType.BOUNDS, listOf(fakeNlComponent), "")) val ktSource = Any() - val layoutSource = Any() + val layoutSource = VisualLintService.getInstance(rule.project).issueModel runInEdtAndWait { fileEditorManager.openFile(ktFile, true) } messageBus.syncPublisher(IssueProviderListener.TOPIC).issueUpdated(ktSource, ktFileIssues) diff --git a/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintAnalysisTest.kt b/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintAnalysisTest.kt index 4c4adc40ba4..4296e6ea914 100644 --- a/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintAnalysisTest.kt +++ b/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintAnalysisTest.kt @@ -270,7 +270,8 @@ class VisualLintAnalysisTest { task.setDecorations(false) try { val result = task.render().get() - VisualLintService.getInstance(projectRule.project).analyzeAfterModelUpdate(result, nlModel, VisualLintBaseConfigIssues()) + val service = VisualLintService.getInstance(projectRule.project) + service.analyzeAfterModelUpdate(service.issueProvider, result, nlModel, VisualLintBaseConfigIssues()) } catch (ex: Exception) { throw RuntimeException(ex) diff --git a/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintServiceTest.kt b/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintServiceTest.kt index b702b1bf84c..4c483a54d34 100644 --- a/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintServiceTest.kt +++ b/designer/testSrc/com/android/tools/idea/uibuilder/visual/visuallint/VisualLintServiceTest.kt @@ -17,8 +17,6 @@ package com.android.tools.idea.uibuilder.visual.visuallint import com.android.testutils.TestUtils import com.android.tools.idea.common.SyncNlModel -import com.android.tools.idea.common.error.IssueModel -import com.android.tools.idea.common.surface.DesignSurface import com.android.tools.idea.common.type.DesignerTypeRegistrar import com.android.tools.idea.rendering.NoSecurityManagerRenderService import com.android.tools.idea.rendering.RenderService @@ -42,11 +40,9 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mockito import kotlin.test.assertEquals class VisualLintServiceTest { - private lateinit var surface: DesignSurface<*> @get:Rule val projectRule = AndroidGradleProjectRule() @@ -56,9 +52,6 @@ class VisualLintServiceTest { projectRule.fixture.testDataPath = TestUtils.resolveWorkspacePath("tools/adt/idea/designer/testData").toString() RenderTestUtil.beforeRenderTestCase() RenderService.setForTesting(projectRule.project, NoSecurityManagerRenderService(projectRule.project)) - val issueModel = IssueModel.createForTesting(projectRule.fixture.testRootDisposable, projectRule.project) - surface = Mockito.mock(DesignSurface::class.java) - Mockito.`when`(surface.issueModel).thenReturn(issueModel) DesignerTypeRegistrar.register(LayoutFileType) val visualLintInspections = arrayOf(BoundsAnalyzerInspection, BottomNavAnalyzerInspection, BottomAppBarAnalyzerInspection, TextFieldSizeAnalyzerInspection, OverlapAnalyzerInspection, LongTextAnalyzerInspection, @@ -79,14 +72,16 @@ class VisualLintServiceTest { projectRule.load("projects/visualLintApplication") projectRule.requestSyncAndWait() + val visualLintService = VisualLintService.getInstance(projectRule.project) + val visualLintIssueModel = visualLintService.issueModel + val module = projectRule.getModule("app") val facet = AndroidFacet.getInstance(module)!! val dashboardLayout = projectRule.project.baseDir.findFileByRelativePath("app/src/main/res/layout/fragment_dashboard.xml")!! val nlModel = SyncNlModel.create(projectRule.project, NlComponentRegistrar, null, null, facet, dashboardLayout) - VisualLintService.getInstance(projectRule.project) - .runVisualLintAnalysis(listOf(nlModel), surface, MoreExecutors.newDirectExecutorService()) + visualLintService.runVisualLintAnalysis(listOf(nlModel), MoreExecutors.newDirectExecutorService()) - val issues = surface.issueModel.issues + val issues = visualLintIssueModel.issues assertEquals(2, issues.size) issues.forEach { assertEquals("Visual Lint Issue", it.category) @@ -95,9 +90,9 @@ class VisualLintServiceTest { val atfLayout = projectRule.project.baseDir.findFileByRelativePath("app/src/main/res/layout/atf_layout.xml")!! val atfModel = SyncNlModel.create(projectRule.project, NlComponentRegistrar, null, null, facet, atfLayout) VisualLintService.getInstance(projectRule.project) - .runVisualLintAnalysis(listOf(atfModel), surface, MoreExecutors.newDirectExecutorService()) + .runVisualLintAnalysis(listOf(atfModel), MoreExecutors.newDirectExecutorService()) - val atfIssues = surface.issueModel.issues + val atfIssues = visualLintIssueModel.issues assertEquals(2, atfIssues.size) atfIssues.forEach { assertEquals("Visual Lint Issue", it.category) @@ -110,9 +105,9 @@ class VisualLintServiceTest { val wearConfiguration = RenderTestUtil.getConfiguration(module, wearLayout, "wearos_small_round") val wearModel = SyncNlModel.create(projectRule.project, NlComponentRegistrar, null, null, facet, wearLayout, wearConfiguration) VisualLintService.getInstance(projectRule.project) - .runVisualLintAnalysis(listOf(wearModel), surface, MoreExecutors.newDirectExecutorService()) + .runVisualLintAnalysis(listOf(wearModel), MoreExecutors.newDirectExecutorService()) - val wearIssues = surface.issueModel.issues + val wearIssues = visualLintIssueModel.issues assertEquals(13, wearIssues.size) wearIssues.forEach { assertEquals("Visual Lint Issue", it.category) |