/* * 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.AutoPopupController; import com.intellij.codeInsight.CodeInsightSettings; import com.intellij.codeInsight.completion.actions.BaseCodeCompletionAction; import com.intellij.codeInsight.completion.impl.CompletionServiceImpl; 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.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.TransactionGuard; 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.Pair; import com.intellij.openapi.util.Ref; 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.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.ThreeState; import com.intellij.util.concurrency.Semaphore; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.util.ArrayList; import java.util.List; @SuppressWarnings("deprecation") 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; private static int ourAutoInsertItemTimeout = 2000; public static CodeCompletionHandlerBase createHandler(@NotNull CompletionType completionType) { return createHandler(completionType, true, false, true); } public static CodeCompletionHandlerBase createHandler(@NotNull CompletionType completionType, boolean invokedExplicitly, boolean autopopup, boolean synchronous) { AnAction codeCompletionAction = ActionManager.getInstance().getAction("CodeCompletion"); assert (codeCompletionAction instanceof BaseCodeCompletionAction); BaseCodeCompletionAction baseCodeCompletionAction = (BaseCodeCompletionAction) codeCompletionAction; return baseCodeCompletionAction.createHandler(completionType, invokedExplicitly, autopopup, synchronous); } 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) { StatisticsUpdate.applyLastCompletionStatisticsUpdate(); } checkNoWriteAccess(); CompletionAssertions.checkEditorValid(editor); int offset = editor.getCaretModel().getOffset(); if (editor.isViewer() || editor.getDocument().getRangeGuard(offset, offset) != null) { editor.getDocument().fireReadOnlyModificationAttempt(); EditorModificationUtil.checkModificationAllowed(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 = () -> { Runnable runnable = () -> { 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); CompletionAssertions.checkEditorValid(editor); 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, @NotNull 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, Project project) { 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(project).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, OffsetTranslator translator, OffsetsInFile hostOffsets, OffsetsInFile hostCopyOffsets) { final Editor editor = initContext.getEditor(); CompletionAssertions.checkEditorValid(editor); CompletionContext context = createCompletionContext(initContext.getFile(), hostCopyOffsets); LookupImpl lookup = obtainLookup(editor, initContext.getProject()); 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 CompletionProgressIndicator indicator = new CompletionProgressIndicator(editor, initContext.getCaret(), parameters, this, initContext.getOffsetMap(), hostOffsets, hasModifiers, lookup); Disposer.register(indicator, hostCopyOffsets.getOffsets()); 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 (indicator.blockingWaitForFinish(ourAutoInsertItemTimeout)) { 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) { return element.getAutoCompletionPolicy(); } 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(), () -> { 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) { CompletionAssertions.checkEditorValid(initContext.getEditor()); Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(initContext.getEditor()); OffsetsInFile topLevelOffsets = new OffsetsInFile(initContext.getFile(), initContext.getOffsetMap()).toTopLevelFile(); OffsetMap hostMap = topLevelOffsets.getOffsets(); PsiFile hostCopy = createFileCopy(topLevelOffsets.getFile()); Document copyDocument = hostCopy.getViewProvider().getDocument(); assert copyDocument != null : "no document"; OffsetsInFile copyOffsets = topLevelOffsets.toFileCopy(hostCopy); OffsetTranslator translator = new OffsetTranslator(hostEditor.getDocument(), initContext.getFile(), copyDocument); CompletionAssertions.checkEditorValid(initContext.getEditor()); String dummyIdentifier = initContext.getDummyIdentifier(); if (!StringUtil.isEmpty(dummyIdentifier)) { int startOffset = hostMap.getOffset(CompletionInitializationContext.START_OFFSET); int endOffset = hostMap.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET); copyDocument.replaceString(startOffset, endOffset, dummyIdentifier); } CompletionAssertions.checkEditorValid(initContext.getEditor()); Project project = initContext.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(); AutoPopupController.runTransactionWithEverythingCommitted(project, () -> { if (phase.checkExpired() || !initContext.getFile().isValid() || !hostCopy.isValid() || !CompletionAssertions.isEditorValid(initContext.getEditor())) { Disposer.dispose(translator); return; } doComplete(initContext, hasModifiers, invocationCount, translator, topLevelOffsets, copyOffsets); }); } else { PsiDocumentManager.getInstance(project).commitDocument(copyDocument); doComplete(initContext, hasModifiers, invocationCount, translator, topLevelOffsets, copyOffsets); } } private static CompletionContext createCompletionContext(PsiFile originalFile, OffsetsInFile hostCopyOffsets) { CompletionAssertions.assertHostInfo(hostCopyOffsets.getFile(), hostCopyOffsets.getOffsets()); int hostStartOffset = hostCopyOffsets.getOffsets().getOffset(CompletionInitializationContext.START_OFFSET); OffsetsInFile translatedOffsets = hostCopyOffsets.toInjectedIfAny(hostStartOffset); InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(originalFile.getProject()); PsiFile injected = translatedOffsets == hostCopyOffsets ? null : translatedOffsets.getFile(); if (injected != null) { if (injected instanceof PsiFileImpl) { ((PsiFileImpl)injected).setOriginalFile(originalFile); } DocumentWindow documentWindow = InjectedLanguageUtil.getDocumentWindow(injected); CompletionAssertions.assertInjectedOffsets(hostStartOffset, injectedLanguageManager, injected, documentWindow); } CompletionContext context = new CompletionContext(translatedOffsets.getFile(), translatedOffsets.getOffsets()); CompletionAssertions.assertFinalOffsets(originalFile, context, injected); return context; } 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 { StatisticsUpdate update = StatisticsUpdate.collectStatisticChanges(item); context = insertItemHonorBlockSelection(indicator, item, completionChar, items, update); update.trackStatistics(context); } 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 StatisticsUpdate update) { final Editor editor = indicator.getEditor(); final int caretOffset = indicator.getCaret().getOffset(); final int idEndOffset = indicator.getOffsetMap().containsOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) ? indicator.getIdentifierEndOffset() : CompletionInitializationContext.calcDefaultIdentifierEnd(editor, caretOffset); final int idEndOffsetDelta = idEndOffset - caretOffset; CompletionAssertions.WatchingInsertionContext context; if (editor.getCaretModel().supportsMultipleCarets()) { List<CompletionAssertions.WatchingInsertionContext> contexts = new ArrayList<>(); Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(editor); OffsetsInFile topLevelOffsets = indicator.getHostOffsets(); hostEditor.getCaretModel().runForEachCaret(new CaretAction() { @Override public void perform(Caret caret) { PsiDocumentManager.getInstance(indicator.getProject()).commitDocument(hostEditor.getDocument()); OffsetsInFile targetOffsets = topLevelOffsets.toInjectedIfAny(caret.getOffset()); PsiFile targetFile = targetOffsets.getFile(); Editor targetEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(hostEditor, targetFile); int targetCaretOffset = targetEditor.getCaretModel().getOffset(); int idEnd = targetCaretOffset + idEndOffsetDelta; if (idEnd > targetEditor.getDocument().getTextLength()) { idEnd = targetCaretOffset; // no replacement by Tab when offsets gone wrong for some reason } CompletionAssertions.WatchingInsertionContext currentContext = insertItem(indicator, item, completionChar, items, update, targetEditor, targetFile, targetCaretOffset, idEnd, targetOffsets.getOffsets()); contexts.add(currentContext); } }); context = contexts.get(contexts.size() - 1); if (context.shouldAddCompletionChar() && context.getCompletionChar() != Lookup.COMPLETE_STATEMENT_SELECT_CHAR) { ApplicationManager.getApplication().runWriteAction(() -> { 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, indicator.getOffsetMap()); } return context; } public static void afterItemInsertion(final CompletionProgressIndicator indicator, final Runnable laterRunnable) { if (laterRunnable != null) { ActionTracker tracker = new ActionTracker(indicator.getEditor(), indicator); Runnable wrapper = () -> { if (!indicator.getProject().isDisposed() && !tracker.hasAnythingHappened()) { laterRunnable.run(); } indicator.disposeIndicator(); }; if (ApplicationManager.getApplication().isUnitTestMode()) { wrapper.run(); } else { TransactionGuard.getInstance().submitTransactionLater(indicator, wrapper); } } else { indicator.disposeIndicator(); } } private static CompletionAssertions.WatchingInsertionContext insertItem(final CompletionProgressIndicator indicator, final LookupElement item, final char completionChar, List<LookupElement> items, final StatisticsUpdate update, final Editor editor, final PsiFile psiFile, final int caretOffset, final int idEndOffset, final OffsetMap offsetMap) { editor.getCaretModel().moveToOffset(caretOffset); final int initialStartOffset = caretOffset - item.getLookupString().length(); assert initialStartOffset >= 0 : "negative startOffset: " + caretOffset + "; " + item.getLookupString(); offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, initialStartOffset); offsetMap.addOffset(CompletionInitializationContext.SELECTION_END_OFFSET, caretOffset); offsetMap.addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, idEndOffset); final CompletionAssertions.WatchingInsertionContext context = new CompletionAssertions.WatchingInsertionContext(offsetMap, psiFile, completionChar, items, editor); ApplicationManager.getApplication().runWriteAction(() -> { 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(); Project project = indicator.getProject(); PsiDocumentManager.getInstance(project).commitAllDocuments(); item.handleInsert(context); 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(); } EditorModificationUtil.scrollToCaret(editor); }); update.addSparedChars(indicator, item, context); return context; } private static void addCompletionChar(Project project, CompletionAssertions.WatchingInsertionContext context, LookupElement item, Editor editor, CompletionProgressIndicator indicator, char completionChar) { if (!context.getOffsetMap().containsOffset(InsertionContext.TAIL_OFFSET)) { LOG.info("tailOffset<0 after inserting " + item + " of " + item.getClass() + "; invalidated at: " + context.invalidateTrace + "\n--------"); } else { editor.getCaretModel().moveToOffset(context.getTailOffset()); } 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<Pair<PsiFile, Document>>> FILE_COPY_KEY = Key.create("CompletionFileCopy"); private static boolean isCopyUpToDate(Document document, @NotNull PsiFile copyFile, @NotNull PsiFile originalFile) { if (!copyFile.getClass().equals(originalFile.getClass()) || !copyFile.isValid() || !copyFile.getName().equals(originalFile.getName())) { 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(copyFile.getProject()).getPsiFile(document); return current != null && current.getViewProvider().getPsi(copyFile.getLanguage()) == copyFile; } private static PsiFile createFileCopy(PsiFile file) { 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(); if (mayCacheCopy) { final Pair<PsiFile, Document> cached = SoftReference.dereference(file.getUserData(FILE_COPY_KEY)); if (cached != null && isCopyUpToDate(cached.second, cached.first, file)) { final PsiFile copy = cached.first; final Document document = cached.second; CompletionAssertions.assertCorrectOriginalFile("Cached", file, copy); Document originalDocument = file.getViewProvider().getDocument(); assert originalDocument != null; assert originalDocument.getTextLength() == file.getTextLength() : originalDocument; document.replaceString(0, document.getTextLength(), originalDocument.getImmutableCharSequence()); return copy; } } final PsiFile copy = (PsiFile)file.copy(); if (copy.isPhysical() || copy.getViewProvider().isEventSystemEnabled()) { LOG.error("File copy should be non-physical and non-event-system-enabled! Language=" + file.getLanguage() + "; file=" + file + " of " + file.getClass()); } CompletionAssertions.assertCorrectOriginalFile("New", file, copy); if (mayCacheCopy) { final Document document = copy.getViewProvider().getDocument(); assert document != null; file.putUserData(FILE_COPY_KEY, new SoftReference<>(Pair.create(copy, document))); } 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 () -> { 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; } @TestOnly public static void setAutoInsertTimeout(int timeout) { ourAutoInsertItemTimeout = timeout; } }