diff --git a/RegExpSupport/src/META-INF/RegExpPlugin.xml b/RegExpSupport/src/META-INF/RegExpPlugin.xml index a245a2eae7cd..ae16a2118329 100644 --- a/RegExpSupport/src/META-INF/RegExpPlugin.xml +++ b/RegExpSupport/src/META-INF/RegExpPlugin.xml @@ -13,5 +13,10 @@ + + + org.intellij.lang.regexp.intention.CheckRegExpIntentionAction + Declaration + diff --git a/plugins/IntelliLang/src/intentionDescriptions/CheckRegExpIntentionAction/after.java.template b/RegExpSupport/src/intentionDescriptions/CheckRegExpIntentionAction/after.java.template similarity index 100% rename from plugins/IntelliLang/src/intentionDescriptions/CheckRegExpIntentionAction/after.java.template rename to RegExpSupport/src/intentionDescriptions/CheckRegExpIntentionAction/after.java.template diff --git a/plugins/IntelliLang/src/intentionDescriptions/CheckRegExpIntentionAction/before.java.template b/RegExpSupport/src/intentionDescriptions/CheckRegExpIntentionAction/before.java.template similarity index 100% rename from plugins/IntelliLang/src/intentionDescriptions/CheckRegExpIntentionAction/before.java.template rename to RegExpSupport/src/intentionDescriptions/CheckRegExpIntentionAction/before.java.template diff --git a/plugins/IntelliLang/src/intentionDescriptions/CheckRegExpIntentionAction/description.html b/RegExpSupport/src/intentionDescriptions/CheckRegExpIntentionAction/description.html similarity index 100% rename from plugins/IntelliLang/src/intentionDescriptions/CheckRegExpIntentionAction/description.html rename to RegExpSupport/src/intentionDescriptions/CheckRegExpIntentionAction/description.html diff --git a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpForm.form b/RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpForm.form similarity index 97% rename from plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpForm.form rename to RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpForm.form index 8203227fdfc0..8d372340c61c 100644 --- a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpForm.form +++ b/RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpForm.form @@ -1,5 +1,5 @@ -
+ diff --git a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpForm.java b/RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpForm.java similarity index 98% rename from plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpForm.java rename to RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpForm.java index 3ceebc01d760..27f07d8f0028 100644 --- a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpForm.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpForm.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2011 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.intellij.plugins.intelliLang.inject.quickedit; +package org.intellij.lang.regexp.intention; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.editor.Document; diff --git a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpIntentionAction.java b/RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpIntentionAction.java similarity index 94% rename from plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpIntentionAction.java rename to RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpIntentionAction.java index 64bdcbad25a7..ce814d398369 100644 --- a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/CheckRegExpIntentionAction.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/intention/CheckRegExpIntentionAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2011 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.intellij.plugins.intelliLang.inject.quickedit; +package org.intellij.lang.regexp.intention; +import com.intellij.codeInsight.intention.impl.QuickEditAction; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditAction.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditAction.java new file mode 100644 index 000000000000..df76329d288b --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditAction.java @@ -0,0 +1,124 @@ +/* + * Copyright 2006 Sascha Weinreuter + * + * 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.intention.IntentionAction; +import com.intellij.codeInsight.intention.LowPriorityAction; +import com.intellij.injected.editor.DocumentWindow; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.util.*; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +/** + * "Quick Edit Language" intention action that provides an editor which shows an injected language + * fragment's complete prefix and suffix in non-editable areas and allows to edit the fragment + * without having to consider any additional escaping rules (e.g. when editing regexes in String + * literals). + * + * @author Gregory Shrago + * @author Konstantin Bulenkov + */ +public class QuickEditAction implements IntentionAction, LowPriorityAction { + public static final Key QUICK_EDIT_HANDLER = Key.create("QUICK_EDIT_HANDLER"); + private String myLastLanguageName; + + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + return getRangePair(file, editor) != null; + } + + @Nullable + protected Pair getRangePair(final PsiFile file, final Editor editor) { + final int offset = editor.getCaretModel().getOffset(); + final PsiLanguageInjectionHost host = + PsiTreeUtil.getParentOfType(file.findElementAt(offset), PsiLanguageInjectionHost.class, false); + if (host == null) return null; + final List> injections = InjectedLanguageUtil.getInjectedPsiFiles(host); + if (injections == null || injections.isEmpty()) return null; + final int offsetInElement = offset - host.getTextRange().getStartOffset(); + final Pair rangePair = ContainerUtil.find(injections, new Condition>() { + public boolean value(final Pair pair) { + return pair.second.containsRange(offsetInElement, offsetInElement); + } + }); + if (rangePair != null) { + myLastLanguageName = rangePair.first.getContainingFile().getLanguage().getDisplayName(); + } + return rangePair; + } + + public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { + final int offset = editor.getCaretModel().getOffset(); + final Pair pair = getRangePair(file, editor); + assert pair != null; + final PsiFile injectedFile = (PsiFile)pair.first; + final int injectedOffset = ((DocumentWindow)PsiDocumentManager.getInstance(project).getDocument(injectedFile)).hostToInjected(offset); + getHandler(project, injectedFile, editor, file).navigate(injectedOffset); + } + + public boolean startInWriteAction() { + return false; + } + + @NotNull + private QuickEditHandler getHandler(Project project, PsiFile injectedFile, Editor editor, PsiFile origFile) { + QuickEditHandler handler = injectedFile.getUserData(QUICK_EDIT_HANDLER); + if (handler != null && handler.isValid()) { + return handler; + } + handler = new QuickEditHandler(project, injectedFile, origFile, editor, this); + injectedFile.putUserData(QUICK_EDIT_HANDLER, handler); + return handler; + } + + protected boolean isShowInBalloon() { + return false; + } + + @Nullable + protected JComponent createBalloonComponent(PsiFile file, Ref ref) { + return null; + } + + @NotNull + public String getText() { + return "Edit "+ StringUtil.notNullize(myLastLanguageName, "Injected")+" Fragment"; + } + + @NotNull + public String getFamilyName() { + return "Edit Injected Fragment"; + } + + public static Balloon.Position getBalloonPosition(Editor editor) { + final int line = editor.getCaretModel().getVisualPosition().line; + final Rectangle area = editor.getScrollingModel().getVisibleArea(); + int startLine = area.y / editor.getLineHeight() + 1; + return (line - startLine) * editor.getLineHeight() < 200 ? Balloon.Position.below : Balloon.Position.above; + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java new file mode 100644 index 000000000000..596f57009729 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/QuickEditHandler.java @@ -0,0 +1,309 @@ +/* + * Copyright 2000-2012 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.lookup.LookupManager; +import com.intellij.lang.Language; +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonShortcuts; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.*; +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.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.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.StringUtil; +import com.intellij.psi.*; +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.testFramework.LightVirtualFile; +import com.intellij.ui.awt.RelativePoint; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.Convertor; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** +* @author Konstantin Bulenkov +*/ +public class QuickEditHandler extends DocumentAdapter implements Disposable { + private final Project myProject; + private final PsiFile myInjectedFile; + private final Editor myEditor; + private final QuickEditAction myAction; + private final Document myOrigDocument; + private final PsiFile myNewFile; + private final LightVirtualFile myNewVirtualFile; + private final Document myNewDocument; + private final Map> myMarkers = + new LinkedHashMap>(); + private EditorWindow mySplittedWindow; + private boolean myReleased; + + QuickEditHandler(Project project, PsiFile injectedFile, final PsiFile origFile, Editor editor, QuickEditAction action) { + myProject = project; + myInjectedFile = injectedFile; + myEditor = editor; + myAction = action; + myOrigDocument = editor.getDocument(); + final Place shreds = InjectedLanguageUtil.getShreds(myInjectedFile); + final FileType fileType = injectedFile.getFileType(); + final Language language = injectedFile.getLanguage(); + + final PsiFileFactory factory = PsiFileFactory.getInstance(project); + final String text = InjectedLanguageManager.getInstance(project).getUnescapedText(injectedFile); + final String newFileName = + StringUtil.notNullize(language.getDisplayName(), "Injected") + " Fragment " + "(" + + origFile.getName() + ":" + shreds.get(0).getHost().getTextRange().getStartOffset() + ")" + "." + fileType.getDefaultExtension(); + myNewFile = factory.createFileFromText(newFileName, language, text, true, true); + myNewVirtualFile = (LightVirtualFile)myNewFile.getVirtualFile(); + assert myNewVirtualFile != null; + final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(project); + myNewFile.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, smartPointerManager.createSmartPsiElementPointer(origFile)); + myNewDocument = PsiDocumentManager.getInstance(project).getDocument(myNewFile); + assert myNewDocument != null; + EditorActionManager.getInstance().setReadonlyFragmentModificationHandler(myNewDocument, new ReadonlyFragmentModificationHandler() { + public void handle(final ReadOnlyFragmentModificationException e) { + //nothing + } + }); + myOrigDocument.addDocumentListener(this); + myNewDocument.addDocumentListener(new DocumentAdapter() { + @Override + public void documentChanged(DocumentEvent e) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + commitToOriginal(); + } + }); + } + }); + EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() { + @Override + public void editorCreated(@NotNull EditorFactoryEvent event) { + if (event.getEditor().getDocument() == myNewDocument) { + new AnAction() { + @Override + public void update(AnActionEvent e) { + Editor editor = PlatformDataKeys.EDITOR.getData(e.getDataContext()); + e.getPresentation().setEnabled(editor != null && LookupManager.getActiveLookup(editor) == null); + } + + @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) { + Disposer.dispose(QuickEditHandler.this); + myReleased = true; + myOrigDocument.removeDocumentListener(QuickEditHandler.this); + myInjectedFile.putUserData(QuickEditAction.QUICK_EDIT_HANDLER, null); + } + } + }, this); + initMarkers(shreds); + } + + public boolean isValid() { + return myNewVirtualFile.isValid() && myInjectedFile.isValid(); + } + + public void navigate(int injectedOffset) { + if (myAction.isShowInBalloon()) { + Ref ref = Ref.create(null); + final JComponent component = myAction.createBalloonComponent(myNewFile, ref); + if (component != null) { + final Balloon balloon = JBPopupFactory.getInstance().createBalloonBuilder(component) + .setShadow(true) + .setAnimationCycle(0) + .setHideOnClickOutside(true) + .setHideOnKeyOutside(true) + .setHideOnAction(false) + .setFillColor(UIUtil.getControlColor()) + .createBalloon(); + ref.set(balloon); + 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); + } + fileEditorManager.openTextEditor(new OpenFileDescriptor(myProject, myNewVirtualFile, injectedOffset), true); + } + } + + @Override + public void documentChanged(DocumentEvent e) { + closeEditor(); + } + + private void closeEditor() { + boolean unsplit = false; + if (mySplittedWindow != null && !mySplittedWindow.isDisposed()) { + final EditorWithProviderComposite[] editors = mySplittedWindow.getEditors(); + if (editors.length == 1 && editors[0].getFile() == myNewVirtualFile) { + unsplit = true; + } + } + FileEditorManager.getInstance(myProject).closeFile(myNewVirtualFile); + if (unsplit) { + for (EditorWindow editorWindow : mySplittedWindow.findSiblings()) { + editorWindow.unsplit(true); + } + } + } + + public void initMarkers(final Place shreds) { + final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject); + 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(); + final RangeMarker origMarker = + myOrigDocument.createRangeMarker(rangeInsideHost.shiftRight(shred.getHost().getTextRange().getStartOffset())); + myMarkers.put(smartPointerManager.createSmartPsiElementPointer(shred.getHost()), Pair.create(origMarker, rangeMarker)); + } + for (Pair markers : myMarkers.values()) { + markers.first.setGreedyToLeft(true); + markers.second.setGreedyToLeft(true); + markers.first.setGreedyToRight(true); + markers.second.setGreedyToRight(true); + } + int curOffset = 0; + for (Pair markerPair : myMarkers.values()) { + final RangeMarker marker = markerPair.second; + final int start = marker.getStartOffset(); + final int end = marker.getEndOffset(); + if (curOffset < start) { + final RangeMarker rangeMarker = myNewDocument.createGuardedBlock(curOffset, start); + if (curOffset == 0) rangeMarker.setGreedyToLeft(true); + } + curOffset = end; + } + if (curOffset < myNewDocument.getTextLength()) { + final RangeMarker rangeMarker = myNewDocument.createGuardedBlock(curOffset, myNewDocument.getTextLength()); + rangeMarker.setGreedyToRight(true); + } + } + + private void commitToOriginal() { + if (!isValid()) return; + final PsiFile origFile = (PsiFile)myNewFile.getUserData(FileContextUtil.INJECTED_IN_ELEMENT).getElement(); + if (!myReleased) myOrigDocument.removeDocumentListener(this); + try { + new WriteCommandAction.Simple(myProject, origFile) { + @Override + protected void run() throws Throwable { + PostprocessReformattingAspect.getInstance(myProject).disablePostprocessFormattingInside(new Runnable() { + @Override + public void run() { + commitToOriginalInner(); + } + }); + } + }.execute(); + } + finally { + if (!myReleased) myOrigDocument.addDocumentListener(this); + } + } + + private void commitToOriginalInner() { + final String text = myNewDocument.getText(); + final Map>>> map = ContainerUtil + .classify(myMarkers.entrySet().iterator(), + new Convertor>, PsiLanguageInjectionHost>() { + public PsiLanguageInjectionHost convert(final Map.Entry> o) { + final PsiElement element = o.getKey().getElement(); + return (PsiLanguageInjectionHost)element; + } + }); + PsiDocumentManager.getInstance(myProject).commitDocument(myOrigDocument); + int localInsideFileCursor = 0; + for (PsiLanguageInjectionHost host : map.keySet()) { + if (host == null) continue; + final String hostText = host.getText(); + TextRange insideHost = null; + final StringBuilder sb = new StringBuilder(); + for (Map.Entry> entry : map.get(host)) { + final RangeMarker origMarker = entry.getValue().first; + final int hostOffset = host.getTextRange().getStartOffset(); + final TextRange localInsideHost = + new TextRange(origMarker.getStartOffset() - hostOffset, origMarker.getEndOffset() - hostOffset); + final RangeMarker rangeMarker = entry.getValue().second; + final TextRange localInsideFile = new TextRange(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()); + } + } + + @Override + public void dispose() { + // noop + } +} diff --git a/plugins/IntelliLang/src/intentionDescriptions/QuickEditAction/after.java.template b/platform/platform-resources-en/src/intentionDescriptions/QuickEditAction/after.java.template similarity index 100% rename from plugins/IntelliLang/src/intentionDescriptions/QuickEditAction/after.java.template rename to platform/platform-resources-en/src/intentionDescriptions/QuickEditAction/after.java.template diff --git a/plugins/IntelliLang/src/intentionDescriptions/QuickEditAction/before.java.template b/platform/platform-resources-en/src/intentionDescriptions/QuickEditAction/before.java.template similarity index 100% rename from plugins/IntelliLang/src/intentionDescriptions/QuickEditAction/before.java.template rename to platform/platform-resources-en/src/intentionDescriptions/QuickEditAction/before.java.template diff --git a/plugins/IntelliLang/src/intentionDescriptions/QuickEditAction/description.html b/platform/platform-resources-en/src/intentionDescriptions/QuickEditAction/description.html similarity index 100% rename from plugins/IntelliLang/src/intentionDescriptions/QuickEditAction/description.html rename to platform/platform-resources-en/src/intentionDescriptions/QuickEditAction/description.html diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 235aad239af4..369fa4693e25 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -551,6 +551,10 @@ com.intellij.codeInsight.intention.impl.EditFoldingOptionsAction + + com.intellij.codeInsight.intention.impl.QuickEditAction + Language Injection + diff --git a/plugins/IntelliLang/src/META-INF/plugin.xml b/plugins/IntelliLang/src/META-INF/plugin.xml index 9db7851caca5..8682c2d26726 100644 --- a/plugins/IntelliLang/src/META-INF/plugin.xml +++ b/plugins/IntelliLang/src/META-INF/plugin.xml @@ -30,16 +30,6 @@ Language Injection - - org.intellij.plugins.intelliLang.inject.quickedit.QuickEditAction - Language Injection - - - - org.intellij.plugins.intelliLang.inject.quickedit.CheckRegExpIntentionAction - Declaration - - diff --git a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/QuickEditAction.java b/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/QuickEditAction.java deleted file mode 100644 index 48d6aaf37d54..000000000000 --- a/plugins/IntelliLang/src/org/intellij/plugins/intelliLang/inject/quickedit/QuickEditAction.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright 2006 Sascha Weinreuter - * - * 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 org.intellij.plugins.intelliLang.inject.quickedit; - -import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.codeInsight.intention.LowPriorityAction; -import com.intellij.codeInsight.lookup.LookupManager; -import com.intellij.injected.editor.DocumentWindow; -import com.intellij.lang.Language; -import com.intellij.lang.injection.InjectedLanguageManager; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonShortcuts; -import com.intellij.openapi.actionSystem.PlatformDataKeys; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.editor.*; -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.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.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.StringUtil; -import com.intellij.psi.*; -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.PsiTreeUtil; -import com.intellij.testFramework.LightVirtualFile; -import com.intellij.ui.awt.RelativePoint; -import com.intellij.util.IncorrectOperationException; -import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.containers.Convertor; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * "Quick Edit Language" intention action that provides an editor which shows an injected language - * fragment's complete prefix and suffix in non-editable areas and allows to edit the fragment - * without having to consider any additional escaping rules (e.g. when editing regexes in String - * literals). - * - * @author Gregory Shrago - * @author Konstantin Bulenkov - */ -public class QuickEditAction implements IntentionAction, LowPriorityAction { - private String myLastLanguageName; - - public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { - return getRangePair(file, editor) != null; - } - - @Nullable - protected Pair getRangePair(final PsiFile file, final Editor editor) { - final int offset = editor.getCaretModel().getOffset(); - final PsiLanguageInjectionHost host = - PsiTreeUtil.getParentOfType(file.findElementAt(offset), PsiLanguageInjectionHost.class, false); - if (host == null) return null; - final List> injections = InjectedLanguageUtil.getInjectedPsiFiles(host); - if (injections == null || injections.isEmpty()) return null; - final int offsetInElement = offset - host.getTextRange().getStartOffset(); - final Pair rangePair = ContainerUtil.find(injections, new Condition>() { - public boolean value(final Pair pair) { - return pair.second.containsRange(offsetInElement, offsetInElement); - } - }); - if (rangePair != null) { - myLastLanguageName = rangePair.first.getContainingFile().getLanguage().getDisplayName(); - } - return rangePair; - } - - public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { - final int offset = editor.getCaretModel().getOffset(); - final Pair pair = getRangePair(file, editor); - assert pair != null; - final PsiFile injectedFile = (PsiFile)pair.first; - final int injectedOffset = ((DocumentWindow)PsiDocumentManager.getInstance(project).getDocument(injectedFile)).hostToInjected(offset); - getHandler(project, injectedFile, editor, file).navigate(injectedOffset); - } - - public boolean startInWriteAction() { - return false; - } - - private static final Key QUICK_EDIT_HANDLER = Key.create("QUICK_EDIT_HANDLER"); - - @NotNull - private MyHandler getHandler(Project project, PsiFile injectedFile, Editor editor, PsiFile origFile) { - MyHandler handler = injectedFile.getUserData(QUICK_EDIT_HANDLER); - if (handler != null && handler.isValid()) { - return handler; - } - handler = new MyHandler(project, injectedFile, origFile, editor); - injectedFile.putUserData(QUICK_EDIT_HANDLER, handler); - return handler; - } - - protected boolean isShowInBalloon() { - return false; - } - - @Nullable - protected JComponent createBalloonComponent(PsiFile file, Ref ref) { - return null; - } - - @NotNull - public String getText() { - return "Edit "+ StringUtil.notNullize(myLastLanguageName, "Injected")+" Fragment"; - } - - @NotNull - public String getFamilyName() { - return "Edit Injected Fragment"; - } - - private class MyHandler extends DocumentAdapter implements Disposable { - private final Project myProject; - private final PsiFile myInjectedFile; - private final Editor myEditor; - private final Document myOrigDocument; - private final PsiFile myNewFile; - private final LightVirtualFile myNewVirtualFile; - private final Document myNewDocument; - private final Map> myMarkers = - new LinkedHashMap>(); - private EditorWindow mySplittedWindow; - private boolean myReleased; - - private MyHandler(Project project, PsiFile injectedFile, final PsiFile origFile, Editor editor) { - myProject = project; - myInjectedFile = injectedFile; - myEditor = editor; - myOrigDocument = editor.getDocument(); - final Place shreds = InjectedLanguageUtil.getShreds(myInjectedFile); - final FileType fileType = injectedFile.getFileType(); - final Language language = injectedFile.getLanguage(); - - final PsiFileFactory factory = PsiFileFactory.getInstance(project); - final String text = InjectedLanguageManager.getInstance(project).getUnescapedText(injectedFile); - final String newFileName = - StringUtil.notNullize(language.getDisplayName(), "Injected") + " Fragment " + "(" + - origFile.getName() + ":" + shreds.get(0).getHost().getTextRange().getStartOffset() + ")" + "." + fileType.getDefaultExtension(); - myNewFile = factory.createFileFromText(newFileName, language, text, true, true); - myNewVirtualFile = (LightVirtualFile)myNewFile.getVirtualFile(); - assert myNewVirtualFile != null; - final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(project); - myNewFile.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, smartPointerManager.createSmartPsiElementPointer(origFile)); - myNewDocument = PsiDocumentManager.getInstance(project).getDocument(myNewFile); - assert myNewDocument != null; - EditorActionManager.getInstance().setReadonlyFragmentModificationHandler(myNewDocument, new ReadonlyFragmentModificationHandler() { - public void handle(final ReadOnlyFragmentModificationException e) { - //nothing - } - }); - myOrigDocument.addDocumentListener(this); - myNewDocument.addDocumentListener(new DocumentAdapter() { - @Override - public void documentChanged(DocumentEvent e) { - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - commitToOriginal(); - } - }); - } - }); - EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() { - @Override - public void editorCreated(@NotNull EditorFactoryEvent event) { - if (event.getEditor().getDocument() == myNewDocument) { - new AnAction() { - @Override - public void update(AnActionEvent e) { - Editor editor = PlatformDataKeys.EDITOR.getData(e.getDataContext()); - e.getPresentation().setEnabled(editor != null && LookupManager.getActiveLookup(editor) == null); - } - - @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) { - Disposer.dispose(MyHandler.this); - myReleased = true; - myOrigDocument.removeDocumentListener(MyHandler.this); - myInjectedFile.putUserData(QUICK_EDIT_HANDLER, null); - } - } - }, this); - initMarkers(shreds); - } - - public boolean isValid() { - return myNewVirtualFile.isValid() && myInjectedFile.isValid(); - } - - public void navigate(int injectedOffset) { - if (isShowInBalloon()) { - Ref ref = Ref.create(null); - final JComponent component = createBalloonComponent(myNewFile, ref); - if (component != null) { - final Balloon balloon = JBPopupFactory.getInstance().createBalloonBuilder(component) - .setShadow(true) - .setAnimationCycle(0) - .setHideOnClickOutside(true) - .setHideOnKeyOutside(true) - .setHideOnAction(false) - .setFillColor(UIUtil.getControlColor()) - .createBalloon(); - ref.set(balloon); - Disposer.register(myNewFile.getProject(), balloon); - final Balloon.Position position = 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); - } - fileEditorManager.openTextEditor(new OpenFileDescriptor(myProject, myNewVirtualFile, injectedOffset), true); - } - } - - @Override - public void documentChanged(DocumentEvent e) { - closeEditor(); - } - - private void closeEditor() { - boolean unsplit = false; - if (mySplittedWindow != null && !mySplittedWindow.isDisposed()) { - final EditorWithProviderComposite[] editors = mySplittedWindow.getEditors(); - if (editors.length == 1 && editors[0].getFile() == myNewVirtualFile) { - unsplit = true; - } - } - FileEditorManager.getInstance(myProject).closeFile(myNewVirtualFile); - if (unsplit) { - for (EditorWindow editorWindow : mySplittedWindow.findSiblings()) { - editorWindow.unsplit(true); - } - } - } - - public void initMarkers(final Place shreds) { - final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject); - 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(); - final RangeMarker origMarker = - myOrigDocument.createRangeMarker(rangeInsideHost.shiftRight(shred.getHost().getTextRange().getStartOffset())); - myMarkers.put(smartPointerManager.createSmartPsiElementPointer(shred.getHost()), Pair.create(origMarker, rangeMarker)); - } - for (Pair markers : myMarkers.values()) { - markers.first.setGreedyToLeft(true); - markers.second.setGreedyToLeft(true); - markers.first.setGreedyToRight(true); - markers.second.setGreedyToRight(true); - } - int curOffset = 0; - for (Pair markerPair : myMarkers.values()) { - final RangeMarker marker = markerPair.second; - final int start = marker.getStartOffset(); - final int end = marker.getEndOffset(); - if (curOffset < start) { - final RangeMarker rangeMarker = myNewDocument.createGuardedBlock(curOffset, start); - if (curOffset == 0) rangeMarker.setGreedyToLeft(true); - } - curOffset = end; - } - if (curOffset < myNewDocument.getTextLength()) { - final RangeMarker rangeMarker = myNewDocument.createGuardedBlock(curOffset, myNewDocument.getTextLength()); - rangeMarker.setGreedyToRight(true); - } - } - - private void commitToOriginal() { - if (!isValid()) return; - final PsiFile origFile = (PsiFile)myNewFile.getUserData(FileContextUtil.INJECTED_IN_ELEMENT).getElement(); - if (!myReleased) myOrigDocument.removeDocumentListener(this); - try { - new WriteCommandAction.Simple(myProject, origFile) { - @Override - protected void run() throws Throwable { - PostprocessReformattingAspect.getInstance(myProject).disablePostprocessFormattingInside(new Runnable() { - @Override - public void run() { - commitToOriginalInner(); - } - }); - } - }.execute(); - } - finally { - if (!myReleased) myOrigDocument.addDocumentListener(this); - } - } - - private void commitToOriginalInner() { - final String text = myNewDocument.getText(); - final Map>>> map = ContainerUtil - .classify(myMarkers.entrySet().iterator(), - new Convertor>, PsiLanguageInjectionHost>() { - public PsiLanguageInjectionHost convert(final Map.Entry> o) { - final PsiElement element = o.getKey().getElement(); - return (PsiLanguageInjectionHost)element; - } - }); - PsiDocumentManager.getInstance(myProject).commitDocument(myOrigDocument); - int localInsideFileCursor = 0; - for (PsiLanguageInjectionHost host : map.keySet()) { - if (host == null) continue; - final String hostText = host.getText(); - TextRange insideHost = null; - final StringBuilder sb = new StringBuilder(); - for (Map.Entry> entry : map.get(host)) { - final RangeMarker origMarker = entry.getValue().first; - final int hostOffset = host.getTextRange().getStartOffset(); - final TextRange localInsideHost = - new TextRange(origMarker.getStartOffset() - hostOffset, origMarker.getEndOffset() - hostOffset); - final RangeMarker rangeMarker = entry.getValue().second; - final TextRange localInsideFile = new TextRange(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()); - } - } - - @Override - public void dispose() { - // noop - } - } - - private static Balloon.Position getBalloonPosition(Editor editor) { - final int line = editor.getCaretModel().getVisualPosition().line; - final Rectangle area = editor.getScrollingModel().getVisibleArea(); - int startLine = area.y / editor.getLineHeight() + 1; - return (line - startLine) * editor.getLineHeight() < 200 ? Balloon.Position.below : Balloon.Position.above; - } -}