mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
initial implementation of synchronous tag editing WEB-432
This commit is contained in:
@@ -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<TagNameSynchronizer> 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<Couple<RangeMarker>> myMarkers = new SmartList<Couple<RangeMarker>>();
|
||||
|
||||
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<RangeMarker> leaders = new SmartList<RangeMarker>();
|
||||
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<RangeMarker> 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<RangeMarker> 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<RangeMarker> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-17
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user