2 * Copyright 2000-2009 JetBrains s.r.o.
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.
17 package com
.intellij
.psi
.impl
.source
.tree
.injected
;
19 import com
.intellij
.injected
.editor
.DocumentWindow
;
20 import com
.intellij
.injected
.editor
.DocumentWindowImpl
;
21 import com
.intellij
.injected
.editor
.VirtualFileWindow
;
22 import com
.intellij
.injected
.editor
.VirtualFileWindowImpl
;
23 import com
.intellij
.lang
.ASTNode
;
24 import com
.intellij
.lang
.Language
;
25 import com
.intellij
.lang
.LanguageParserDefinitions
;
26 import com
.intellij
.lang
.ParserDefinition
;
27 import com
.intellij
.lang
.injection
.MultiHostRegistrar
;
28 import com
.intellij
.lexer
.Lexer
;
29 import com
.intellij
.openapi
.editor
.RangeMarker
;
30 import com
.intellij
.openapi
.editor
.ex
.DocumentEx
;
31 import com
.intellij
.openapi
.editor
.impl
.DocumentImpl
;
32 import com
.intellij
.openapi
.fileEditor
.impl
.FileDocumentManagerImpl
;
33 import com
.intellij
.openapi
.fileTypes
.SyntaxHighlighter
;
34 import com
.intellij
.openapi
.fileTypes
.SyntaxHighlighterFactory
;
35 import com
.intellij
.openapi
.progress
.ProcessCanceledException
;
36 import com
.intellij
.openapi
.project
.Project
;
37 import com
.intellij
.openapi
.util
.*;
38 import com
.intellij
.openapi
.util
.text
.StringUtil
;
39 import com
.intellij
.openapi
.vfs
.VirtualFile
;
40 import com
.intellij
.psi
.*;
41 import com
.intellij
.psi
.impl
.PsiDocumentManagerImpl
;
42 import com
.intellij
.psi
.impl
.source
.PsiFileImpl
;
43 import com
.intellij
.psi
.impl
.source
.resolve
.FileContextUtil
;
44 import com
.intellij
.psi
.impl
.source
.text
.BlockSupportImpl
;
45 import com
.intellij
.psi
.impl
.source
.tree
.*;
46 import com
.intellij
.psi
.tree
.IElementType
;
47 import com
.intellij
.psi
.util
.PsiTreeUtil
;
48 import com
.intellij
.psi
.util
.PsiUtilBase
;
49 import com
.intellij
.util
.ArrayUtil
;
50 import com
.intellij
.util
.SmartList
;
51 import org
.jetbrains
.annotations
.NonNls
;
52 import org
.jetbrains
.annotations
.NotNull
;
53 import org
.jetbrains
.annotations
.Nullable
;
55 import java
.util
.ArrayList
;
56 import java
.util
.List
;
62 public class MultiHostRegistrarImpl
implements MultiHostRegistrar
{
64 private Language myLanguage
;
65 private List
<LiteralTextEscaper
<?
extends PsiLanguageInjectionHost
>> escapers
;
66 private List
<PsiLanguageInjectionHost
.Shred
> shreds
;
67 private StringBuilder outChars
;
68 private boolean isOneLineEditor
;
69 private boolean cleared
;
70 private final Project myProject
;
71 private final PsiManager myPsiManager
;
72 private DocumentEx myHostDocument
;
73 private VirtualFile myHostVirtualFile
;
74 private final PsiElement myContextElement
;
75 private final PsiFile myHostPsiFile
;
77 MultiHostRegistrarImpl(@NotNull Project project
, @NotNull PsiFile hostPsiFile
, @NotNull PsiElement contextElement
) {
79 myContextElement
= contextElement
;
80 myHostPsiFile
= PsiUtilBase
.getTemplateLanguageFile(hostPsiFile
);
81 myPsiManager
= myHostPsiFile
.getManager();
86 public MultiHostRegistrar
startInjecting(@NotNull Language language
) {
87 escapers
= new SmartList
<LiteralTextEscaper
<?
extends PsiLanguageInjectionHost
>>();
88 shreds
= new SmartList
<PsiLanguageInjectionHost
.Shred
>();
89 outChars
= new StringBuilder();
93 throw new IllegalStateException("Seems you haven't called doneInjecting()");
96 if (LanguageParserDefinitions
.INSTANCE
.forLanguage(language
) == null) {
97 throw new UnsupportedOperationException("Cannot inject language '" + language
+ "' since its getParserDefinition() returns null");
99 myLanguage
= language
;
101 FileViewProvider viewProvider
= myHostPsiFile
.getViewProvider();
102 myHostVirtualFile
= viewProvider
.getVirtualFile();
103 myHostDocument
= (DocumentEx
)viewProvider
.getDocument();
104 assert myHostDocument
!= null : myHostPsiFile
+ "; " + viewProvider
;
108 private void clear() {
111 outChars
.setLength(0);
112 isOneLineEditor
= false;
119 public MultiHostRegistrar
addPlace(@NonNls @Nullable String prefix
,
120 @NonNls @Nullable String suffix
,
121 @NotNull PsiLanguageInjectionHost host
,
122 @NotNull TextRange rangeInsideHost
) {
123 ProperTextRange
.assertProperRange(rangeInsideHost
);
125 PsiFile containingFile
= PsiUtilBase
.getTemplateLanguageFile(host
);
126 assert containingFile
== myHostPsiFile
: exceptionContext("Trying to inject into foreign file: "+containingFile
);
127 TextRange hostTextRange
= host
.getTextRange();
128 if (!hostTextRange
.contains(rangeInsideHost
.shiftRight(hostTextRange
.getStartOffset()))) {
130 throw new IllegalArgumentException("rangeInsideHost must lie within host text range. rangeInsideHost:"+rangeInsideHost
+"; host textRange:"+
133 if (myLanguage
== null) {
135 throw new IllegalStateException("Seems you haven't called startInjecting()");
138 if (prefix
== null) prefix
= "";
139 if (suffix
== null) suffix
= "";
141 int startOffset
= outChars
.length();
142 outChars
.append(prefix
);
143 LiteralTextEscaper
<?
extends PsiLanguageInjectionHost
> textEscaper
= host
.createLiteralTextEscaper();
144 escapers
.add(textEscaper
);
145 isOneLineEditor
|= textEscaper
.isOneLine();
146 TextRange relevantRange
= textEscaper
.getRelevantTextRange().intersection(rangeInsideHost
);
147 if (relevantRange
== null) {
148 relevantRange
= TextRange
.from(textEscaper
.getRelevantTextRange().getStartOffset(), 0);
151 boolean result
= textEscaper
.decode(relevantRange
, outChars
);
153 // if there are invalid chars, adjust the range
154 int offsetInHost
= textEscaper
.getOffsetInHost(outChars
.length() - startOffset
, rangeInsideHost
);
155 relevantRange
= relevantRange
.intersection(new ProperTextRange(0, offsetInHost
));
158 outChars
.append(suffix
);
159 int endOffset
= outChars
.length();
160 TextRange relevantRangeInHost
= relevantRange
.shiftRight(hostTextRange
.getStartOffset());
161 RangeMarker relevantMarker
= myHostDocument
.createRangeMarker(relevantRangeInHost
);
162 relevantMarker
.setGreedyToLeft(true);
163 relevantMarker
.setGreedyToRight(true);
164 shreds
.add(new PsiLanguageInjectionHost
.Shred(host
, relevantMarker
, prefix
, suffix
, new ProperTextRange(startOffset
, endOffset
)));
168 public void doneInjecting() {
170 if (shreds
.isEmpty()) {
171 throw new IllegalStateException("Seems you haven't called addPlace()");
173 PsiDocumentManager documentManager
= PsiDocumentManager
.getInstance(myProject
);
174 assert ArrayUtil
.indexOf(documentManager
.getUncommittedDocuments(), myHostDocument
) == -1 : "document is uncommitted: "+myHostDocument
;
175 assert myHostPsiFile
.getText().equals(myHostDocument
.getText()) : "host text mismatch";
177 Place place
= new Place(shreds
, null);
178 DocumentWindowImpl documentWindow
= new DocumentWindowImpl(myHostDocument
, isOneLineEditor
, place
);
179 VirtualFileWindowImpl virtualFile
= new VirtualFileWindowImpl(myHostVirtualFile
, documentWindow
, myLanguage
, outChars
);
180 myLanguage
= LanguageSubstitutors
.INSTANCE
.substituteLanguage(myLanguage
, virtualFile
, myProject
);
181 virtualFile
.setLanguage(myLanguage
);
183 DocumentImpl decodedDocument
;
184 if (StringUtil
.indexOf(outChars
, '\r') == -1) {
185 decodedDocument
= new DocumentImpl(outChars
);
188 decodedDocument
= new DocumentImpl(true);
189 decodedDocument
.setAcceptSlashR(true);
190 decodedDocument
.replaceString(0,0,outChars
);
192 FileDocumentManagerImpl
.registerDocument(decodedDocument
, virtualFile
);
194 InjectedFileViewProvider viewProvider
= new InjectedFileViewProvider(myPsiManager
, virtualFile
, place
, documentWindow
, myLanguage
);
195 ParserDefinition parserDefinition
= LanguageParserDefinitions
.INSTANCE
.forLanguage(myLanguage
);
196 assert parserDefinition
!= null : "Parser definition for language "+myLanguage
+" is null";
197 PsiFile psiFile
= parserDefinition
.createFile(viewProvider
);
199 SmartPsiElementPointer
<PsiLanguageInjectionHost
> pointer
= createHostSmartPointer(shreds
.get(0).host
);
201 synchronized (PsiLock
.LOCK
) {
202 final ASTNode parsedNode
= keepTreeFromChameleoningBack(psiFile
);
204 assert parsedNode
instanceof FileElement
: "Parsed to "+parsedNode
+" instead of FileElement";
206 String documentText
= documentWindow
.getText();
207 assert outChars
.toString().equals(parsedNode
.getText()) : exceptionContext("Before patch: doc:\n" + documentText
+ "\n---PSI:\n" + parsedNode
.getText() + "\n---chars:\n"+outChars
);
209 patchLeafs(parsedNode
, escapers
, place
);
211 catch (ProcessCanceledException e
) {
214 catch (RuntimeException e
) {
215 throw new RuntimeException(exceptionContext("Patch error"), e
);
217 assert parsedNode
.getText().equals(documentText
) : exceptionContext("After patch: doc:\n" + documentText
+ "\n---PSI:\n" + parsedNode
.getText() + "\n---chars:\n"+outChars
);
219 virtualFile
.setContent(null, documentWindow
.getText(), false);
221 cacheEverything(place
, documentWindow
, viewProvider
, psiFile
, pointer
);
223 PsiFile cachedPsiFile
= documentManager
.getCachedPsiFile(documentWindow
);
224 assert cachedPsiFile
== psiFile
: "Cached psi :"+ cachedPsiFile
+" instead of "+psiFile
;
226 assert place
.isValid();
227 assert viewProvider
.isValid();
228 PsiFile newFile
= registerDocument(documentWindow
, psiFile
, place
, myHostPsiFile
, documentManager
);
229 boolean mergeHappened
= newFile
!= psiFile
;
231 InjectedLanguageUtil
.clearCaches(psiFile
);
233 viewProvider
= (InjectedFileViewProvider
)psiFile
.getViewProvider();
234 documentWindow
= (DocumentWindowImpl
)viewProvider
.getDocument();
235 virtualFile
= (VirtualFileWindowImpl
)viewProvider
.getVirtualFile();
236 cacheEverything(place
, documentWindow
, viewProvider
, psiFile
, pointer
);
239 assert psiFile
.isValid();
240 assert place
.isValid();
241 assert viewProvider
.isValid();
244 List
<Trinity
<IElementType
, PsiLanguageInjectionHost
, TextRange
>> tokens
= obtainHighlightTokensFromLexer(myLanguage
, outChars
, escapers
, place
, virtualFile
, myProject
);
245 psiFile
.putUserData(InjectedLanguageUtil
.HIGHLIGHT_TOKENS
, tokens
);
247 catch (ProcessCanceledException e
) {
250 catch (RuntimeException e
) {
251 throw new RuntimeException(exceptionContext("Obtaining tokens error"), e
);
256 assertEverythingIsAllright(documentManager
, documentWindow
, psiFile
);
264 private static void cacheEverything(Place place
,
265 DocumentWindowImpl documentWindow
,
266 InjectedFileViewProvider viewProvider
,
268 SmartPsiElementPointer
<PsiLanguageInjectionHost
> pointer
) {
269 FileDocumentManagerImpl
.registerDocument(documentWindow
, viewProvider
.getVirtualFile());
271 viewProvider
.forceCachedPsi(psiFile
);
273 psiFile
.putUserData(FileContextUtil
.INJECTED_IN_ELEMENT
, pointer
);
274 PsiDocumentManagerImpl
.cachePsi(documentWindow
, psiFile
);
276 keepTreeFromChameleoningBack(psiFile
);
277 place
.setInjectedPsi(psiFile
);
279 viewProvider
.setShreds(place
);
284 private String
exceptionContext(@NonNls String msg
) {
286 "Language: " +myLanguage
+";\n "+
287 "Host file: "+myHostPsiFile
+" in '" + myHostVirtualFile
.getPresentableUrl() + "'\n" +
288 "Context element "+myContextElement
.getTextRange() + ": '" + myContextElement
+"'; "+
292 private static final Key
<ASTNode
> TREE_HARD_REF
= Key
.create("TREE_HARD_REF");
293 private static ASTNode
keepTreeFromChameleoningBack(PsiFile psiFile
) {
294 psiFile
.getFirstChild();
295 // need to keep tree reacheable to avoid being garbage-collected (via WeakReference in PsiFileImpl)
296 // and then being reparsed from wrong (escaped) document content
297 ASTNode node
= psiFile
.getNode();
298 assert !TreeUtil
.isCollapsedChameleon(node
) : "Chameleon "+node
+" is collapsed";
299 psiFile
.putUserData(TREE_HARD_REF
, node
);
303 private void assertEverythingIsAllright(PsiDocumentManager documentManager
, DocumentWindowImpl documentWindow
, PsiFile psiFile
) {
304 boolean isAncestor
= false;
305 for (PsiLanguageInjectionHost
.Shred shred
: shreds
) {
306 PsiLanguageInjectionHost host
= shred
.host
;
307 isAncestor
|= PsiTreeUtil
.isAncestor(myContextElement
, host
, false);
309 assert isAncestor
: exceptionContext(myContextElement
+ " must be the parent of at least one of injection hosts");
311 InjectedFileViewProvider injectedFileViewProvider
= (InjectedFileViewProvider
)psiFile
.getViewProvider();
312 assert injectedFileViewProvider
.isValid() : "Invalid view provider: "+injectedFileViewProvider
;
313 assert documentWindow
.getText().equals(psiFile
.getText()) : "Document window text mismatch";
314 assert injectedFileViewProvider
.getDocument() == documentWindow
: "Provider document mismatch";
315 assert documentManager
.getCachedDocument(psiFile
) == documentWindow
: "Cached document mismatch";
316 assert psiFile
.getVirtualFile() == injectedFileViewProvider
.getVirtualFile() : "Virtual file mismatch";
317 PsiDocumentManagerImpl
.checkConsistency(psiFile
, documentWindow
);
320 private void addToResults(Place place
) {
321 if (result
== null) {
322 result
= new Places();
327 private static <T
extends PsiLanguageInjectionHost
> SmartPsiElementPointer
<T
> createHostSmartPointer(final T host
) {
328 return host
.isPhysical()
329 ? SmartPointerManager
.getInstance(host
.getProject()).createSmartPsiElementPointer(host
)
330 : new IdentitySmartPointer
<T
>(host
);
333 private static void patchLeafs(ASTNode parsedNode
, List
<LiteralTextEscaper
<?
extends PsiLanguageInjectionHost
>> escapers
, Place shreds
) {
334 LeafPatcher patcher
= new LeafPatcher(shreds
, escapers
);
335 ((TreeElement
)parsedNode
).acceptTree(patcher
);
337 String nodeText
= parsedNode
.getText();
338 assert nodeText
.equals(patcher
.catLeafs
.toString()) : "Malformed PSI structure: leaf texts do not add up to the whole file text." +
339 "\nFile text (from tree) :'"+nodeText
+"'" +
340 "\nFile text (from PSI) :'"+parsedNode
.getPsi().getText()+"'" +
341 "\nLeaf texts concatenated:'"+ patcher
.catLeafs
+"';" +
342 "\nFile root: "+parsedNode
+
343 "\nLanguage: "+parsedNode
.getPsi().getLanguage()+
344 "\nHost file: "+shreds
.get(0).host
.getContainingFile().getVirtualFile()
346 for (Map
.Entry
<LeafElement
, String
> entry
: patcher
.newTexts
.entrySet()) {
347 LeafElement leaf
= entry
.getKey();
348 String newText
= entry
.getValue();
349 leaf
.rawReplaceWithText(newText
);
351 ((TreeElement
)parsedNode
).acceptTree(new RecursiveTreeElementWalkingVisitor(){
352 protected void visitNode(TreeElement element
) {
353 element
.clearCaches();
354 super.visitNode(element
);
359 private static PsiFile
registerDocument(final DocumentWindowImpl documentWindow
,
360 final PsiFile injectedPsi
,
362 final PsiFile hostPsiFile
,
363 final PsiDocumentManager documentManager
) {
364 DocumentEx hostDocument
= documentWindow
.getDelegate();
365 List
<DocumentWindow
> injected
= InjectedLanguageUtil
.getCachedInjectedDocuments(hostPsiFile
);
367 for (int i
= injected
.size()-1; i
>=0; i
--) {
368 DocumentWindowImpl oldDocument
= (DocumentWindowImpl
)injected
.get(i
);
369 final PsiFileImpl oldFile
= (PsiFileImpl
)documentManager
.getCachedPsiFile(oldDocument
);
370 FileViewProvider viewProvider
;
372 if (oldFile
== null ||
373 !oldFile
.isValid() ||
374 !((viewProvider
= oldFile
.getViewProvider()) instanceof InjectedFileViewProvider
) ||
375 ((InjectedFileViewProvider
)viewProvider
).isDisposed()
378 Disposer
.dispose(oldDocument
);
381 InjectedFileViewProvider oldViewProvider
= (InjectedFileViewProvider
)viewProvider
;
383 final ASTNode injectedNode
= injectedPsi
.getNode();
384 final ASTNode oldFileNode
= oldFile
.getNode();
385 assert injectedNode
!= null : "New node is null";
386 assert oldFileNode
!= null : "Old node is null";
387 if (oldDocument
.areRangesEqual(documentWindow
)) {
388 if (oldFile
.getFileType() != injectedPsi
.getFileType() || oldFile
.getLanguage() != injectedPsi
.getLanguage()) {
390 Disposer
.dispose(oldDocument
);
393 oldFile
.putUserData(FileContextUtil
.INJECTED_IN_ELEMENT
, injectedPsi
.getUserData(FileContextUtil
.INJECTED_IN_ELEMENT
));
395 assert shreds
.isValid();
396 oldViewProvider
.performNonPhysically(new Runnable() {
398 BlockSupportImpl
.mergeTrees(oldFile
, oldFileNode
, injectedNode
);
401 assert shreds
.isValid();
406 injected
.add(documentWindow
);
408 cacheInjectedRegion(documentWindow
, hostDocument
);
412 private static void cacheInjectedRegion(DocumentWindowImpl documentWindow
, DocumentEx hostDocument
) {
413 List
<RangeMarker
> injectedRegions
= InjectedLanguageUtil
.getCachedInjectedRegions(hostDocument
);
414 RangeMarker newMarker
= documentWindow
.getHostRanges()[0];
415 TextRange newRange
= InjectedLanguageUtil
.toTextRange(newMarker
);
416 for (int i
= 0; i
< injectedRegions
.size(); i
++) {
417 RangeMarker stored
= injectedRegions
.get(i
);
418 TextRange storedRange
= InjectedLanguageUtil
.toTextRange(stored
);
419 if (storedRange
.intersects(newRange
)) {
420 injectedRegions
.set(i
, newMarker
);
423 if (storedRange
.getStartOffset() > newRange
.getEndOffset()) {
424 injectedRegions
.add(i
, newMarker
);
428 if (injectedRegions
.isEmpty() || newRange
.getStartOffset() > injectedRegions
.get(injectedRegions
.size()-1).getEndOffset()) {
429 injectedRegions
.add(newMarker
);
433 // returns lexer elemet types with corresponsing ranges in encoded (injection host based) PSI
434 private static List
<Trinity
<IElementType
, PsiLanguageInjectionHost
, TextRange
>> obtainHighlightTokensFromLexer(Language language
,
435 StringBuilder outChars
,
436 List
<LiteralTextEscaper
<?
extends PsiLanguageInjectionHost
>> escapers
,
438 VirtualFileWindow virtualFile
,
440 List
<Trinity
<IElementType
, PsiLanguageInjectionHost
, TextRange
>> tokens
= new ArrayList
<Trinity
<IElementType
, PsiLanguageInjectionHost
, TextRange
>>(10);
441 SyntaxHighlighter syntaxHighlighter
= SyntaxHighlighterFactory
.getSyntaxHighlighter(language
, project
, (VirtualFile
)virtualFile
);
442 Lexer lexer
= syntaxHighlighter
.getHighlightingLexer();
443 lexer
.start(outChars
);
445 int prevHostEndOffset
= 0;
446 PsiLanguageInjectionHost host
= null;
447 LiteralTextEscaper
<?
extends PsiLanguageInjectionHost
> escaper
= null;
448 int prefixLength
= 0;
449 int suffixLength
= 0;
450 TextRange rangeInsideHost
= null;
451 int shredEndOffset
= -1;
452 for (IElementType tokenType
= lexer
.getTokenType(); tokenType
!= null; lexer
.advance(), tokenType
= lexer
.getTokenType()) {
453 TextRange range
= new ProperTextRange(lexer
.getTokenStart(), lexer
.getTokenEnd());
454 while (range
!= null && !range
.isEmpty()) {
455 if (range
.getStartOffset() >= shredEndOffset
) {
457 shredEndOffset
= shreds
.get(hostNum
).range
.getEndOffset();
458 prevHostEndOffset
= range
.getStartOffset();
459 host
= shreds
.get(hostNum
).host
;
460 escaper
= escapers
.get(hostNum
);
461 rangeInsideHost
= shreds
.get(hostNum
).getRangeInsideHost();
462 prefixLength
= shreds
.get(hostNum
).prefix
.length();
463 suffixLength
= shreds
.get(hostNum
).suffix
.length();
465 //in prefix/suffix or spills over to next fragment
466 if (range
.getStartOffset() < prevHostEndOffset
+ prefixLength
) {
467 range
= new TextRange(prevHostEndOffset
+ prefixLength
, range
.getEndOffset());
469 TextRange spilled
= null;
470 if (range
.getEndOffset() >= shredEndOffset
- suffixLength
) {
471 spilled
= new TextRange(shredEndOffset
, range
.getEndOffset());
472 range
= new TextRange(range
.getStartOffset(), shredEndOffset
);
474 if (!range
.isEmpty()) {
475 int start
= escaper
.getOffsetInHost(range
.getStartOffset() - prevHostEndOffset
- prefixLength
, rangeInsideHost
);
476 if (start
== -1) start
= rangeInsideHost
.getStartOffset();
477 int end
= escaper
.getOffsetInHost(range
.getEndOffset() - prevHostEndOffset
- prefixLength
, rangeInsideHost
);
479 end
= rangeInsideHost
.getEndOffset();
480 prevHostEndOffset
= shredEndOffset
;
482 TextRange rangeInHost
= new ProperTextRange(start
, end
);
483 tokens
.add(Trinity
.create(tokenType
, host
, rangeInHost
));
491 void addToResults(Places places
) {
492 for (Place place
: places
) {