IDEADEV-41476 Edit HTML fragment: Vanished when resized
[fedora-idea.git] / plugins / IntelliLang / src / org / intellij / plugins / intelliLang / inject / quickedit / QuickEditAction.java
blobd8f15f90a4328822cd68fe68cbd8d54c1b9edaac
1 /*
2 * Copyright 2006 Sascha Weinreuter
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.intellij.plugins.intelliLang.inject.quickedit;
18 import com.intellij.codeInsight.intention.IntentionAction;
19 import com.intellij.lang.injection.InjectedLanguageManager;
20 import com.intellij.openapi.editor.*;
21 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
22 import com.intellij.openapi.editor.actionSystem.ReadonlyFragmentModificationHandler;
23 import com.intellij.openapi.fileTypes.FileType;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.popup.ComponentPopupBuilder;
26 import com.intellij.openapi.ui.popup.JBPopup;
27 import com.intellij.openapi.ui.popup.JBPopupFactory;
28 import com.intellij.openapi.util.Computable;
29 import com.intellij.openapi.util.Condition;
30 import com.intellij.openapi.util.Pair;
31 import com.intellij.openapi.util.TextRange;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.psi.*;
34 import com.intellij.psi.impl.source.resolve.FileContextUtil;
35 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
36 import com.intellij.psi.impl.source.tree.injected.Place;
37 import com.intellij.psi.util.PsiTreeUtil;
38 import com.intellij.util.IncorrectOperationException;
39 import com.intellij.util.LocalTimeCounter;
40 import com.intellij.util.containers.ContainerUtil;
41 import com.intellij.util.containers.Convertor;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Set;
49 /**
50 * "Quick Edit Language" intention action that provides a popup which shows an injected language
51 * fragment's complete prefix and suffix in non-editable areas and allows to edit the fragment
52 * without having to consider any additional escaping rules (e.g. when editing regexes in String
53 * literals).
54 * <p/>
55 * This is a bit experimental because it doesn't play very well with some quickfixes, such as the
56 * JavaScript's "Create Method/Function" one which opens another editor window. Though harmless,
57 * this is quite confusing.
58 * <p/>
59 * I wonder if such QuickFixes should try to get an Editor from the DataContext
60 * (see {@link QuickEditEditor.MyPanel#getData(java.lang.String)}) instead of using the "tactical nuke"
61 * com.intellij.openapi.fileEditor.FileEditorManager#openTextEditor(com.intellij.openapi.fileEditor.OpenFileDescriptor, boolean).
63 public class QuickEditAction implements IntentionAction {
65 private String myLastLanguageName;
67 @NotNull
68 public String getText() {
69 return "Edit "+ StringUtil.notNullize(myLastLanguageName, "Injected")+" Fragment";
72 @NotNull
73 public String getFamilyName() {
74 return "Quick Edit";
77 public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
78 return getRangePair(file, editor.getCaretModel().getOffset()) != null;
81 @Nullable
82 private Pair<PsiElement, TextRange> getRangePair(final PsiFile file, final int offset) {
83 final PsiLanguageInjectionHost host =
84 PsiTreeUtil.getParentOfType(file.findElementAt(offset), PsiLanguageInjectionHost.class, false);
85 if (host == null) return null;
86 final List<Pair<PsiElement, TextRange>> injections = InjectedLanguageUtil.getInjectedPsiFiles(host);
87 if (injections == null || injections.isEmpty()) return null;
88 final int offsetInElement = offset - host.getTextRange().getStartOffset();
89 final Pair<PsiElement, TextRange> rangePair = ContainerUtil.find(injections, new Condition<Pair<PsiElement, TextRange>>() {
90 public boolean value(final Pair<PsiElement, TextRange> pair) {
91 return pair.second.containsRange(offsetInElement, offsetInElement);
93 });
94 if (rangePair != null) {
95 myLastLanguageName = rangePair.first.getContainingFile().getLanguage().getDisplayName();
97 return rangePair;
100 public void invoke(@NotNull Project project, final Editor editor, PsiFile file) throws IncorrectOperationException {
101 final int offset = editor.getCaretModel().getOffset();
102 final Pair<PsiElement, TextRange> pair = getRangePair(file, offset);
103 assert pair != null;
104 final PsiFile injectedFile = (PsiFile)pair.first;
105 final Place shreds = InjectedLanguageUtil.getShreds(injectedFile);
107 final FileType fileType = injectedFile.getFileType();
109 final PsiFileFactory factory = PsiFileFactory.getInstance(project);
110 final String text = InjectedLanguageManager.getInstance(project).getUnescapedText(injectedFile);
111 final PsiFile file2 =
112 factory.createFileFromText("dummy." + fileType.getDefaultExtension(), fileType, text, LocalTimeCounter.currentTime(), true);
113 file2.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, SmartPointerManager.getInstance(project).createSmartPsiElementPointer(file));
114 final Document document = PsiDocumentManager.getInstance(project).getDocument(file2);
115 assert document != null;
116 EditorActionManager.getInstance().setReadonlyFragmentModificationHandler(document, new ReadonlyFragmentModificationHandler() {
117 public void handle(final ReadOnlyFragmentModificationException e) {
118 //nothing
122 final Map<PsiLanguageInjectionHost.Shred, RangeMarker> markers = ContainerUtil.assignValues(shreds.iterator(), new Convertor<PsiLanguageInjectionHost.Shred, RangeMarker>() {
123 public RangeMarker convert(final PsiLanguageInjectionHost.Shred shred) {
124 return document.createRangeMarker(shred.range.getStartOffset() + shred.prefix.length(), shred.range.getEndOffset() - shred.suffix.length());
127 boolean first = true;
128 for (PsiLanguageInjectionHost.Shred shred : shreds) {
129 final RangeMarker marker = markers.get(shred);
130 if (first) marker.setGreedyToLeft(true);
131 marker.setGreedyToRight(true);
132 first = false;
134 int curOffset = 0;
135 for (PsiLanguageInjectionHost.Shred shred : shreds) {
136 final RangeMarker marker = markers.get(shred);
137 final int start = marker.getStartOffset();
138 final int end = marker.getEndOffset();
139 if (curOffset < start) {
140 final RangeMarker rangeMarker = document.createGuardedBlock(curOffset, start);
141 if (curOffset == 0) rangeMarker.setGreedyToLeft(true);
143 curOffset = end + 1;
145 if (curOffset < text.length()) {
146 document.createGuardedBlock(curOffset, text.length()).setGreedyToRight(true);
149 final QuickEditEditor e = new QuickEditEditor(document, project, fileType, new QuickEditEditor.QuickEditSaver() {
150 public void save(final String text) {
151 final Map<PsiLanguageInjectionHost, Set<PsiLanguageInjectionHost.Shred>> map = ContainerUtil.classify(shreds.iterator(), new Convertor<PsiLanguageInjectionHost.Shred, PsiLanguageInjectionHost>() {
152 public PsiLanguageInjectionHost convert(final PsiLanguageInjectionHost.Shred o) {
153 return o.host;
156 for (PsiLanguageInjectionHost host : map.keySet()) {
157 final String hostText = host.getText();
158 TextRange insideHost = null;
159 final StringBuilder sb = new StringBuilder();
160 for (PsiLanguageInjectionHost.Shred shred : map.get(host)) {
161 final TextRange localInsideHost = shred.getRangeInsideHost();
162 final TextRange localInsideFile = new TextRange(markers.get(shred).getStartOffset(), markers.get(shred).getEndOffset());
163 if (insideHost != null) {
164 sb.append(hostText.substring(insideHost.getEndOffset(), localInsideHost.getStartOffset()));
166 sb.append(localInsideFile.substring(text));
168 insideHost = insideHost == null? localInsideHost : insideHost.union(localInsideHost);
170 assert insideHost != null;
171 ElementManipulators.getManipulator(host).handleContentChange(host, insideHost, sb.toString());
175 if (!shreds.isEmpty()) {
176 final int start = markers.get(shreds.get(0)).getStartOffset();
177 e.getEditor().getCaretModel().moveToOffset(start);
178 e.getEditor().getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
181 // Using the popup doesn't seem to be a good idea because there's no completion possible inside it: When the
182 // completion popup closes, the quickedit popup is gone as well - but I like the movable and resizable popup :(
183 final ComponentPopupBuilder builder =
184 JBPopupFactory.getInstance().createComponentPopupBuilder(e.getComponent(), e.getPreferredFocusedComponent());
185 builder.setMovable(true);
186 builder.setResizable(true);
187 builder.setRequestFocus(true);
188 builder.setTitle("<html>Edit <b>" + fileType.getName() + "</b> Fragment</html>");
189 builder.setAdText("Press Ctrl+Enter to save, Escape to cancel.");
190 builder.setCancelCallback(new Computable<Boolean>() {
191 public Boolean compute() {
192 e.setCancel(true);
193 try {
194 e.uninstall();
196 catch (Exception e1) {
198 return Boolean.TRUE;
201 builder.setModalContext(true);
202 builder.setDimensionServiceKey(project, getClass().getSimpleName()+"DimensionKey", false);
204 final JBPopup popup = builder.createPopup();
205 e.install(popup);
207 popup.showCenteredInCurrentWindow(project);
210 public boolean startInWriteAction() {
211 return false;