| /* |
| * 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.daemon.impl; |
| |
| import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder; |
| import com.intellij.concurrency.JobLauncher; |
| import com.intellij.injected.editor.DocumentWindow; |
| import com.intellij.lang.Language; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.HighlighterColors; |
| import com.intellij.openapi.editor.colors.EditorColors; |
| import com.intellij.openapi.editor.colors.TextAttributesKey; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.fileTypes.SyntaxHighlighter; |
| import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; |
| import com.intellij.openapi.progress.ProcessCanceledException; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.project.DumbAware; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; |
| import com.intellij.psi.impl.source.tree.injected.Place; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.util.Processor; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.*; |
| import java.util.*; |
| import java.util.List; |
| |
| public class InjectedGeneralHighlightingPass extends GeneralHighlightingPass implements DumbAware { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.InjectedGeneralHighlightingPass"); |
| private static final String PRESENTABLE_NAME = "Injected fragments"; |
| |
| InjectedGeneralHighlightingPass(@NotNull Project project, |
| @NotNull PsiFile file, |
| @NotNull Document document, |
| int startOffset, |
| int endOffset, |
| boolean updateAll, |
| @NotNull ProperTextRange priorityRange, |
| @Nullable Editor editor, |
| @NotNull HighlightInfoProcessor highlightInfoProcessor) { |
| super(project, file, document, startOffset, endOffset, updateAll, priorityRange, editor, highlightInfoProcessor); |
| } |
| |
| @Override |
| protected String getPresentableName() { |
| return PRESENTABLE_NAME; |
| } |
| |
| @Override |
| protected void collectInformationWithProgress(@NotNull final ProgressIndicator progress) { |
| if (!Registry.is("editor.injected.highlighting.enabled")) return; |
| |
| final Set<HighlightInfo> gotHighlights = new THashSet<HighlightInfo>(100); |
| |
| final List<PsiElement> inside = new ArrayList<PsiElement>(); |
| final List<PsiElement> outside = new ArrayList<PsiElement>(); |
| List<ProperTextRange> insideRanges = new ArrayList<ProperTextRange>(); |
| List<ProperTextRange> outsideRanges = new ArrayList<ProperTextRange>(); |
| Divider.divideInsideAndOutside(myFile, myStartOffset, myEndOffset, myPriorityRange, inside, insideRanges, outside, |
| outsideRanges, false, FILE_FILTER); |
| |
| |
| // all infos for the "injected fragment for the host which is inside" are indeed inside |
| // but some of the infos for the "injected fragment for the host which is outside" can be still inside |
| Set<HighlightInfo> injectedResult = new THashSet<HighlightInfo>(); |
| Set<PsiFile> injected = getInjectedPsiFiles(inside, outside, progress); |
| setProgressLimit(injected.size()); |
| |
| if (!addInjectedPsiHighlights(injected, progress, Collections.synchronizedSet(injectedResult))) { |
| throw new ProcessCanceledException(); |
| } |
| final List<HighlightInfo> injectionsOutside = new ArrayList<HighlightInfo>(gotHighlights.size()); |
| |
| Set<HighlightInfo> result; |
| synchronized (injectedResult) { |
| // sync here because all writes happened in another thread |
| result = injectedResult; |
| } |
| for (HighlightInfo info : result) { |
| if (myStartOffset <= info.getStartOffset() && info.getEndOffset() <= myEndOffset) { |
| gotHighlights.add(info); |
| } |
| else { |
| // nonconditionally apply injected results regardless whether they are in myStartOffset,myEndOffset |
| injectionsOutside.add(info); |
| } |
| } |
| |
| if (!injectionsOutside.isEmpty()) { |
| final ProperTextRange priorityIntersection = myPriorityRange.intersection(new TextRange(myStartOffset, myEndOffset)); |
| if ((!inside.isEmpty() || !gotHighlights.isEmpty()) && |
| priorityIntersection != null) { // do not apply when there were no elements to highlight |
| // clear infos found in visible area to avoid applying them twice |
| final List<HighlightInfo> toApplyInside = new ArrayList<HighlightInfo>(gotHighlights); |
| myHighlights.addAll(toApplyInside); |
| gotHighlights.clear(); |
| |
| myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, toApplyInside, myPriorityRange, myRestrictRange); |
| } |
| |
| List<HighlightInfo> toApply = new ArrayList<HighlightInfo>(); |
| for (HighlightInfo info : gotHighlights) { |
| if (!myRestrictRange.containsRange(info.getStartOffset(), info.getEndOffset())) continue; |
| if (!myPriorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) { |
| toApply.add(info); |
| } |
| } |
| toApply.addAll(injectionsOutside); |
| |
| myHighlightInfoProcessor.highlightsOutsideVisiblePartAreProduced(myHighlightingSession, toApply, myRestrictRange, new ProperTextRange(0, myDocument.getTextLength())); |
| } |
| else { |
| // else apply only result (by default apply command) and only within inside |
| myHighlights.addAll(gotHighlights); |
| myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, myHighlights, myRestrictRange, myRestrictRange); |
| } |
| } |
| |
| @NotNull |
| private Set<PsiFile> getInjectedPsiFiles(@NotNull final List<PsiElement> elements1, |
| @NotNull final List<PsiElement> elements2, |
| @NotNull final ProgressIndicator progress) { |
| final Set<PsiFile> outInjected = new THashSet<PsiFile>(); |
| |
| List<DocumentWindow> injected = InjectedLanguageUtil.getCachedInjectedDocuments(myFile); |
| Collection<PsiElement> hosts = new THashSet<PsiElement>(elements1.size() + elements2.size() + injected.size()); |
| |
| //rehighlight all injected PSI regardless the range, |
| //since change in one place can lead to invalidation of injected PSI in (completely) other place. |
| for (DocumentWindow documentRange : injected) { |
| progress.checkCanceled(); |
| if (!documentRange.isValid()) continue; |
| PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(documentRange); |
| if (file == null) continue; |
| PsiElement context = InjectedLanguageManager.getInstance(file.getProject()).getInjectionHost(file); |
| if (context != null |
| && context.isValid() |
| && !file.getProject().isDisposed() |
| && (myUpdateAll || new ProperTextRange(myStartOffset, myEndOffset).intersects(context.getTextRange()))) { |
| hosts.add(context); |
| } |
| } |
| hosts.addAll(elements1); |
| hosts.addAll(elements2); |
| |
| final PsiLanguageInjectionHost.InjectedPsiVisitor visitor = new PsiLanguageInjectionHost.InjectedPsiVisitor() { |
| @Override |
| public void visit(@NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { |
| synchronized (outInjected) { |
| outInjected.add(injectedPsi); |
| } |
| } |
| }; |
| if (!JobLauncher.getInstance().invokeConcurrentlyUnderProgress(new ArrayList<PsiElement>(hosts), progress, true, |
| new Processor<PsiElement>() { |
| @Override |
| public boolean process(PsiElement element) { |
| progress.checkCanceled(); |
| InjectedLanguageUtil.enumerate(element, myFile, false, visitor); |
| return true; |
| } |
| })) { |
| throw new ProcessCanceledException(); |
| } |
| synchronized (outInjected) { |
| return outInjected; |
| } |
| } |
| |
| // returns false if canceled |
| private boolean addInjectedPsiHighlights(@NotNull final Set<PsiFile> injectedFiles, |
| @NotNull final ProgressIndicator progress, |
| @NotNull final Collection<HighlightInfo> outInfos) { |
| if (injectedFiles.isEmpty()) return true; |
| final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(myProject); |
| final TextAttributes injectedAttributes = myGlobalScheme.getAttributes(EditorColors.INJECTED_LANGUAGE_FRAGMENT); |
| |
| return JobLauncher.getInstance() |
| .invokeConcurrentlyUnderProgress(new ArrayList<PsiFile>(injectedFiles), progress, isFailFastOnAcquireReadAction(), |
| new Processor<PsiFile>() { |
| @Override |
| public boolean process(final PsiFile injectedPsi) { |
| return addInjectedPsiHighlights(injectedPsi, injectedAttributes, outInfos, progress, injectedLanguageManager); |
| } |
| }); |
| } |
| |
| private boolean addInjectedPsiHighlights(@NotNull PsiFile injectedPsi, |
| TextAttributes injectedAttributes, |
| @NotNull Collection<HighlightInfo> outInfos, |
| @NotNull ProgressIndicator progress, |
| @NotNull InjectedLanguageManager injectedLanguageManager) { |
| DocumentWindow documentWindow = (DocumentWindow)PsiDocumentManager.getInstance(myProject).getCachedDocument(injectedPsi); |
| if (documentWindow == null) return true; |
| Place places = InjectedLanguageUtil.getShreds(injectedPsi); |
| for (PsiLanguageInjectionHost.Shred place : places) { |
| PsiLanguageInjectionHost host = place.getHost(); |
| if (host == null) continue; |
| TextRange textRange = place.getRangeInsideHost().shiftRight(host.getTextRange().getStartOffset()); |
| if (textRange.isEmpty()) continue; |
| String desc = injectedPsi.getLanguage().getDisplayName() + ": " + injectedPsi.getText(); |
| HighlightInfo.Builder builder = HighlightInfo.newHighlightInfo(HighlightInfoType.INJECTED_LANGUAGE_BACKGROUND).range(textRange); |
| if (injectedAttributes != null && InjectedLanguageUtil.isHighlightInjectionBackground(host)) { |
| builder.textAttributes(injectedAttributes); |
| } |
| builder.unescapedToolTip(desc); |
| HighlightInfo info = builder.createUnconditionally(); |
| info.setFromInjection(true); |
| outInfos.add(info); |
| } |
| |
| HighlightInfoHolder holder = createInfoHolder(injectedPsi); |
| runHighlightVisitorsForInjected(injectedPsi, holder, progress); |
| for (int i = 0; i < holder.size(); i++) { |
| HighlightInfo info = holder.get(i); |
| final int startOffset = documentWindow.injectedToHost(info.startOffset); |
| final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset); |
| addPatchedInfos(info, injectedPsi, documentWindow, injectedLanguageManager, fixedTextRange, outInfos); |
| } |
| int injectedStart = holder.size(); |
| highlightInjectedSyntax(injectedPsi, holder); |
| for (int i = injectedStart; i < holder.size(); i++) { |
| HighlightInfo info = holder.get(i); |
| final int startOffset = info.startOffset; |
| final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset); |
| if (fixedTextRange == null) { |
| info.setFromInjection(true); |
| outInfos.add(info); |
| } |
| else { |
| HighlightInfo patched = |
| new HighlightInfo(info.forcedTextAttributes, info.forcedTextAttributesKey, |
| info.type, fixedTextRange.getStartOffset(), |
| fixedTextRange.getEndOffset(), |
| info.getDescription(), info.getToolTip(), info.type.getSeverity(null), |
| info.isAfterEndOfLine(), null, false, 0, info.getProblemGroup(), info.getGutterIconRenderer()); |
| patched.setFromInjection(true); |
| outInfos.add(patched); |
| } |
| } |
| |
| if (!isDumbMode()) { |
| List<HighlightInfo> todos = new ArrayList<HighlightInfo>(); |
| highlightTodos(injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), progress, myPriorityRange, todos, todos); |
| for (HighlightInfo info : todos) { |
| addPatchedInfos(info, injectedPsi, documentWindow, injectedLanguageManager, null, outInfos); |
| } |
| } |
| advanceProgress(1); |
| return true; |
| } |
| |
| @Nullable("null means invalid") |
| private static TextRange getFixedTextRange(@NotNull DocumentWindow documentWindow, int startOffset) { |
| final TextRange fixedTextRange; |
| TextRange textRange = documentWindow.getHostRange(startOffset); |
| if (textRange == null) { |
| // todo[cdr] check this fix. prefix/suffix code annotation case |
| textRange = findNearestTextRange(documentWindow, startOffset); |
| if (textRange == null) return null; |
| final boolean isBefore = startOffset < textRange.getStartOffset(); |
| fixedTextRange = new ProperTextRange(isBefore ? textRange.getStartOffset() - 1 : textRange.getEndOffset(), |
| isBefore ? textRange.getStartOffset() : textRange.getEndOffset() + 1); |
| } |
| else { |
| fixedTextRange = null; |
| } |
| return fixedTextRange; |
| } |
| |
| private static void addPatchedInfos(@NotNull HighlightInfo info, |
| @NotNull PsiFile injectedPsi, |
| @NotNull DocumentWindow documentWindow, |
| @NotNull InjectedLanguageManager injectedLanguageManager, |
| @Nullable TextRange fixedTextRange, |
| @NotNull Collection<HighlightInfo> out) { |
| ProperTextRange textRange = new ProperTextRange(info.startOffset, info.endOffset); |
| List<TextRange> editables = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, textRange); |
| for (TextRange editable : editables) { |
| TextRange hostRange = fixedTextRange == null ? documentWindow.injectedToHost(editable) : fixedTextRange; |
| |
| boolean isAfterEndOfLine = info.isAfterEndOfLine(); |
| if (isAfterEndOfLine) { |
| // convert injected afterEndOfLine to either host' afterEndOfLine or not-afterEndOfLine highlight of the injected fragment boundary |
| int hostEndOffset = hostRange.getEndOffset(); |
| int lineNumber = documentWindow.getDelegate().getLineNumber(hostEndOffset); |
| int hostLineEndOffset = documentWindow.getDelegate().getLineEndOffset(lineNumber); |
| if (hostEndOffset < hostLineEndOffset) { |
| // convert to non-afterEndOfLine |
| isAfterEndOfLine = false; |
| hostRange = new ProperTextRange(hostRange.getStartOffset(), hostEndOffset+1); |
| } |
| } |
| |
| HighlightInfo patched = |
| new HighlightInfo(info.forcedTextAttributes, info.forcedTextAttributesKey, info.type, |
| hostRange.getStartOffset(), hostRange.getEndOffset(), |
| info.getDescription(), info.getToolTip(), info.type.getSeverity(null), isAfterEndOfLine, null, false, 0, info.getProblemGroup(), info.getGutterIconRenderer()); |
| patched.setHint(info.hasHint()); |
| |
| if (info.quickFixActionRanges != null) { |
| for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair : info.quickFixActionRanges) { |
| TextRange quickfixTextRange = pair.getSecond(); |
| List<TextRange> editableQF = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, quickfixTextRange); |
| for (TextRange editableRange : editableQF) { |
| HighlightInfo.IntentionActionDescriptor descriptor = pair.getFirst(); |
| if (patched.quickFixActionRanges == null) patched.quickFixActionRanges = new ArrayList<Pair<HighlightInfo.IntentionActionDescriptor, TextRange>>(); |
| TextRange hostEditableRange = documentWindow.injectedToHost(editableRange); |
| patched.quickFixActionRanges.add(Pair.create(descriptor, hostEditableRange)); |
| } |
| } |
| } |
| patched.setFromInjection(true); |
| out.add(patched); |
| } |
| } |
| |
| // finds the first nearest text range |
| @Nullable("null means invalid") |
| private static TextRange findNearestTextRange(final DocumentWindow documentWindow, final int startOffset) { |
| TextRange textRange = null; |
| for (Segment marker : documentWindow.getHostRanges()) { |
| TextRange curRange = ProperTextRange.create(marker); |
| if (curRange.getStartOffset() > startOffset && textRange != null) break; |
| textRange = curRange; |
| } |
| return textRange; |
| } |
| |
| private void runHighlightVisitorsForInjected(@NotNull PsiFile injectedPsi, |
| @NotNull final HighlightInfoHolder holder, |
| @NotNull final ProgressIndicator progress) { |
| HighlightVisitor[] filtered = getHighlightVisitors(injectedPsi); |
| try { |
| final List<PsiElement> elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength()); |
| for (final HighlightVisitor visitor : filtered) { |
| visitor.analyze(injectedPsi, true, holder, new Runnable() { |
| @Override |
| public void run() { |
| for (PsiElement element : elements) { |
| progress.checkCanceled(); |
| visitor.visit(element); |
| } |
| } |
| }); |
| } |
| } |
| finally { |
| incVisitorUsageCount(-1); |
| } |
| } |
| |
| private void highlightInjectedSyntax(@NotNull PsiFile injectedPsi, @NotNull HighlightInfoHolder holder) { |
| List<Trinity<IElementType, SmartPsiElementPointer<PsiLanguageInjectionHost>, TextRange>> tokens = InjectedLanguageUtil |
| .getHighlightTokens(injectedPsi); |
| if (tokens == null) return; |
| |
| final Language injectedLanguage = injectedPsi.getLanguage(); |
| Project project = injectedPsi.getProject(); |
| SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(injectedLanguage, project, injectedPsi.getVirtualFile()); |
| final TextAttributes defaultAttrs = myGlobalScheme.getAttributes(HighlighterColors.TEXT); |
| |
| for (Trinity<IElementType, SmartPsiElementPointer<PsiLanguageInjectionHost>, TextRange> token : tokens) { |
| ProgressManager.checkCanceled(); |
| IElementType tokenType = token.getFirst(); |
| PsiLanguageInjectionHost injectionHost = token.getSecond().getElement(); |
| if (injectionHost == null) continue; |
| TextRange textRange = token.getThird(); |
| TextAttributesKey[] keys = syntaxHighlighter.getTokenHighlights(tokenType); |
| if (textRange.getLength() == 0) continue; |
| |
| TextRange annRange = textRange.shiftRight(injectionHost.getTextRange().getStartOffset()); |
| // force attribute colors to override host' ones |
| TextAttributes attributes = null; |
| for(TextAttributesKey key:keys) { |
| TextAttributes attrs2 = myGlobalScheme.getAttributes(key); |
| if (attrs2 != null) { |
| attributes = attributes == null ? attrs2 : TextAttributes.merge(attributes, attrs2); |
| } |
| } |
| TextAttributes forcedAttributes; |
| if (attributes == null || attributes.isEmpty() || attributes.equals(defaultAttrs)) { |
| forcedAttributes = TextAttributes.ERASE_MARKER; |
| } |
| else { |
| HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT).range(annRange).textAttributes( |
| TextAttributes.ERASE_MARKER).createUnconditionally(); |
| holder.add(info); |
| |
| Color back = attributes.getBackgroundColor() == null ? myGlobalScheme.getDefaultBackground() : attributes.getBackgroundColor(); |
| Color fore = attributes.getForegroundColor() == null ? myGlobalScheme.getDefaultForeground() : attributes.getForegroundColor(); |
| forcedAttributes = new TextAttributes(fore, back, attributes.getEffectColor(), attributes.getEffectType(), attributes.getFontType()); |
| } |
| |
| HighlightInfo info = |
| HighlightInfo.newHighlightInfo(HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT).range(annRange).textAttributes(forcedAttributes) |
| .createUnconditionally(); |
| holder.add(info); |
| } |
| } |
| |
| @Override |
| protected void applyInformationWithProgress() { |
| } |
| } |