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")
+ }
+ }
+}