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
;
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
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.
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
;
68 public String
getText() {
69 return "Edit "+ StringUtil
.notNullize(myLastLanguageName
, "Injected")+" Fragment";
73 public String
getFamilyName() {
77 public boolean isAvailable(@NotNull Project project
, Editor editor
, PsiFile file
) {
78 return getRangePair(file
, editor
.getCaretModel().getOffset()) != null;
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
);
94 if (rangePair
!= null) {
95 myLastLanguageName
= rangePair
.first
.getContainingFile().getLanguage().getDisplayName();
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
);
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
) {
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);
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);
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
) {
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() {
196 catch (Exception e1
) {
201 builder
.setModalContext(true);
202 builder
.setDimensionServiceKey(project
, getClass().getSimpleName()+"DimensionKey", false);
204 final JBPopup popup
= builder
.createPopup();
207 popup
.showCenteredInCurrentWindow(project
);
210 public boolean startInWriteAction() {