diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml index 8e823ed9f305..e830886909b5 100644 --- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml +++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml @@ -189,6 +189,8 @@ + + diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/PyChangeLocalityDetector.kt b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyChangeLocalityDetector.kt new file mode 100644 index 000000000000..9270fda4f319 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/PyChangeLocalityDetector.kt @@ -0,0 +1,18 @@ +package com.jetbrains.python.inspections + +import com.intellij.codeInsight.daemon.ChangeLocalityDetector +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import com.jetbrains.python.psi.PyFile + +class PyChangeLocalityDetector : ChangeLocalityDetector { + override fun getChangeHighlightingDirtyScopeFor(changedElement: PsiElement): PsiElement? { + if (changedElement is PsiWhiteSpace) { + val parent = changedElement.parent + if (parent is PyFile) { + return parent + } + } + return null + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyChangeLocalityDetectorTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyChangeLocalityDetectorTest.kt new file mode 100644 index 000000000000..8b367383d1c6 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyChangeLocalityDetectorTest.kt @@ -0,0 +1,98 @@ +package com.jetbrains.python.inspections + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.fixtures.PyTestCase +import com.jetbrains.python.psi.PyFile +import junit.framework.TestCase + +class PyChangeLocalityDetectorTest : PyTestCase() { + + fun `test top level function body change`() = checkChangeScope(""" + def f(): + x = 1 + """, null) + + fun `test top level function whitespace at the start change`() = checkChangeScope(""" + + def f(): + x = 1 + + def bar(): + pass + """.trimIndent(), """ + + + def f(): + x = 1 + + def bar(): + pass + """.trimIndent()) + + fun `test top level function whitespace at the end change`() = checkChangeScope(""" + def f(): + x = 1 + + def bar(): + pass + """.trimIndent(), """ + def f(): + x = 1 + + def bar(): + pass + """.trimIndent()) + + fun `test top level class whitespace at the end change`() = checkChangeScope(""" + class A: + x = 1 + + def bar(): + pass + """.trimIndent(), """ + class A: + x = 1 + + def bar(): + pass + """.trimIndent()) + + private fun checkChangeScope(text: String, expectedScope: String?) { + myFixture.configureByText("main.py", text) + + val selectionModel = myFixture.editor.selectionModel + assertTrue("changed item has to be specified in ", selectionModel.hasSelection()) + + var element = PsiTreeUtil.findElementOfClassAtRange( + myFixture.file, + selectionModel.selectionStart, + selectionModel.selectionEnd, + PsiElement::class.java + ) ?: error("No PsiElement at selection range") + + val changeLocalityDetector = PyChangeLocalityDetector() + + /** + * Emulate [com.intellij.codeInsight.daemon.impl.PsiChangeHandler.updateByChange] logic + * by walking up the tree and calling [com.intellij.codeInsight.daemon.ChangeLocalityDetector.getChangeHighlightingDirtyScopeFor] + * until non-null is returned + */ + while (element !is PyFile) { + val dirtyScope = changeLocalityDetector.getChangeHighlightingDirtyScopeFor(element) + if (dirtyScope != null) { + if (expectedScope == null) { + throw IllegalStateException("scope was calculated when no scope was expected") + } + + TestCase.assertEquals(expectedScope.trimIndent(), dirtyScope.text) + return + } + element = element.parent + } + + if (expectedScope != null) { + throw IllegalStateException("scope wasn't calculated") + } + } +}