| /* |
| * Copyright 2000-2014 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.completion; |
| |
| import com.intellij.codeInsight.CodeInsightSettings; |
| import com.intellij.codeInsight.CodeInsightUtilBase; |
| import com.intellij.codeInsight.completion.impl.CompletionServiceImpl; |
| import com.intellij.codeInsight.editorActions.CompletionAutoPopupHandler; |
| import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor; |
| import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessors; |
| import com.intellij.codeInsight.lookup.*; |
| import com.intellij.codeInsight.lookup.impl.LookupImpl; |
| import com.intellij.featureStatistics.FeatureUsageTracker; |
| import com.intellij.ide.DataManager; |
| import com.intellij.injected.editor.DocumentWindow; |
| import com.intellij.lang.Language; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.openapi.actionSystem.DataContext; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.actionSystem.EditorActionManager; |
| import com.intellij.openapi.editor.ex.DocumentEx; |
| import com.intellij.openapi.editor.ex.util.EditorUtil; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.project.DumbService; |
| import com.intellij.openapi.project.IndexNotReadyException; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.Trinity; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.impl.PsiFileEx; |
| import com.intellij.psi.impl.PsiModificationTrackerImpl; |
| import com.intellij.psi.impl.source.PostprocessReformattingAspect; |
| import com.intellij.psi.impl.source.PsiFileImpl; |
| import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; |
| import com.intellij.psi.util.PsiUtilBase; |
| import com.intellij.psi.util.PsiUtilCore; |
| import com.intellij.reference.SoftReference; |
| import com.intellij.util.DocumentUtil; |
| import com.intellij.util.ThreeState; |
| import com.intellij.util.concurrency.Semaphore; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class CodeCompletionHandlerBase { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.CodeCompletionHandlerBase"); |
| private static final Key<Boolean> CARET_PROCESSED = Key.create("CodeCompletionHandlerBase.caretProcessed"); |
| |
| @NotNull private final CompletionType myCompletionType; |
| final boolean invokedExplicitly; |
| final boolean synchronous; |
| final boolean autopopup; |
| |
| public CodeCompletionHandlerBase(@NotNull CompletionType completionType) { |
| this(completionType, true, false, true); |
| } |
| |
| public CodeCompletionHandlerBase(@NotNull CompletionType completionType, boolean invokedExplicitly, boolean autopopup, boolean synchronous) { |
| myCompletionType = completionType; |
| this.invokedExplicitly = invokedExplicitly; |
| this.autopopup = autopopup; |
| this.synchronous = synchronous; |
| |
| if (invokedExplicitly) { |
| assert synchronous; |
| } |
| if (autopopup) { |
| assert !invokedExplicitly; |
| } |
| } |
| |
| public final void invokeCompletion(final Project project, final Editor editor) { |
| try { |
| invokeCompletion(project, editor, 1); |
| } |
| catch (IndexNotReadyException e) { |
| DumbService.getInstance(project).showDumbModeNotification("Code completion is not available here while indices are being built"); |
| } |
| } |
| |
| public final void invokeCompletion(@NotNull final Project project, @NotNull final Editor editor, int time) { |
| invokeCompletion(project, editor, time, false, false); |
| } |
| |
| public final void invokeCompletion(@NotNull final Project project, @NotNull final Editor editor, int time, boolean hasModifiers, boolean restarted) { |
| clearCaretMarkers(editor); |
| invokeCompletion(project, editor, time, hasModifiers, restarted, editor.getCaretModel().getPrimaryCaret()); |
| } |
| |
| public final void invokeCompletion(@NotNull final Project project, @NotNull final Editor editor, int time, boolean hasModifiers, boolean restarted, @NotNull final Caret caret) { |
| markCaretAsProcessed(caret); |
| |
| if (invokedExplicitly) { |
| CompletionLookupArranger.applyLastCompletionStatisticsUpdate(); |
| } |
| |
| checkNoWriteAccess(); |
| |
| CompletionAssertions.checkEditorValid(editor); |
| |
| int offset = editor.getCaretModel().getOffset(); |
| if (editor.isViewer() || editor.getDocument().getRangeGuard(offset, offset) != null) { |
| editor.getDocument().fireReadOnlyModificationAttempt(); |
| CodeInsightUtilBase.showReadOnlyViewWarning(editor); |
| return; |
| } |
| |
| if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), project)) { |
| return; |
| } |
| |
| CompletionPhase phase = CompletionServiceImpl.getCompletionPhase(); |
| boolean repeated = phase.indicator != null && phase.indicator.isRepeatedInvocation(myCompletionType, editor); |
| /* |
| if (repeated && isAutocompleteCommonPrefixOnInvocation() && phase.fillInCommonPrefix()) { |
| return; |
| } |
| */ |
| |
| final int newTime = phase.newCompletionStarted(time, repeated); |
| if (invokedExplicitly) { |
| time = newTime; |
| } |
| final int invocationCount = time; |
| if (CompletionServiceImpl.isPhase(CompletionPhase.InsertedSingleItem.class)) { |
| CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion); |
| } |
| CompletionServiceImpl.assertPhase(CompletionPhase.NoCompletion.getClass(), CompletionPhase.CommittingDocuments.class); |
| |
| if (invocationCount > 1 && myCompletionType == CompletionType.BASIC) { |
| FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.SECOND_BASIC_COMPLETION); |
| } |
| |
| final CompletionInitializationContext[] initializationContext = {null}; |
| |
| |
| Runnable initCmd = new Runnable() { |
| @Override |
| public void run() { |
| Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| EditorUtil.fillVirtualSpaceUntilCaret(editor); |
| PsiDocumentManager.getInstance(project).commitAllDocuments(); |
| CompletionAssertions.checkEditorValid(editor); |
| |
| final PsiFile psiFile = PsiUtilBase.getPsiFileInEditor(caret, project); |
| assert psiFile != null : "no PSI file: " + FileDocumentManager.getInstance().getFile(editor.getDocument()); |
| psiFile.putUserData(PsiFileEx.BATCH_REFERENCE_PROCESSING, Boolean.TRUE); |
| CompletionAssertions.assertCommitSuccessful(editor, psiFile); |
| |
| initializationContext[0] = runContributorsBeforeCompletion(editor, psiFile, invocationCount, caret); |
| } |
| }; |
| ApplicationManager.getApplication().runWriteAction(runnable); |
| } |
| }; |
| if (autopopup) { |
| CommandProcessor.getInstance().runUndoTransparentAction(initCmd); |
| if (!restarted && shouldSkipAutoPopup(editor, initializationContext[0].getFile())) { |
| CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion); |
| return; |
| } |
| } else { |
| CommandProcessor.getInstance().executeCommand(project, initCmd, null, null); |
| } |
| |
| insertDummyIdentifier(initializationContext[0], hasModifiers, invocationCount); |
| } |
| |
| private CompletionInitializationContext runContributorsBeforeCompletion(Editor editor, PsiFile psiFile, int invocationCount, Caret caret) { |
| final Ref<CompletionContributor> current = Ref.create(null); |
| CompletionInitializationContext context = new CompletionInitializationContext(editor, caret, psiFile, myCompletionType, invocationCount) { |
| CompletionContributor dummyIdentifierChanger; |
| |
| @Override |
| public void setDummyIdentifier(@NotNull String dummyIdentifier) { |
| super.setDummyIdentifier(dummyIdentifier); |
| |
| if (dummyIdentifierChanger != null) { |
| LOG.error("Changing the dummy identifier twice, already changed by " + dummyIdentifierChanger); |
| } |
| dummyIdentifierChanger = current.get(); |
| } |
| }; |
| List<CompletionContributor> contributors = CompletionContributor.forLanguage(context.getPositionLanguage()); |
| Project project = psiFile.getProject(); |
| List<CompletionContributor> filteredContributors = DumbService.getInstance(project).filterByDumbAwareness(contributors); |
| for (final CompletionContributor contributor : filteredContributors) { |
| current.set(contributor); |
| contributor.beforeCompletion(context); |
| CompletionAssertions.checkEditorValid(editor); |
| assert !PsiDocumentManager.getInstance(project).isUncommited(editor.getDocument()) : "Contributor " + contributor + " left the document uncommitted"; |
| } |
| return context; |
| } |
| |
| private static void checkNoWriteAccess() { |
| if (!ApplicationManager.getApplication().isUnitTestMode()) { |
| if (ApplicationManager.getApplication().isWriteAccessAllowed()) { |
| throw new AssertionError("Completion should not be invoked inside write action"); |
| } |
| } |
| } |
| |
| private static boolean shouldSkipAutoPopup(Editor editor, PsiFile psiFile) { |
| int offset = editor.getCaretModel().getOffset(); |
| int psiOffset = Math.max(0, offset - 1); |
| |
| PsiElement elementAt = InjectedLanguageUtil.findInjectedElementNoCommit(psiFile, psiOffset); |
| if (elementAt == null) { |
| elementAt = psiFile.findElementAt(psiOffset); |
| } |
| if (elementAt == null) return true; |
| |
| Language language = PsiUtilCore.findLanguageFromElement(elementAt); |
| |
| for (CompletionConfidence confidence : CompletionConfidenceEP.forLanguage(language)) { |
| final ThreeState result = confidence.shouldSkipAutopopup(elementAt, psiFile, offset); |
| if (result != ThreeState.UNSURE) { |
| LOG.debug(confidence + " has returned shouldSkipAutopopup=" + result); |
| return result == ThreeState.YES; |
| } |
| } |
| return false; |
| } |
| |
| @NotNull |
| private LookupImpl obtainLookup(Editor editor) { |
| CompletionAssertions.checkEditorValid(editor); |
| LookupImpl existing = (LookupImpl)LookupManager.getActiveLookup(editor); |
| if (existing != null && existing.isCompletion()) { |
| existing.markReused(); |
| if (!autopopup) { |
| existing.setFocusDegree(LookupImpl.FocusDegree.FOCUSED); |
| } |
| return existing; |
| } |
| |
| LookupImpl lookup = (LookupImpl)LookupManager.getInstance(editor.getProject()).createLookup(editor, LookupElement.EMPTY_ARRAY, "", |
| new LookupArranger.DefaultArranger()); |
| if (editor.isOneLineMode()) { |
| lookup.setCancelOnClickOutside(true); |
| lookup.setCancelOnOtherWindowOpen(true); |
| } |
| lookup.setFocusDegree(autopopup ? LookupImpl.FocusDegree.UNFOCUSED : LookupImpl.FocusDegree.FOCUSED); |
| return lookup; |
| } |
| |
| private void doComplete(CompletionInitializationContext initContext, |
| boolean hasModifiers, |
| int invocationCount, |
| PsiFile hostCopy, |
| OffsetMap hostMap, OffsetTranslator translator) { |
| final Editor editor = initContext.getEditor(); |
| CompletionAssertions.checkEditorValid(editor); |
| |
| CompletionContext context = createCompletionContext(hostCopy, hostMap.getOffset(CompletionInitializationContext.START_OFFSET), hostMap, initContext.getFile()); |
| LookupImpl lookup = obtainLookup(editor); |
| CompletionParameters parameters = createCompletionParameters(invocationCount, context, editor); |
| |
| CompletionPhase phase = CompletionServiceImpl.getCompletionPhase(); |
| if (phase instanceof CompletionPhase.CommittingDocuments) { |
| if (phase.indicator != null) { |
| phase.indicator.closeAndFinish(false); |
| } |
| ((CompletionPhase.CommittingDocuments)phase).replaced = true; |
| } else { |
| CompletionServiceImpl.assertPhase(CompletionPhase.NoCompletion.getClass()); |
| } |
| |
| final Semaphore freezeSemaphore = new Semaphore(); |
| freezeSemaphore.down(); |
| final CompletionProgressIndicator indicator = new CompletionProgressIndicator(editor, parameters, this, freezeSemaphore, |
| initContext.getOffsetMap(), hasModifiers, lookup); |
| Disposer.register(indicator, hostMap); |
| Disposer.register(indicator, context.getOffsetMap()); |
| Disposer.register(indicator, translator); |
| |
| CompletionServiceImpl.setCompletionPhase(synchronous ? new CompletionPhase.Synchronous(indicator) : new CompletionPhase.BgCalculation(indicator)); |
| |
| indicator.startCompletion(initContext); |
| |
| if (!synchronous) { |
| return; |
| } |
| |
| if (freezeSemaphore.waitFor(2000)) { |
| if (!indicator.isRunning() && !indicator.isCanceled()) { // the completion is really finished, now we may auto-insert or show lookup |
| try { |
| indicator.getLookup().refreshUi(true, false); |
| } |
| catch (Exception e) { |
| CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion); |
| LOG.error(e); |
| return; |
| } |
| |
| completionFinished(indicator, hasModifiers); |
| return; |
| } |
| } |
| |
| CompletionServiceImpl.setCompletionPhase(new CompletionPhase.BgCalculation(indicator)); |
| indicator.showLookup(); |
| } |
| |
| private static void checkNotSync(CompletionProgressIndicator indicator, List<LookupElement> allItems) { |
| if (CompletionServiceImpl.isPhase(CompletionPhase.Synchronous.class)) { |
| LOG.error("sync phase survived: " + allItems + "; indicator=" + CompletionServiceImpl.getCompletionPhase().indicator + "; myIndicator=" + indicator); |
| CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion); |
| } |
| } |
| |
| private CompletionParameters createCompletionParameters(int invocationCount, |
| final CompletionContext newContext, Editor editor) { |
| final int offset = newContext.getStartOffset(); |
| final PsiFile fileCopy = newContext.file; |
| PsiFile originalFile = fileCopy.getOriginalFile(); |
| final PsiElement insertedElement = findCompletionPositionLeaf(newContext, offset, fileCopy, originalFile); |
| insertedElement.putUserData(CompletionContext.COMPLETION_CONTEXT_KEY, newContext); |
| return new CompletionParameters(insertedElement, originalFile, myCompletionType, offset, invocationCount, editor); |
| } |
| |
| @NotNull |
| private static PsiElement findCompletionPositionLeaf(CompletionContext newContext, int offset, PsiFile fileCopy, PsiFile originalFile) { |
| final PsiElement insertedElement = newContext.file.findElementAt(offset); |
| CompletionAssertions.assertCompletionPositionPsiConsistent(newContext, offset, fileCopy, originalFile, insertedElement); |
| return insertedElement; |
| } |
| |
| private AutoCompletionDecision shouldAutoComplete(final CompletionProgressIndicator indicator, List<LookupElement> items) { |
| if (!invokedExplicitly) { |
| return AutoCompletionDecision.SHOW_LOOKUP; |
| } |
| final CompletionParameters parameters = indicator.getParameters(); |
| final LookupElement item = items.get(0); |
| if (items.size() == 1) { |
| final AutoCompletionPolicy policy = getAutocompletionPolicy(item); |
| if (policy == AutoCompletionPolicy.NEVER_AUTOCOMPLETE) return AutoCompletionDecision.SHOW_LOOKUP; |
| if (policy == AutoCompletionPolicy.ALWAYS_AUTOCOMPLETE) return AutoCompletionDecision.insertItem(item); |
| if (!indicator.getLookup().itemMatcher(item).isStartMatch(item)) return AutoCompletionDecision.SHOW_LOOKUP; |
| } |
| if (!isAutocompleteOnInvocation(parameters.getCompletionType())) { |
| return AutoCompletionDecision.SHOW_LOOKUP; |
| } |
| if (isInsideIdentifier(indicator.getOffsetMap())) { |
| return AutoCompletionDecision.SHOW_LOOKUP; |
| } |
| if (items.size() == 1 && getAutocompletionPolicy(item) == AutoCompletionPolicy.GIVE_CHANCE_TO_OVERWRITE) { |
| return AutoCompletionDecision.insertItem(item); |
| } |
| |
| AutoCompletionContext context = new AutoCompletionContext(parameters, items.toArray(new LookupElement[items.size()]), indicator.getOffsetMap(), indicator.getLookup()); |
| for (final CompletionContributor contributor : CompletionContributor.forParameters(parameters)) { |
| final AutoCompletionDecision decision = contributor.handleAutoCompletionPossibility(context); |
| if (decision != null) { |
| return decision; |
| } |
| } |
| |
| return AutoCompletionDecision.SHOW_LOOKUP; |
| } |
| |
| @Nullable |
| private static AutoCompletionPolicy getAutocompletionPolicy(LookupElement element) { |
| final AutoCompletionPolicy policy = AutoCompletionPolicy.getPolicy(element); |
| if (policy != null) { |
| return policy; |
| } |
| |
| final LookupItem item = element.as(LookupItem.CLASS_CONDITION_KEY); |
| if (item != null) { |
| return item.getAutoCompletionPolicy(); |
| } |
| |
| return null; |
| } |
| |
| private static boolean isInsideIdentifier(final OffsetMap offsetMap) { |
| return offsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) != offsetMap.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET); |
| } |
| |
| protected void completionFinished(final CompletionProgressIndicator indicator, boolean hasModifiers) { |
| final List<LookupElement> items = indicator.getLookup().getItems(); |
| if (items.isEmpty()) { |
| LookupManager.getInstance(indicator.getProject()).hideActiveLookup(); |
| |
| Caret nextCaret = getNextCaretToProcess(indicator.getEditor()); |
| if (nextCaret != null) { |
| invokeCompletion(indicator.getProject(), indicator.getEditor(), indicator.getParameters().getInvocationCount(), hasModifiers, false, nextCaret); |
| } |
| else { |
| indicator.handleEmptyLookup(true); |
| checkNotSync(indicator, items); |
| } |
| return; |
| } |
| |
| LOG.assertTrue(!indicator.isRunning(), "running"); |
| LOG.assertTrue(!indicator.isCanceled(), "canceled"); |
| |
| try { |
| final AutoCompletionDecision decision = shouldAutoComplete(indicator, items); |
| if (decision == AutoCompletionDecision.SHOW_LOOKUP) { |
| CompletionServiceImpl.setCompletionPhase(new CompletionPhase.ItemsCalculated(indicator)); |
| indicator.getLookup().setCalculating(false); |
| indicator.showLookup(); |
| } |
| else if (decision instanceof AutoCompletionDecision.InsertItem) { |
| final Runnable restorePrefix = rememberDocumentState(indicator.getEditor()); |
| |
| final LookupElement item = ((AutoCompletionDecision.InsertItem)decision).getElement(); |
| CommandProcessor.getInstance().executeCommand(indicator.getProject(), new Runnable() { |
| @Override |
| public void run() { |
| indicator.setMergeCommand(); |
| indicator.getLookup().finishLookup(Lookup.AUTO_INSERT_SELECT_CHAR, item); |
| } |
| }, "Autocompletion", null); |
| |
| // the insert handler may have started a live template with completion |
| if (CompletionService.getCompletionService().getCurrentCompletion() == null && |
| // ...or scheduled another autopopup |
| !CompletionServiceImpl.isPhase(CompletionPhase.CommittingDocuments.class)) { |
| CompletionServiceImpl.setCompletionPhase(hasModifiers? new CompletionPhase.InsertedSingleItem(indicator, restorePrefix) : CompletionPhase.NoCompletion); |
| } |
| } else if (decision == AutoCompletionDecision.CLOSE_LOOKUP) { |
| LookupManager.getInstance(indicator.getProject()).hideActiveLookup(); |
| } |
| } |
| catch (Throwable e) { |
| CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion); |
| LOG.error(e); |
| } |
| finally { |
| checkNotSync(indicator, items); |
| } |
| } |
| |
| private void insertDummyIdentifier(final CompletionInitializationContext initContext, |
| final boolean hasModifiers, |
| final int invocationCount) { |
| final PsiFile originalFile = initContext.getFile(); |
| InjectedLanguageManager manager = InjectedLanguageManager.getInstance(originalFile.getProject()); |
| final PsiFile hostFile = manager.getTopLevelFile(originalFile); |
| final Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(initContext.getEditor()); |
| final OffsetMap hostMap = translateOffsetMapToHost(initContext, originalFile, hostFile, hostEditor); |
| |
| final PsiFile[] hostCopy = {null}; |
| DocumentUtil.writeInRunUndoTransparentAction(new Runnable() { |
| @Override |
| public void run() { |
| hostCopy[0] = createFileCopy(hostFile, initContext.getStartOffset(), initContext.getSelectionEndOffset()); |
| } |
| }); |
| |
| final Document copyDocument = hostCopy[0].getViewProvider().getDocument(); |
| assert copyDocument != null : "no document"; |
| final OffsetTranslator translator = new OffsetTranslator(hostEditor.getDocument(), initContext.getFile(), copyDocument); |
| |
| CompletionAssertions.checkEditorValid(initContext.getEditor()); |
| CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { |
| @Override |
| public void run() { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| String dummyIdentifier = initContext.getDummyIdentifier(); |
| if (StringUtil.isEmpty(dummyIdentifier)) return; |
| |
| int startOffset = hostMap.getOffset(CompletionInitializationContext.START_OFFSET); |
| int endOffset = hostMap.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET); |
| copyDocument.replaceString(startOffset, endOffset, dummyIdentifier); |
| } |
| }); |
| } |
| }); |
| CompletionAssertions.checkEditorValid(initContext.getEditor()); |
| |
| final Project project = originalFile.getProject(); |
| |
| if (!synchronous) { |
| if (CompletionServiceImpl.isPhase(CompletionPhase.NoCompletion.getClass()) || |
| !CompletionServiceImpl.assertPhase(CompletionPhase.CommittingDocuments.class)) { |
| Disposer.dispose(translator); |
| return; |
| } |
| |
| final CompletionPhase.CommittingDocuments phase = (CompletionPhase.CommittingDocuments)CompletionServiceImpl.getCompletionPhase(); |
| |
| CompletionAutoPopupHandler.runLaterWithCommitted(project, copyDocument, new Runnable() { |
| @Override |
| public void run() { |
| if (phase.checkExpired()) { |
| Disposer.dispose(translator); |
| return; |
| } |
| doComplete(initContext, hasModifiers, invocationCount, hostCopy[0], hostMap, translator); |
| } |
| }); |
| } |
| else { |
| PsiDocumentManager.getInstance(project).commitDocument(copyDocument); |
| |
| doComplete(initContext, hasModifiers, invocationCount, hostCopy[0], hostMap, translator); |
| } |
| } |
| |
| private static OffsetMap translateOffsetMapToHost(CompletionInitializationContext initContext, |
| PsiFile context, |
| PsiFile hostFile, |
| Editor hostEditor) { |
| final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(hostFile.getProject()); |
| final OffsetMap hostMap = new OffsetMap(hostEditor.getDocument()); |
| final OffsetMap original = initContext.getOffsetMap(); |
| for (final OffsetKey key : original.getAllOffsets()) { |
| hostMap.addOffset(key, injectedLanguageManager.injectedToHost(context, original.getOffset(key))); |
| } |
| return hostMap; |
| } |
| |
| private static CompletionContext createCompletionContext(PsiFile hostCopy, |
| int hostStartOffset, |
| OffsetMap hostMap, PsiFile originalFile) { |
| CompletionAssertions.assertHostInfo(hostCopy, hostMap); |
| |
| InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(hostCopy.getProject()); |
| CompletionContext context; |
| PsiFile injected = InjectedLanguageUtil.findInjectedPsiNoCommit(hostCopy, hostStartOffset); |
| if (injected != null) { |
| if (injected instanceof PsiFileImpl) { |
| ((PsiFileImpl)injected).setOriginalFile(originalFile); |
| } |
| DocumentWindow documentWindow = InjectedLanguageUtil.getDocumentWindow(injected); |
| CompletionAssertions.assertInjectedOffsets(hostStartOffset, injectedLanguageManager, injected, documentWindow); |
| |
| context = new CompletionContext(injected, translateOffsetMapToInjected(hostMap, documentWindow)); |
| } else { |
| context = new CompletionContext(hostCopy, hostMap); |
| } |
| |
| CompletionAssertions.assertFinalOffsets(originalFile, context, injected); |
| |
| return context; |
| } |
| |
| private static OffsetMap translateOffsetMapToInjected(OffsetMap hostMap, DocumentWindow injectedDocument) { |
| final OffsetMap map = new OffsetMap(injectedDocument); |
| for (final OffsetKey key : hostMap.getAllOffsets()) { |
| map.addOffset(key, injectedDocument.hostToInjected(hostMap.getOffset(key))); |
| } |
| return map; |
| } |
| |
| protected void lookupItemSelected(final CompletionProgressIndicator indicator, @NotNull final LookupElement item, final char completionChar, |
| final List<LookupElement> items) { |
| if (indicator.isAutopopupCompletion()) { |
| FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_BASIC); |
| } |
| |
| CompletionAssertions.WatchingInsertionContext context = null; |
| try { |
| Lookup lookup = indicator.getLookup(); |
| CompletionLookupArranger.StatisticsUpdate update = CompletionLookupArranger.collectStatisticChanges(item, lookup); |
| context = insertItemHonorBlockSelection(indicator, item, completionChar, items, update); |
| CompletionLookupArranger.trackStatistics(context, update); |
| } |
| finally { |
| afterItemInsertion(indicator, context == null ? null : context.getLaterRunnable()); |
| } |
| |
| } |
| |
| private static CompletionAssertions.WatchingInsertionContext insertItemHonorBlockSelection(final CompletionProgressIndicator indicator, |
| final LookupElement item, |
| final char completionChar, |
| final List<LookupElement> items, |
| final CompletionLookupArranger.StatisticsUpdate update) { |
| final Editor editor = indicator.getEditor(); |
| |
| final int caretOffset = editor.getCaretModel().getOffset(); |
| int idEndOffset = indicator.getIdentifierEndOffset(); |
| if (idEndOffset < 0) { |
| idEndOffset = CompletionInitializationContext.calcDefaultIdentifierEnd(editor, caretOffset); |
| } |
| final int idEndOffsetDelta = idEndOffset - caretOffset; |
| |
| CompletionAssertions.WatchingInsertionContext context = null; |
| if (editor.getSelectionModel().hasBlockSelection() && editor.getSelectionModel().getBlockSelectionEnds().length > 0) { |
| List<RangeMarker> insertionPoints = new ArrayList<RangeMarker>(); |
| int idDelta = 0; |
| Document document = editor.getDocument(); |
| int caretLine = document.getLineNumber(editor.getCaretModel().getOffset()); |
| |
| for (int point : editor.getSelectionModel().getBlockSelectionEnds()) { |
| insertionPoints.add(document.createRangeMarker(point, point)); |
| if (document.getLineNumber(point) == document.getLineNumber(idEndOffset)) { |
| idDelta = idEndOffset - point; |
| } |
| } |
| |
| List<RangeMarker> caretsAfter = new ArrayList<RangeMarker>(); |
| for (RangeMarker marker : insertionPoints) { |
| if (marker.isValid()) { |
| int insertionPoint = marker.getStartOffset(); |
| context = insertItem(indicator, item, completionChar, items, update, editor, indicator.getParameters().getOriginalFile(), |
| insertionPoint, idDelta + insertionPoint); |
| int offset = editor.getCaretModel().getOffset(); |
| caretsAfter.add(document.createRangeMarker(offset, offset)); |
| } |
| } |
| assert context != null; |
| |
| restoreBlockSelection(editor, caretsAfter, caretLine); |
| |
| for (RangeMarker insertionPoint : insertionPoints) { |
| insertionPoint.dispose(); |
| } |
| for (RangeMarker marker : caretsAfter) { |
| marker.dispose(); |
| } |
| |
| } else if (editor.getCaretModel().supportsMultipleCarets()) { |
| final List<CompletionAssertions.WatchingInsertionContext> contexts = new ArrayList<CompletionAssertions.WatchingInsertionContext>(); |
| final Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(editor); |
| hostEditor.getCaretModel().runForEachCaret(new CaretAction() { |
| @Override |
| public void perform(Caret caret) { |
| PsiFile hostFile = InjectedLanguageUtil.getTopLevelFile(indicator.getParameters().getOriginalFile()); |
| PsiFile targetFile = InjectedLanguageUtil.findInjectedPsiNoCommit(hostFile, caret.getOffset()); |
| Editor targetEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(hostEditor, targetFile); |
| int targetCaretOffset = targetEditor.getCaretModel().getOffset(); |
| CompletionAssertions.WatchingInsertionContext currentContext = insertItem(indicator, item, completionChar, items, update, |
| targetEditor, targetFile == null ? hostFile : targetFile, |
| targetCaretOffset, targetCaretOffset + idEndOffsetDelta); |
| contexts.add(currentContext); |
| } |
| }, true); |
| context = contexts.get(contexts.size() - 1); |
| if (context.shouldAddCompletionChar() && context.getCompletionChar() != Lookup.COMPLETE_STATEMENT_SELECT_CHAR) { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| DataContext dataContext = DataManager.getInstance().getDataContext(editor.getContentComponent()); |
| EditorActionManager.getInstance().getTypedAction().getHandler().execute(editor, completionChar, dataContext); |
| } |
| }); |
| } |
| for (CompletionAssertions.WatchingInsertionContext insertionContext : contexts) { |
| insertionContext.stopWatching(); |
| } |
| } else { |
| context = insertItem(indicator, item, completionChar, items, update, editor, indicator.getParameters().getOriginalFile(), caretOffset, |
| idEndOffset); |
| } |
| return context; |
| } |
| |
| private static void afterItemInsertion(final CompletionProgressIndicator indicator, final Runnable laterRunnable) { |
| if (laterRunnable != null) { |
| final Runnable runnable1 = new Runnable() { |
| @Override |
| public void run() { |
| if (!indicator.getProject().isDisposed()) { |
| laterRunnable.run(); |
| } |
| indicator.disposeIndicator(); |
| } |
| }; |
| if (ApplicationManager.getApplication().isUnitTestMode()) { |
| runnable1.run(); |
| } |
| else { |
| ApplicationManager.getApplication().invokeLater(runnable1); |
| } |
| } |
| else { |
| indicator.disposeIndicator(); |
| } |
| } |
| |
| private static void restoreBlockSelection(Editor editor, List<RangeMarker> caretsAfter, int caretLine) { |
| int column = -1; |
| int minLine = Integer.MAX_VALUE; |
| int maxLine = -1; |
| for (RangeMarker marker : caretsAfter) { |
| if (marker.isValid()) { |
| LogicalPosition lp = editor.offsetToLogicalPosition(marker.getStartOffset()); |
| if (column == -1) { |
| column = lp.column; |
| } else if (column != lp.column) { |
| return; |
| } |
| minLine = Math.min(minLine, lp.line); |
| maxLine = Math.max(maxLine, lp.line); |
| |
| if (lp.line == caretLine) { |
| editor.getCaretModel().moveToLogicalPosition(lp); |
| } |
| } |
| } |
| editor.getSelectionModel().setBlockSelection(new LogicalPosition(minLine, column), new LogicalPosition(maxLine, column)); |
| } |
| |
| private static CompletionAssertions.WatchingInsertionContext insertItem(final CompletionProgressIndicator indicator, |
| final LookupElement item, |
| final char completionChar, |
| List<LookupElement> items, |
| final CompletionLookupArranger.StatisticsUpdate update, |
| final Editor editor, |
| final PsiFile psiFile, |
| final int caretOffset, final int idEndOffset) { |
| editor.getCaretModel().moveToOffset(caretOffset); |
| final int initialStartOffset = caretOffset - item.getLookupString().length(); |
| assert initialStartOffset >= 0 : "negative startOffset: " + caretOffset + "; " + item.getLookupString(); |
| |
| indicator.getOffsetMap().addOffset(CompletionInitializationContext.START_OFFSET, initialStartOffset); |
| indicator.getOffsetMap().addOffset(CompletionInitializationContext.SELECTION_END_OFFSET, caretOffset); |
| indicator.getOffsetMap().addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, idEndOffset); |
| |
| final CompletionAssertions.WatchingInsertionContext |
| context = new CompletionAssertions.WatchingInsertionContext(indicator.getOffsetMap(), psiFile, |
| completionChar, items, editor); |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| if (caretOffset != idEndOffset && completionChar == Lookup.REPLACE_SELECT_CHAR) { |
| editor.getDocument().deleteString(caretOffset, idEndOffset); |
| } |
| |
| assert context.getStartOffset() >= 0 : "stale startOffset: was " + initialStartOffset + "; selEnd=" + caretOffset + "; idEnd=" + idEndOffset + "; file=" + context.getFile(); |
| assert context.getTailOffset() >= 0 : "stale tail: was " + initialStartOffset + "; selEnd=" + caretOffset + "; idEnd=" + idEndOffset + "; file=" + context.getFile(); |
| |
| item.handleInsert(context); |
| Project project = indicator.getProject(); |
| PostprocessReformattingAspect.getInstance(project).doPostponedFormatting(); |
| |
| if (context.shouldAddCompletionChar()) { |
| addCompletionChar(project, context, item, editor, indicator, completionChar); |
| } |
| if (!editor.getCaretModel().supportsMultipleCarets()) { // done later, outside of this method |
| context.stopWatching(); |
| } |
| editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); |
| } |
| }); |
| update.addSparedChars(indicator, item, context, completionChar); |
| return context; |
| } |
| |
| private static void addCompletionChar(Project project, |
| CompletionAssertions.WatchingInsertionContext context, |
| LookupElement item, |
| Editor editor, CompletionProgressIndicator indicator, char completionChar) { |
| int tailOffset = context.getTailOffset(); |
| if (tailOffset < 0) { |
| LOG.info("tailOffset<0 after inserting " + item + " of " + item.getClass() + "; invalidated at: " + context.invalidateTrace + "\n--------"); |
| } |
| else { |
| editor.getCaretModel().moveToOffset(tailOffset); |
| } |
| if (context.getCompletionChar() == Lookup.COMPLETE_STATEMENT_SELECT_CHAR) { |
| final Language language = PsiUtilBase.getLanguageInEditor(editor, project); |
| if (language != null) { |
| for (SmartEnterProcessor processor : SmartEnterProcessors.INSTANCE.forKey(language)) { |
| if (processor.processAfterCompletion(editor, indicator.getParameters().getOriginalFile())) break; |
| } |
| } |
| } |
| else if (!editor.getCaretModel().supportsMultipleCarets()) { // this will be done outside of runForEach caret context |
| DataContext dataContext = DataManager.getInstance().getDataContext(editor.getContentComponent()); |
| EditorActionManager.getInstance().getTypedAction().getHandler().execute(editor, completionChar, dataContext); |
| } |
| } |
| |
| private static final Key<SoftReference<Trinity<PsiFile, Document, Long>>> FILE_COPY_KEY = Key.create("CompletionFileCopy"); |
| |
| private static boolean isCopyUpToDate(Document document, @NotNull PsiFile file) { |
| if (!file.isValid()) { |
| return false; |
| } |
| // the psi file cache might have been cleared by some external activity, |
| // in which case PSI-document sync may stop working |
| PsiFile current = PsiDocumentManager.getInstance(file.getProject()).getPsiFile(document); |
| return current != null && current.getViewProvider().getPsi(file.getLanguage()) == file; |
| } |
| |
| private static PsiFile createFileCopy(PsiFile file, long caret, long selEnd) { |
| final VirtualFile virtualFile = file.getVirtualFile(); |
| boolean mayCacheCopy = file.isPhysical() && |
| // we don't want to cache code fragment copies even if they appear to be physical |
| virtualFile != null && virtualFile.isInLocalFileSystem(); |
| long combinedOffsets = caret + (selEnd << 32); |
| if (mayCacheCopy) { |
| final Trinity<PsiFile, Document, Long> cached = SoftReference.dereference(file.getUserData(FILE_COPY_KEY)); |
| if (cached != null && cached.first.getClass().equals(file.getClass()) && isCopyUpToDate(cached.second, cached.first)) { |
| final PsiFile copy = cached.first; |
| if (copy.getViewProvider().getModificationStamp() > file.getViewProvider().getModificationStamp() && |
| cached.third.longValue() != combinedOffsets) { |
| // the copy PSI might have some caches that are not cleared on its modification because there are no events in the copy |
| // so, clear all the caches |
| // hopefully it's a rare situation that the user invokes completion in different parts of the file |
| // without modifying anything physical in between |
| ((PsiModificationTrackerImpl) file.getManager().getModificationTracker()).incCounter(); |
| } |
| final Document document = cached.second; |
| assert document != null; |
| file.putUserData(FILE_COPY_KEY, new SoftReference<Trinity<PsiFile,Document, Long>>(Trinity.create(copy, document, combinedOffsets))); |
| |
| Document originalDocument = file.getViewProvider().getDocument(); |
| assert originalDocument != null; |
| assert originalDocument.getTextLength() == file.getTextLength() : originalDocument; |
| document.setText(originalDocument.getImmutableCharSequence()); |
| return copy; |
| } |
| } |
| |
| final PsiFile copy = (PsiFile)file.copy(); |
| if (mayCacheCopy) { |
| final Document document = copy.getViewProvider().getDocument(); |
| assert document != null; |
| file.putUserData(FILE_COPY_KEY, new SoftReference<Trinity<PsiFile,Document, Long>>(Trinity.create(copy, document, combinedOffsets))); |
| } |
| return copy; |
| } |
| |
| private static boolean isAutocompleteOnInvocation(final CompletionType type) { |
| final CodeInsightSettings settings = CodeInsightSettings.getInstance(); |
| if (type == CompletionType.SMART) { |
| return settings.AUTOCOMPLETE_ON_SMART_TYPE_COMPLETION; |
| } |
| return settings.AUTOCOMPLETE_ON_CODE_COMPLETION; |
| } |
| |
| private static Runnable rememberDocumentState(final Editor _editor) { |
| final Editor editor = InjectedLanguageUtil.getTopLevelEditor(_editor); |
| final String documentText = editor.getDocument().getText(); |
| final int caret = editor.getCaretModel().getOffset(); |
| final int selStart = editor.getSelectionModel().getSelectionStart(); |
| final int selEnd = editor.getSelectionModel().getSelectionEnd(); |
| |
| final int vOffset = editor.getScrollingModel().getVerticalScrollOffset(); |
| final int hOffset = editor.getScrollingModel().getHorizontalScrollOffset(); |
| |
| return new Runnable() { |
| @Override |
| public void run() { |
| DocumentEx document = (DocumentEx) editor.getDocument(); |
| |
| document.replaceString(0, document.getTextLength(), documentText); |
| editor.getCaretModel().moveToOffset(caret); |
| editor.getSelectionModel().setSelection(selStart, selEnd); |
| |
| editor.getScrollingModel().scrollHorizontally(hOffset); |
| editor.getScrollingModel().scrollVertically(vOffset); |
| } |
| }; |
| } |
| |
| private static void clearCaretMarkers(@NotNull Editor editor) { |
| for (Caret caret : editor.getCaretModel().getAllCarets()) { |
| caret.putUserData(CARET_PROCESSED, null); |
| } |
| } |
| |
| private static void markCaretAsProcessed(@NotNull Caret caret) { |
| caret.putUserData(CARET_PROCESSED, Boolean.TRUE); |
| } |
| |
| private static Caret getNextCaretToProcess(@NotNull Editor editor) { |
| for (Caret caret : editor.getCaretModel().getAllCarets()) { |
| if (caret.getUserData(CARET_PROCESSED) == null) { |
| return caret; |
| } |
| } |
| return null; |
| } |
| } |