diff --git a/plugins/properties/properties-psi-impl/src/com/intellij/lang/properties/psi/impl/PropertiesPsiTreeChangePreprocessor.java b/plugins/properties/properties-psi-impl/src/com/intellij/lang/properties/psi/impl/PropertiesPsiTreeChangePreprocessor.java
new file mode 100644
index 000000000000..8f886f9ac715
--- /dev/null
+++ b/plugins/properties/properties-psi-impl/src/com/intellij/lang/properties/psi/impl/PropertiesPsiTreeChangePreprocessor.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.lang.properties.psi.impl;
+
+import com.intellij.lang.properties.parsing.PropertiesTokenTypes;
+import com.intellij.lang.properties.psi.PropertiesFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.PsiTreeChangeEventImpl;
+import com.intellij.psi.impl.PsiTreeChangePreprocessorBase;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.psi.util.PsiUtilCore;
+import org.jetbrains.annotations.NotNull;
+
+public class PropertiesPsiTreeChangePreprocessor extends PsiTreeChangePreprocessorBase {
+ private static final TokenSet CODE_BLOCK_ELEMENTS = TokenSet.create(PropertiesTokenTypes.VALUE_CHARACTERS,
+ PropertiesTokenTypes.END_OF_LINE_COMMENT,
+ PropertiesTokenTypes.WHITE_SPACE,
+ PropertiesTokenTypes.KEY_VALUE_SEPARATOR);
+
+ public PropertiesPsiTreeChangePreprocessor(@NotNull PsiManager psiManager) {
+ super(psiManager);
+ }
+
+ @Override
+ protected void onTreeChanged(@NotNull PsiTreeChangeEventImpl event) {
+ if (event.isGenericChange()) return;
+ switch (event.getCode()) {
+ case BEFORE_PROPERTY_CHANGE:
+ case BEFORE_CHILD_REMOVAL:
+ case BEFORE_CHILD_ADDITION:
+ case BEFORE_CHILD_MOVEMENT:
+ case BEFORE_CHILDREN_CHANGE:
+ case BEFORE_CHILD_REPLACEMENT:
+ return;
+ case CHILD_ADDED:
+ if (isCodeBlock(event.getChild())) return;
+ break;
+ case CHILD_REMOVED:
+ if (isCodeBlock(event.getChild())) return;
+ break;
+ case CHILD_REPLACED:
+ if (isCodeBlock(event.getOldChild()) || isCodeBlock(event.getNewChild())) return;
+ break;
+ case CHILD_MOVED:
+ if (isCodeBlock(event.getChild())) return;
+ break;
+ case CHILDREN_CHANGED:
+ if (isCodeBlock(event.getChild())) return;
+ case PROPERTY_CHANGED:
+ break;
+ }
+ doIncOutOfCodeBlockCounter();
+ }
+
+ @Override
+ protected boolean acceptsEvent(@NotNull PsiTreeChangeEventImpl event) {
+ return event.getFile() instanceof PropertiesFile;
+ }
+
+ @Override
+ protected boolean isOutOfCodeBlock(@NotNull PsiElement element) {
+ throw new IllegalStateException();
+ }
+
+ private static boolean isCodeBlock(@NotNull PsiElement element) {
+ return CODE_BLOCK_ELEMENTS.contains(PsiUtilCore.getElementType(element));
+ }
+}
diff --git a/plugins/properties/src/META-INF/plugin.xml b/plugins/properties/src/META-INF/plugin.xml
index fc22b76a0c68..6bc313ede341 100644
--- a/plugins/properties/src/META-INF/plugin.xml
+++ b/plugins/properties/src/META-INF/plugin.xml
@@ -123,6 +123,8 @@
+
+
diff --git a/plugins/properties/testSrc/com/intellij/lang/properties/PropertiesModificationTest.java b/plugins/properties/testSrc/com/intellij/lang/properties/PropertiesModificationTest.java
new file mode 100644
index 000000000000..8e4198e9b73d
--- /dev/null
+++ b/plugins/properties/testSrc/com/intellij/lang/properties/PropertiesModificationTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.lang.properties;
+
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.editor.SelectionModel;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.util.PsiModificationTracker;
+import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
+import org.jetbrains.annotations.NotNull;
+
+public class PropertiesModificationTest extends LightCodeInsightFixtureTestCase {
+
+ public void testValueEditing() {
+ doTestEditing(" # comment", false);
+ }
+
+ public void testKeyEditing() {
+ doTestEditing("key = value", true);
+ }
+
+ public void testCommentEditing() {
+ doTestEditing("key = value", false);
+ }
+
+ public void testKeyReplacement() {
+ doTestReplacement("key = value", true);
+ }
+
+ public void testValueReplacement() {
+ doTestReplacement("key = value", false);
+ }
+
+ public void testCommentReplacement() {
+ doTestReplacement("key = value \n# comment \n key2 = value2", false);
+ }
+
+ public void testKeyDeletion() {
+ doTestDeletion("key = value", true);
+ }
+
+ public void testPropertiesDeletion() {
+ doTestDeletion("key1 = value1 \n" +
+ "key2 = value2 \n" +
+ "key3 = value3 \n" +
+ "key4 = value4 \n" +
+ "key5 = value5 \n", true);
+ }
+
+ private void doTestEditing(@NotNull String text, boolean isOutOfBlockModificationExpected) {
+ doTest(text, () -> myFixture.type("xxx"), isOutOfBlockModificationExpected);
+ }
+
+ private void doTestReplacement(@NotNull String text, boolean isOutOfBlockModificationExpected) {
+ doTest(text, () -> WriteCommandAction.runWriteCommandAction(getProject(), () -> {
+ SelectionModel sel = getEditor().getSelectionModel();
+ getEditor().getDocument().replaceString(sel.getSelectionStart(), sel.getSelectionEnd(), "xxx");
+ }), isOutOfBlockModificationExpected);
+ }
+
+ private void doTestDeletion(@NotNull String text, boolean isOutOfBlockModificationExpected) {
+ doTest(text, () -> WriteCommandAction.runWriteCommandAction(getProject(), () -> {
+ SelectionModel sel = getEditor().getSelectionModel();
+ getEditor().getDocument().deleteString(sel.getSelectionStart(), sel.getSelectionEnd());
+ }), isOutOfBlockModificationExpected);
+ }
+
+ private void doTest(@NotNull String text, Runnable modificationAction, boolean isOutOfBlockModificationExpected) {
+ myFixture.configureByText("test.properties", text);
+ PsiModificationTracker tracker = myFixture.getPsiManager().getModificationTracker();
+ long oldMod = tracker.getOutOfCodeBlockModificationCount();
+ modificationAction.run();
+ PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
+ long newMod = tracker.getOutOfCodeBlockModificationCount();
+ assertTrue(isOutOfBlockModificationExpected ^ oldMod == newMod);
+ }
+}