diff --git a/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java b/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java new file mode 100644 index 000000000000..1b50460eecb3 --- /dev/null +++ b/xml/impl/src/com/intellij/codeInsight/editorActions/XmlTagNameSynchronizer.java @@ -0,0 +1,287 @@ +/* + * Copyright 2000-2014 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.codeInsight.editorActions; + +import com.intellij.codeInspection.htmlInspections.RenameTagBeginOrEndIntentionAction; +import com.intellij.ide.highlighter.HtmlFileType; +import com.intellij.ide.highlighter.XHtmlFileType; +import com.intellij.ide.highlighter.XmlFileType; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandAdapter; +import com.intellij.openapi.command.CommandEvent; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.components.ApplicationComponent; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.event.DocumentListener; +import com.intellij.openapi.editor.event.EditorFactoryAdapter; +import com.intellij.openapi.editor.event.EditorFactoryEvent; +import com.intellij.openapi.editor.impl.EditorImpl; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Couple; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.util.SmartList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @author Dennis.Ushakov + */ +public class XmlTagNameSynchronizer extends CommandAdapter implements ApplicationComponent { + private static final Key SYNCHRONIZER_KEY = Key.create("tag_name_synchronizer"); + private final FileDocumentManager myFileDocumentManager; + + public XmlTagNameSynchronizer(EditorFactory editorFactory, FileDocumentManager manager, CommandProcessor processor) { + myFileDocumentManager = manager; + editorFactory.addEditorFactoryListener(new EditorFactoryAdapter() { + @Override + public void editorCreated(@NotNull EditorFactoryEvent event) { + installSynchronizer(event.getEditor()); + } + + @Override + public void editorReleased(@NotNull EditorFactoryEvent event) { + event.getEditor().getDocument().putUserData(SYNCHRONIZER_KEY, null); + } + }, ApplicationManager.getApplication()); + processor.addCommandListener(this); + } + + private void installSynchronizer(Editor editor) { + final Project project = editor.getProject(); + if (project == null) return; + + final Document document = editor.getDocument(); + final VirtualFile file = myFileDocumentManager.getFile(document); + final FileType type = file != null ? file.getFileType() : null; + if (isAccepted(type)) new TagNameSynchronizer(editor, project); + } + + private static boolean isAccepted(FileType type) { + return type == XmlFileType.INSTANCE || type == HtmlFileType.INSTANCE || type == XHtmlFileType.INSTANCE; + } + + @NotNull + @Override + public String getComponentName() { + return "XmlTagNameSynchronizer"; + } + + @Override + public void initComponent() { + + } + + @Override + public void disposeComponent() { + + } + + @Override + public void commandStarted(CommandEvent event) { + final TagNameSynchronizer synchronizer = findSynchronizer(event); + if (synchronizer != null) { + synchronizer.commandStarted(); + } + } + + @Nullable + public TagNameSynchronizer findSynchronizer(CommandEvent event) { + final Document document = event.getDocument(); + return document != null ? document.getUserData(SYNCHRONIZER_KEY) : null; + } + + @Override + public void beforeCommandFinished(CommandEvent event) { + final TagNameSynchronizer synchronizer = findSynchronizer(event); + if (synchronizer != null) { + synchronizer.beforeCommandFinished(); + } + } + + private static class TagNameSynchronizer implements DocumentListener { + private PsiDocumentManager myDocumentManager; + + private enum State {INITIAL, TRACKING, APPLYING} + + private final Editor myEditor; + private State myState; + private final List> myMarkers = new SmartList>(); + + public TagNameSynchronizer(Editor editor, Project project) { + myEditor = editor; + final Disposable disposable = ((EditorImpl)editor).getDisposable(); + final Document document = editor.getDocument(); + document.addDocumentListener(this, disposable); + document.putUserData(SYNCHRONIZER_KEY, this); + myDocumentManager = PsiDocumentManager.getInstance(project); + } + + @Override + public void beforeDocumentChange(DocumentEvent event) { + if (myState == State.APPLYING) return; + + if (myState == State.INITIAL) { + final Document document = event.getDocument(); + + final PsiFile file = myDocumentManager.getPsiFile(document); + + if (file == null) return; + + + final SmartList leaders = new SmartList(); + for (Caret caret : myEditor.getCaretModel().getAllCarets()) { + final RangeMarker leader = createTagNameMarker(caret); + if (leader == null) { + clearMarkers(); + return; + } + leader.setGreedyToLeft(true); + leader.setGreedyToRight(true); + leaders.add(leader); + } + if (leaders.isEmpty()) return; + + if (myDocumentManager.isUncommited(document)) { + myDocumentManager.commitDocument(document); + } + + for (RangeMarker leader : leaders) { + final RangeMarker support = findSupport(leader, file, document); + if (support == null) { + clearMarkers(); + return; + } + support.setGreedyToLeft(true); + support.setGreedyToRight(true); + myMarkers.add(Couple.of(leader, support)); + } + + myState = State.TRACKING; + } + if (myMarkers.isEmpty()) return; + + final CharSequence fragment = event.getNewFragment(); + final int offset = event.getOffset(); + final int newLength = event.getNewLength(); + final int oldLength = event.getOldLength(); + + for (int i = 0; i < newLength; i++) { + if (!isValidTagNameChar(fragment.charAt(i))) { + clearMarkers(); + return; + } + } + + boolean fitsInMarker = false; + for (Couple leaderAndSupport : myMarkers) { + final RangeMarker leader = leaderAndSupport.first; + if (!leader.isValid()) { + fitsInMarker = false; + break; + } + fitsInMarker |= offset >= leader.getStartOffset() && offset + oldLength <= leader.getEndOffset(); + } + if (!fitsInMarker) { + clearMarkers(); + } + } + + public void clearMarkers() { + for (Couple leaderAndSupport : myMarkers) { + leaderAndSupport.first.dispose(); + leaderAndSupport.second.dispose(); + } + myMarkers.clear(); + } + + private RangeMarker createTagNameMarker(Caret caret) { + final int offset = caret.getOffset(); + final Document document = myEditor.getDocument(); + final CharSequence sequence = document.getCharsSequence(); + int start = -1; + int end = -1; + for (int i = offset - 1; i >= Math.max(0, offset - 50); i--) { + final char c = sequence.charAt(i); + if (c == '<' || (c == '/' && i > 0 && sequence.charAt(i - 1) == '<')) { + start = i + 1; + break; + } + if (!isValidTagNameChar(c)) break; + } + if (start < 0) return null; + for (int i = offset; i < Math.min(document.getTextLength(), offset + 50); i++) { + final char c = sequence.charAt(i); + if (!isValidTagNameChar(c)) { + end = i; + break; + } + } + if (end < 0 || start >= end) return null; + return document.createRangeMarker(start, end, true); + } + + private static boolean isValidTagNameChar(char c) { + return Character.isJavaIdentifierPart(c) || c == ':'; + } + + @Override + public void documentChanged(DocumentEvent event) {} + + public void commandStarted() { + myState = State.INITIAL; + clearMarkers(); + } + + public void beforeCommandFinished() { + if (myMarkers.isEmpty()) return; + + myState = State.APPLYING; + + final Document document = myEditor.getDocument(); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + for (Couple couple : myMarkers) { + final RangeMarker leader = couple.first; + final RangeMarker support = couple.second; + final String name = document.getText(new TextRange(leader.getStartOffset(), leader.getEndOffset())); + document.replaceString(support.getStartOffset(), support.getEndOffset(), name); + } + } + }); + clearMarkers(); + } + + private static RangeMarker findSupport(RangeMarker leader, PsiFile file, Document document) { + final PsiElement element = file.findElementAt(leader.getStartOffset()); + PsiElement support = RenameTagBeginOrEndIntentionAction.findOtherSide(element, false); + support = support == null ? RenameTagBeginOrEndIntentionAction.findOtherSide(element, true) : support; + final TextRange range = support != null ? support.getTextRange() : null; + return range != null ? document.createRangeMarker(range.getStartOffset(), range.getEndOffset(), true) : null; + } + } +} diff --git a/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/RenameTagBeginOrEndIntentionAction.java b/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/RenameTagBeginOrEndIntentionAction.java index 71b587b12a7e..1231a79bd449 100644 --- a/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/RenameTagBeginOrEndIntentionAction.java +++ b/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/RenameTagBeginOrEndIntentionAction.java @@ -32,6 +32,7 @@ import com.intellij.psi.xml.XmlTokenType; import com.intellij.util.IncorrectOperationException; import com.intellij.xml.util.XmlTagUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * @author spleaner @@ -89,23 +90,7 @@ public class RenameTagBeginOrEndIntentionAction implements IntentionAction { } else { // we're in the other - PsiElement parent = psiElement.getParent(); - if (parent instanceof PsiErrorElement) { - parent = parent.getParent(); - } - - if (parent instanceof XmlTag) { - if (myStart) { - target = XmlTagUtil.getStartTagNameElement((XmlTag)parent); - } - else { - target = XmlTagUtil.getEndTagNameElement((XmlTag)parent); - if (target == null) { - final PsiErrorElement errorElement = PsiTreeUtil.getChildOfType(parent, PsiErrorElement.class); - target = XmlWrongClosingTagNameInspection.findEndTagName(errorElement); - } - } - } + target = findOtherSide(psiElement, myStart); } if (target != null) { @@ -119,6 +104,29 @@ public class RenameTagBeginOrEndIntentionAction implements IntentionAction { } } + @Nullable + public static PsiElement findOtherSide(PsiElement psiElement, final boolean start) { + PsiElement target = null; + PsiElement parent = psiElement.getParent(); + if (parent instanceof PsiErrorElement) { + parent = parent.getParent(); + } + + if (parent instanceof XmlTag) { + if (start) { + target = XmlTagUtil.getStartTagNameElement((XmlTag)parent); + } + else { + target = XmlTagUtil.getEndTagNameElement((XmlTag)parent); + if (target == null) { + final PsiErrorElement errorElement = PsiTreeUtil.getChildOfType(parent, PsiErrorElement.class); + target = XmlWrongClosingTagNameInspection.findEndTagName(errorElement); + } + } + } + return target; + } + @Override public boolean startInWriteAction() { return true;