cd2c4f61c14261bc6069f4e4945b58db946e54a9
[idea/community.git] / platform / analysis-impl / src / com / intellij / codeInsight / completion / CompletionInitializationUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInsight.completion;
3
4 import com.intellij.injected.editor.DocumentWindow;
5 import com.intellij.injected.editor.EditorWindow;
6 import com.intellij.injected.editor.VirtualFileWindow;
7 import com.intellij.lang.injection.InjectedLanguageManager;
8 import com.intellij.openapi.Disposable;
9 import com.intellij.openapi.application.WriteAction;
10 import com.intellij.openapi.diagnostic.Logger;
11 import com.intellij.openapi.editor.Caret;
12 import com.intellij.openapi.editor.Document;
13 import com.intellij.openapi.editor.Editor;
14 import com.intellij.openapi.editor.impl.DocumentImpl;
15 import com.intellij.openapi.fileEditor.FileDocumentManager;
16 import com.intellij.openapi.project.Project;
17 import com.intellij.openapi.util.Disposer;
18 import com.intellij.openapi.util.Key;
19 import com.intellij.openapi.util.Pair;
20 import com.intellij.openapi.util.Ref;
21 import com.intellij.openapi.vfs.VirtualFile;
22 import com.intellij.psi.PsiDocumentManager;
23 import com.intellij.psi.PsiElement;
24 import com.intellij.psi.PsiFile;
25 import com.intellij.psi.impl.PsiFileEx;
26 import com.intellij.psi.impl.source.PsiFileImpl;
27 import com.intellij.psi.util.PsiTreeUtil;
28 import com.intellij.psi.util.PsiUtilBase;
29 import com.intellij.psi.util.PsiUtilCore;
30 import com.intellij.reference.SoftReference;
31 import org.jetbrains.annotations.ApiStatus;
32 import com.intellij.util.indexing.DumbModeAccessType;
33 import com.intellij.util.indexing.FileBasedIndex;
34 import org.jetbrains.annotations.NotNull;
35
36 import java.util.Objects;
37 import java.util.function.Consumer;
38 import java.util.function.Supplier;
39
40 /**
41  * @author yole
42  */
43 @ApiStatus.Internal
44 public class CompletionInitializationUtil {
45   private static final Logger LOG = Logger.getInstance(CompletionInitializationUtil.class);
46
47   public static CompletionInitializationContextImpl createCompletionInitializationContext(@NotNull Project project,
48                                                                                           @NotNull Editor editor,
49                                                                                           @NotNull Caret caret,
50                                                                                           int invocationCount,
51                                                                                           CompletionType completionType) {
52     return WriteAction.compute(() -> {
53       PsiDocumentManager.getInstance(project).commitAllDocuments();
54       CompletionAssertions.checkEditorValid(editor);
55
56       final PsiFile psiFile = PsiUtilBase.getPsiFileInEditor(editor, project);
57       assert psiFile != null : "no PSI file: " + FileDocumentManager.getInstance().getFile(editor.getDocument());
58       psiFile.putUserData(PsiFileEx.BATCH_REFERENCE_PROCESSING, Boolean.TRUE);
59       CompletionAssertions.assertCommitSuccessful(editor, psiFile);
60
61       return runContributorsBeforeCompletion(editor, psiFile, invocationCount, caret, completionType);
62     });
63   }
64
65   private static CompletionInitializationContextImpl runContributorsBeforeCompletion(Editor editor,
66                                                                                      PsiFile psiFile,
67                                                                                      int invocationCount,
68                                                                                      @NotNull Caret caret,
69                                                                                      CompletionType completionType) {
70     final Ref<CompletionContributor> current = Ref.create(null);
71     CompletionInitializationContextImpl context =
72       new CompletionInitializationContextImpl(editor, caret, psiFile, completionType, invocationCount) {
73         CompletionContributor dummyIdentifierChanger;
74
75         @Override
76         public void setDummyIdentifier(@NotNull String dummyIdentifier) {
77           super.setDummyIdentifier(dummyIdentifier);
78
79           if (dummyIdentifierChanger != null) {
80             LOG.error("Changing the dummy identifier twice, already changed by " + dummyIdentifierChanger);
81           }
82           dummyIdentifierChanger = current.get();
83         }
84       };
85     Project project = psiFile.getProject();
86     FileBasedIndex.getInstance().ignoreDumbMode(() -> {
87       for (final CompletionContributor contributor : CompletionContributor.forLanguageHonorDumbness(context.getPositionLanguage(), project)) {
88         current.set(contributor);
89         contributor.beforeCompletion(context);
90         CompletionAssertions.checkEditorValid(editor);
91         assert !PsiDocumentManager.getInstance(project).isUncommited(editor.getDocument()) : "Contributor " +
92                                                                                              contributor +
93                                                                                              " left the document uncommitted";
94       }
95     }, DumbModeAccessType.RELIABLE_DATA_ONLY);
96     return context;
97   }
98
99   @NotNull
100   public static CompletionParameters createCompletionParameters(CompletionInitializationContext initContext,
101                                                                 CompletionProcess indicator, OffsetsInFile finalOffsets) {
102     int offset = finalOffsets.getOffsets().getOffset(CompletionInitializationContext.START_OFFSET);
103     PsiFile fileCopy = finalOffsets.getFile();
104     PsiFile originalFile = fileCopy.getOriginalFile();
105     PsiElement insertedElement = findCompletionPositionLeaf(finalOffsets, offset, originalFile);
106     insertedElement.putUserData(CompletionContext.COMPLETION_CONTEXT_KEY, new CompletionContext(fileCopy, finalOffsets.getOffsets()));
107     return new CompletionParameters(insertedElement, originalFile, initContext.getCompletionType(), offset,
108                                     initContext.getInvocationCount(),
109                                     initContext.getEditor(), indicator);
110   }
111
112   public static Supplier<OffsetsInFile> insertDummyIdentifier(CompletionInitializationContext initContext, CompletionProcessEx indicator) {
113     OffsetsInFile topLevelOffsets = indicator.getHostOffsets();
114     final Consumer<Supplier<Disposable>> registerDisposable = supplier -> indicator.registerChildDisposable(supplier);
115
116     return doInsertDummyIdentifier(initContext, topLevelOffsets, registerDisposable);
117   }
118
119   //need for code with me
120   public static Supplier<OffsetsInFile> insertDummyIdentifier(CompletionInitializationContext initContext, OffsetsInFile topLevelOffsets, Disposable parentDisposable) {
121     final Consumer<Supplier<Disposable>> registerDisposable = supplier -> Disposer.register(parentDisposable, supplier.get());
122
123     return doInsertDummyIdentifier(initContext, topLevelOffsets, registerDisposable);
124   }
125
126   private static Supplier<OffsetsInFile> doInsertDummyIdentifier(CompletionInitializationContext initContext,
127                                                                  OffsetsInFile topLevelOffsets,
128                                                                  Consumer<Supplier<Disposable>> registerDisposable) {
129
130     CompletionAssertions.checkEditorValid(initContext.getEditor());
131     if (initContext.getDummyIdentifier().isEmpty()) {
132       return () -> topLevelOffsets;
133     }
134
135     Editor editor = initContext.getEditor();
136     Editor hostEditor = editor instanceof EditorWindow ? ((EditorWindow)editor).getDelegate() : editor;
137     OffsetMap hostMap = topLevelOffsets.getOffsets();
138
139     PsiFile hostCopy = obtainFileCopy(topLevelOffsets.getFile());
140     Document copyDocument = Objects.requireNonNull(hostCopy.getViewProvider().getDocument());
141
142     String dummyIdentifier = initContext.getDummyIdentifier();
143     int startOffset = hostMap.getOffset(CompletionInitializationContext.START_OFFSET);
144     int endOffset = hostMap.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET);
145
146     Supplier<OffsetsInFile> apply = topLevelOffsets.replaceInCopy(hostCopy, startOffset, endOffset, dummyIdentifier);
147
148     // despite being non-physical, the copy file should only be modified in a write action,
149     // because it's reused in multiple completions and it can also escapes uncontrollably into other threads (e.g. quick doc)
150     return () -> WriteAction.compute(() -> {
151       registerDisposable.accept(() -> new OffsetTranslator(hostEditor.getDocument(), initContext.getFile(), copyDocument, startOffset, endOffset, dummyIdentifier));
152       OffsetsInFile copyOffsets = apply.get();
153
154       registerDisposable.accept(() -> copyOffsets.getOffsets());
155
156       return copyOffsets;
157     });
158   }
159
160   public static OffsetsInFile toInjectedIfAny(PsiFile originalFile, OffsetsInFile hostCopyOffsets) {
161     CompletionAssertions.assertHostInfo(hostCopyOffsets.getFile(), hostCopyOffsets.getOffsets());
162
163     int hostStartOffset = hostCopyOffsets.getOffsets().getOffset(CompletionInitializationContext.START_OFFSET);
164     OffsetsInFile translatedOffsets = hostCopyOffsets.toInjectedIfAny(hostStartOffset);
165     if (translatedOffsets != hostCopyOffsets) {
166       PsiFile injected = translatedOffsets.getFile();
167       if (originalFile != injected &&
168           injected instanceof PsiFileImpl &&
169           InjectedLanguageManager.getInstance(originalFile.getProject()).isInjectedFragment(originalFile)) {
170         setOriginalFile((PsiFileImpl)injected, originalFile);
171       }
172       VirtualFile virtualFile = injected.getVirtualFile();
173       DocumentWindow documentWindow = null;
174       if (virtualFile instanceof VirtualFileWindow) {
175         documentWindow = ((VirtualFileWindow)virtualFile).getDocumentWindow();
176       }
177       CompletionAssertions.assertInjectedOffsets(hostStartOffset, injected, documentWindow);
178
179       if (injected.getTextRange().contains(translatedOffsets.getOffsets().getOffset(CompletionInitializationContext.START_OFFSET))) {
180         return translatedOffsets;
181       }
182     }
183
184     return hostCopyOffsets;
185   }
186
187   private static void setOriginalFile(PsiFileImpl copy, PsiFile origin) {
188     PsiFile currentOrigin = copy.getOriginalFile();
189     if (currentOrigin == copy) {
190       copy.setOriginalFile(origin);
191     } else {
192       PsiUtilCore.ensureValid(currentOrigin);
193       if (currentOrigin != origin) {
194         LOG.error(currentOrigin + " != " + origin + "\n" + currentOrigin.getViewProvider() + " != " + origin.getViewProvider());
195       }
196     }
197   }
198
199   @NotNull
200   private static PsiElement findCompletionPositionLeaf(OffsetsInFile offsets, int offset, PsiFile originalFile) {
201     PsiElement insertedElement = offsets.getFile().findElementAt(offset);
202     if (insertedElement == null && offsets.getFile().getTextLength() == offset) {
203       insertedElement = PsiTreeUtil.getDeepestLast(offsets.getFile());
204     }
205     CompletionAssertions.assertCompletionPositionPsiConsistent(offsets, offset, originalFile, insertedElement);
206     return insertedElement;
207   }
208
209   private static PsiFile obtainFileCopy(PsiFile file) {
210     final VirtualFile virtualFile = file.getVirtualFile();
211     boolean mayCacheCopy = file.isPhysical() &&
212                            // we don't want to cache code fragment copies even if they appear to be physical
213                            virtualFile != null && virtualFile.isInLocalFileSystem();
214     if (mayCacheCopy) {
215       final Pair<PsiFile, Document> cached = SoftReference.dereference(file.getUserData(FILE_COPY_KEY));
216       if (cached != null && isCopyUpToDate(cached.second, cached.first, file)) {
217         PsiFile copy = cached.first;
218         CompletionAssertions.assertCorrectOriginalFile("Cached", file, copy);
219         return copy;
220       }
221     }
222
223     final PsiFile copy = (PsiFile)file.copy();
224     if (copy.isPhysical() || copy.getViewProvider().isEventSystemEnabled()) {
225       LOG.error("File copy should be non-physical and non-event-system-enabled! Language=" +
226                 file.getLanguage() +
227                 "; file=" +
228                 file +
229                 " of " +
230                 file.getClass());
231     }
232     CompletionAssertions.assertCorrectOriginalFile("New", file, copy);
233
234     if (mayCacheCopy) {
235       final Document document = copy.getViewProvider().getDocument();
236       assert document != null;
237       syncAcceptSlashR(file.getViewProvider().getDocument(), document);
238       file.putUserData(FILE_COPY_KEY, new SoftReference<>(Pair.create(copy, document)));
239     }
240     return copy;
241   }
242
243   private static final Key<SoftReference<Pair<PsiFile, Document>>> FILE_COPY_KEY = Key.create("CompletionFileCopy");
244
245   private static boolean isCopyUpToDate(Document document, @NotNull PsiFile copyFile, @NotNull PsiFile originalFile) {
246     if (!copyFile.getClass().equals(originalFile.getClass()) ||
247         !copyFile.isValid() ||
248         !copyFile.getName().equals(originalFile.getName())) {
249       return false;
250     }
251     // the psi file cache might have been cleared by some external activity,
252     // in which case PSI-document sync may stop working
253     PsiFile current = PsiDocumentManager.getInstance(copyFile.getProject()).getPsiFile(document);
254     return current != null && current.getViewProvider().getPsi(copyFile.getLanguage()) == copyFile;
255   }
256
257   private static void syncAcceptSlashR(Document originalDocument, Document documentCopy) {
258     if (!(originalDocument instanceof DocumentImpl) || !(documentCopy instanceof DocumentImpl)) {
259       return;
260     }
261
262     ((DocumentImpl)documentCopy).setAcceptSlashR(((DocumentImpl)originalDocument).acceptsSlashR());
263   }
264 }