blob: b236e1061315223cd57bc39f9264001d046c61e9 [file] [log] [blame]
/*
* Copyright 2000-2013 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.intention.impl;
import com.intellij.codeInsight.editorActions.CopyPastePreProcessor;
import com.intellij.codeInsight.lookup.LookupManager;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.lang.Language;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.EditorFactoryAdapter;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.fileEditor.impl.EditorWindow;
import com.intellij.openapi.fileEditor.impl.EditorWithProviderComposite;
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringHash;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
import com.intellij.psi.impl.source.resolve.FileContextUtil;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.impl.source.tree.injected.Place;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.DocumentUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.hash.LinkedHashMap;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Gregory Shrago
*/
public class QuickEditHandler extends DocumentAdapter implements Disposable {
private final Project myProject;
private final QuickEditAction myAction;
private final Editor myEditor;
private final Document myOrigDocument;
private final Document myNewDocument;
private final PsiFile myNewFile;
private final LightVirtualFile myNewVirtualFile;
private final long myOrigCreationStamp;
private EditorWindow mySplittedWindow;
private boolean myCommittingToOriginal;
@Nullable
private final PsiFile myInjectedFile;
private final List<Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer>> myMarkers = ContainerUtil.newLinkedList();
@Nullable
private final RangeMarker myAltFullRange;
private static final Key<String> REPLACEMENT_KEY = Key.create("REPLACEMENT_KEY");
QuickEditHandler(Project project, @NotNull PsiFile injectedFile, final PsiFile origFile, Editor editor, QuickEditAction action) {
myProject = project;
myEditor = editor;
myAction = action;
myOrigDocument = editor.getDocument();
Place shreds = InjectedLanguageUtil.getShreds(injectedFile);
FileType fileType = injectedFile.getFileType();
Language language = injectedFile.getLanguage();
PsiLanguageInjectionHost.Shred firstShred = ContainerUtil.getFirstItem(shreds);
PsiFileFactory factory = PsiFileFactory.getInstance(project);
String text = InjectedLanguageManager.getInstance(project).getUnescapedText(injectedFile);
String newFileName =
StringUtil.notNullize(language.getDisplayName(), "Injected") + " Fragment " + "(" +
origFile.getName() + ":" + firstShred.getHost().getTextRange().getStartOffset() + ")" + "." + fileType.getDefaultExtension();
// preserve \r\n as it is done in MultiHostRegistrarImpl
myNewFile = factory.createFileFromText(newFileName, language, text, true, false);
myNewVirtualFile = ObjectUtils.assertNotNull((LightVirtualFile)myNewFile.getVirtualFile());
myNewVirtualFile.setOriginalFile(origFile.getVirtualFile());
assert myNewFile != null : "PSI file is null";
assert myNewFile.getTextLength() == myNewVirtualFile.getContent().length() : "PSI / Virtual file text mismatch";
myNewVirtualFile.setOriginalFile(origFile.getVirtualFile());
// suppress possible errors as in injected mode
myNewFile.putUserData(InjectedLanguageUtil.FRANKENSTEIN_INJECTION,
injectedFile.getUserData(InjectedLanguageUtil.FRANKENSTEIN_INJECTION));
myNewFile.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, shreds.getHostPointer());
myNewDocument = PsiDocumentManager.getInstance(project).getDocument(myNewFile);
assert myNewDocument != null;
EditorActionManager.getInstance().setReadonlyFragmentModificationHandler(myNewDocument, new MyQuietHandler());
myOrigCreationStamp = myOrigDocument.getModificationStamp(); // store creation stamp for UNDO tracking
myOrigDocument.addDocumentListener(this, this);
myNewDocument.addDocumentListener(this, this);
EditorFactory editorFactory = ObjectUtils.assertNotNull(EditorFactory.getInstance());
// not FileEditorManager listener because of RegExp checker and alike
editorFactory.addEditorFactoryListener(new EditorFactoryAdapter() {
int myEditorCount;
@Override
public void editorCreated(@NotNull EditorFactoryEvent event) {
if (event.getEditor().getDocument() != myNewDocument) return;
myEditorCount ++;
final EditorActionHandler editorEscape = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ESCAPE);
if (!myAction.isShowInBalloon()) {
new AnAction() {
@Override
public void update(AnActionEvent e) {
Editor editor = CommonDataKeys.EDITOR.getData(e.getDataContext());
e.getPresentation().setEnabled(
editor != null && LookupManager.getActiveLookup(editor) == null &&
TemplateManager.getInstance(myProject).getActiveTemplate(editor) == null &&
(editorEscape == null || !editorEscape.isEnabled(editor, e.getDataContext())));
}
@Override
public void actionPerformed(AnActionEvent e) {
closeEditor();
}
}.registerCustomShortcutSet(CommonShortcuts.ESCAPE, event.getEditor().getContentComponent());
}
}
@Override
public void editorReleased(@NotNull EditorFactoryEvent event) {
if (event.getEditor().getDocument() != myNewDocument) return;
if (-- myEditorCount > 0) return;
if (Boolean.TRUE.equals(myNewVirtualFile.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN))) return;
Disposer.dispose(QuickEditHandler.this);
}
}, this);
if ("JAVA".equals(firstShred.getHost().getLanguage().getID())) {
PsiLanguageInjectionHost.Shred lastShred = ContainerUtil.getLastItem(shreds);
myAltFullRange = myOrigDocument.createRangeMarker(
firstShred.getHostRangeMarker().getStartOffset(),
lastShred.getHostRangeMarker().getEndOffset());
myAltFullRange.setGreedyToLeft(true);
myAltFullRange.setGreedyToRight(true);
initGuardedBlocks(shreds);
myInjectedFile = null;
}
else {
initMarkers(shreds);
myAltFullRange = null;
myInjectedFile = injectedFile;
}
}
public boolean isValid() {
boolean valid = myNewVirtualFile.isValid() && (myAltFullRange == null && myInjectedFile.isValid() || myAltFullRange.isValid());
if (valid) {
for (Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> t : myMarkers) {
if (!t.first.isValid() || !t.second.isValid() || t.third.getElement() == null) {
valid = false;
break;
}
}
}
return valid;
}
public void navigate(int injectedOffset) {
if (myAction.isShowInBalloon()) {
final JComponent component = myAction.createBalloonComponent(myNewFile);
if (component != null) {
final Balloon balloon = JBPopupFactory.getInstance().createBalloonBuilder(component)
.setShadow(true)
.setAnimationCycle(0)
.setHideOnClickOutside(true)
.setHideOnKeyOutside(true)
.setHideOnAction(false)
.setFillColor(UIUtil.getControlColor())
.createBalloon();
new AnAction() {
@Override
public void actionPerformed(AnActionEvent e) {
balloon.hide();
}
}.registerCustomShortcutSet(CommonShortcuts.ESCAPE, component);
Disposer.register(myNewFile.getProject(), balloon);
final Balloon.Position position = QuickEditAction.getBalloonPosition(myEditor);
RelativePoint point = JBPopupFactory.getInstance().guessBestPopupLocation(myEditor);
if (position == Balloon.Position.above) {
final Point p = point.getPoint();
point = new RelativePoint(point.getComponent(), new Point(p.x, p.y - myEditor.getLineHeight()));
}
balloon.show(point, position);
}
}
else {
final FileEditorManagerEx fileEditorManager = FileEditorManagerEx.getInstanceEx(myProject);
final FileEditor[] editors = fileEditorManager.getEditors(myNewVirtualFile);
if (editors.length == 0) {
final EditorWindow curWindow = fileEditorManager.getCurrentWindow();
mySplittedWindow = curWindow.split(SwingConstants.HORIZONTAL, false, myNewVirtualFile, true);
}
Editor editor = fileEditorManager.openTextEditor(new OpenFileDescriptor(myProject, myNewVirtualFile, injectedOffset), true);
// fold missing values
if (editor != null) {
editor.putUserData(QuickEditAction.QUICK_EDIT_HANDLER, this);
final FoldingModel foldingModel = editor.getFoldingModel();
foldingModel.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
for (RangeMarker o : ContainerUtil.reverse(((DocumentEx)myNewDocument).getGuardedBlocks())) {
String replacement = o.getUserData(REPLACEMENT_KEY);
if (StringUtil.isEmpty(replacement)) continue;
FoldRegion region = foldingModel.addFoldRegion(o.getStartOffset(), o.getEndOffset(), replacement);
if (region != null) region.setExpanded(false);
}
}
});
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
});
}
}
@Override
public void documentChanged(DocumentEvent e) {
UndoManager undoManager = UndoManager.getInstance(myProject);
boolean undoOrRedo = undoManager.isUndoInProgress() || undoManager.isRedoInProgress();
if (undoOrRedo) {
// allow undo/redo up until 'creation stamp' back in time
// and check it after action is completed
if (e.getDocument() == myOrigDocument) {
//noinspection SSBasedInspection
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (myOrigCreationStamp > myOrigDocument.getModificationStamp()) {
closeEditor();
}
}
});
}
}
else if (e.getDocument() == myNewDocument) {
commitToOriginal();
if (!isValid()) closeEditor();
}
else if (e.getDocument() == myOrigDocument) {
if (myCommittingToOriginal || myAltFullRange != null && myAltFullRange.isValid()) return;
closeEditor();
}
}
private void closeEditor() {
boolean unsplit = false;
if (mySplittedWindow != null && !mySplittedWindow.isDisposed()) {
final EditorWithProviderComposite[] editors = mySplittedWindow.getEditors();
if (editors.length == 1 && Comparing.equal(editors[0].getFile(), myNewVirtualFile)) {
unsplit = true;
}
}
FileEditorManager.getInstance(myProject).closeFile(myNewVirtualFile);
if (unsplit) {
for (EditorWindow editorWindow : mySplittedWindow.findSiblings()) {
editorWindow.unsplit(true);
}
}
}
public void initMarkers(Place shreds) {
SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject);
int curOffset = -1;
for (PsiLanguageInjectionHost.Shred shred : shreds) {
final RangeMarker rangeMarker = myNewDocument.createRangeMarker(
shred.getRange().getStartOffset() + shred.getPrefix().length(),
shred.getRange().getEndOffset() - shred.getSuffix().length());
final TextRange rangeInsideHost = shred.getRangeInsideHost();
PsiLanguageInjectionHost host = shred.getHost();
RangeMarker origMarker = myOrigDocument.createRangeMarker(rangeInsideHost.shiftRight(host.getTextRange().getStartOffset()));
SmartPsiElementPointer<PsiLanguageInjectionHost> elementPointer = smartPointerManager.createSmartPsiElementPointer(host);
Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> markers =
Trinity.<RangeMarker, RangeMarker, SmartPsiElementPointer>create(origMarker, rangeMarker, elementPointer);
myMarkers.add(markers);
origMarker.setGreedyToRight(true);
rangeMarker.setGreedyToRight(true);
if (origMarker.getStartOffset() > curOffset) {
origMarker.setGreedyToLeft(true);
rangeMarker.setGreedyToLeft(true);
}
curOffset = origMarker.getEndOffset();
}
initGuardedBlocks(shreds);
}
private void initGuardedBlocks(Place shreds) {
int origOffset = -1;
int curOffset = 0;
for (PsiLanguageInjectionHost.Shred shred : shreds) {
Segment hostRangeMarker = shred.getHostRangeMarker();
int start = shred.getRange().getStartOffset() + shred.getPrefix().length();
int end = shred.getRange().getEndOffset() - shred.getSuffix().length();
if (curOffset < start) {
RangeMarker guard = myNewDocument.createGuardedBlock(curOffset, start);
if (curOffset == 0 && shred == shreds.get(0)) guard.setGreedyToLeft(true);
String padding = origOffset < 0 ? "" : myOrigDocument.getText().substring(origOffset, hostRangeMarker.getStartOffset());
guard.putUserData(REPLACEMENT_KEY, fixQuotes(padding));
}
curOffset = end;
origOffset = hostRangeMarker.getEndOffset();
}
if (curOffset < myNewDocument.getTextLength()) {
RangeMarker guard = myNewDocument.createGuardedBlock(curOffset, myNewDocument.getTextLength());
guard.setGreedyToRight(true);
guard.putUserData(REPLACEMENT_KEY, "");
}
}
private void commitToOriginal() {
VirtualFile origVirtualFile = PsiUtilCore.getVirtualFile(myNewFile.getContext());
myCommittingToOriginal = true;
try {
if (origVirtualFile == null || !ReadonlyStatusHandler.getInstance(myProject).ensureFilesWritable(origVirtualFile).hasReadonlyFiles()) {
PostprocessReformattingAspect.getInstance(myProject).disablePostprocessFormattingInside(new Runnable() {
@Override
public void run() {
if (myAltFullRange != null) {
altCommitToOriginal();
return;
}
commitToOriginalInner();
}
});
}
}
finally {
myCommittingToOriginal = false;
}
}
private void commitToOriginalInner() {
final String text = myNewDocument.getText();
final Map<PsiLanguageInjectionHost, Set<Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer>>> map = ContainerUtil
.classify(myMarkers.iterator(),
new Convertor<Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer>, PsiLanguageInjectionHost>() {
@Override
public PsiLanguageInjectionHost convert(final Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> o) {
final PsiElement element = o.third.getElement();
return (PsiLanguageInjectionHost)element;
}
});
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
documentManager.commitDocument(myOrigDocument); // commit here and after each manipulator update
int localInsideFileCursor = 0;
for (PsiLanguageInjectionHost host : map.keySet()) {
if (host == null) continue;
String hostText = host.getText();
ProperTextRange insideHost = null;
StringBuilder sb = new StringBuilder();
for (Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> entry : map.get(host)) {
RangeMarker origMarker = entry.first; // check for validity?
int hostOffset = host.getTextRange().getStartOffset();
ProperTextRange localInsideHost = new ProperTextRange(origMarker.getStartOffset() - hostOffset, origMarker.getEndOffset() - hostOffset);
RangeMarker rangeMarker = entry.second;
ProperTextRange localInsideFile = new ProperTextRange(Math.max(localInsideFileCursor, rangeMarker.getStartOffset()), rangeMarker.getEndOffset());
if (insideHost != null) {
//append unchanged inter-markers fragment
sb.append(hostText.substring(insideHost.getEndOffset(), localInsideHost.getStartOffset()));
}
sb.append(localInsideFile.getEndOffset() <= text.length() && !localInsideFile.isEmpty()? localInsideFile.substring(text) : "");
localInsideFileCursor = localInsideFile.getEndOffset();
insideHost = insideHost == null ? localInsideHost : insideHost.union(localInsideHost);
}
assert insideHost != null;
ElementManipulators.getManipulator(host).handleContentChange(host, insideHost, sb.toString());
documentManager.commitDocument(myOrigDocument);
}
}
private void altCommitToOriginal() {
final PsiFile origPsiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myOrigDocument);
String newText = myNewDocument.getText();
// prepare guarded blocks
LinkedHashMap<String, String> replacementMap = new LinkedHashMap<String, String>();
int count = 0;
for (RangeMarker o : ContainerUtil.reverse(((DocumentEx)myNewDocument).getGuardedBlocks())) {
String replacement = o.getUserData(REPLACEMENT_KEY);
String tempText = "REPLACE"+(count++)+Long.toHexString(StringHash.calc(replacement));
newText = newText.substring(0, o.getStartOffset()) + tempText + newText.substring(o.getEndOffset());
replacementMap.put(tempText, replacement);
}
// run preformat processors
myEditor.getCaretModel().moveToOffset(myAltFullRange.getStartOffset());
for (CopyPastePreProcessor preProcessor : Extensions.getExtensions(CopyPastePreProcessor.EP_NAME)) {
newText = preProcessor.preprocessOnPaste(myProject, origPsiFile, myEditor, newText, null);
}
myOrigDocument.replaceString(myAltFullRange.getStartOffset(), myAltFullRange.getEndOffset(), newText);
// replace temp strings for guarded blocks
for (String tempText : replacementMap.keySet()) {
int idx = CharArrayUtil.indexOf(myOrigDocument.getCharsSequence(), tempText, myAltFullRange.getStartOffset(), myAltFullRange.getEndOffset());
myOrigDocument.replaceString(idx, idx + tempText.length(), replacementMap.get(tempText));
}
// JAVA: fix occasional char literal concatenation
fixDocumentQuotes(myOrigDocument, myAltFullRange.getStartOffset() - 1);
fixDocumentQuotes(myOrigDocument, myAltFullRange.getEndOffset());
// reformat
PsiDocumentManager.getInstance(myProject).commitDocument(myOrigDocument);
Runnable task = new Runnable() {
@Override
public void run() {
try {
CodeStyleManager.getInstance(myProject).reformatRange(
origPsiFile, myAltFullRange.getStartOffset(), myAltFullRange.getEndOffset(), true);
}
catch (IncorrectOperationException e) {
//LOG.error(e);
}
}
};
DocumentUtil.executeInBulk(myOrigDocument, true, task);
myEditor.getCaretModel().moveToOffset(myAltFullRange.getStartOffset());
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
private static String fixQuotes(String padding) {
if (padding.isEmpty()) return padding;
if (padding.startsWith("'")) padding = '\"' + padding.substring(1);
if (padding.endsWith("'")) padding = padding.substring(0, padding.length() - 1) + "\"";
return padding;
}
private static void fixDocumentQuotes(Document doc, int offset) {
if (doc.getCharsSequence().charAt(offset) == '\'') {
doc.replaceString(offset, offset+1, "\"");
}
}
@Override
public void dispose() {
// noop
}
@TestOnly
public PsiFile getNewFile() {
return myNewFile;
}
public boolean changesRange(TextRange range) {
if (myAltFullRange != null) {
return range.intersects(myAltFullRange.getStartOffset(), myAltFullRange.getEndOffset());
}
else if (!myMarkers.isEmpty()) {
TextRange hostRange = TextRange.create(myMarkers.get(0).first.getStartOffset(),
myMarkers.get(myMarkers.size()-1).first.getEndOffset());
return range.intersects(hostRange);
}
return false;
}
private static class MyQuietHandler implements ReadonlyFragmentModificationHandler {
@Override
public void handle(final ReadOnlyFragmentModificationException e) {
//nothing
}
}
}