fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / template / impl / TemplateManagerImpl.java
1 // Copyright 2000-2019 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.template.impl;
3
4 import com.intellij.codeInsight.CodeInsightBundle;
5 import com.intellij.codeInsight.completion.CompletionUtil;
6 import com.intellij.codeInsight.completion.OffsetKey;
7 import com.intellij.codeInsight.completion.OffsetsInFile;
8 import com.intellij.codeInsight.template.*;
9 import com.intellij.lang.Language;
10 import com.intellij.openapi.Disposable;
11 import com.intellij.openapi.application.ApplicationManager;
12 import com.intellij.openapi.command.CommandProcessor;
13 import com.intellij.openapi.editor.*;
14 import com.intellij.openapi.editor.event.EditorFactoryEvent;
15 import com.intellij.openapi.editor.event.EditorFactoryListener;
16 import com.intellij.openapi.extensions.ExtensionPoint;
17 import com.intellij.openapi.project.Project;
18 import com.intellij.openapi.util.*;
19 import com.intellij.psi.PsiCompiledElement;
20 import com.intellij.psi.PsiDocumentManager;
21 import com.intellij.psi.PsiFile;
22 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
23 import com.intellij.psi.util.CachedValueProvider;
24 import com.intellij.psi.util.CachedValuesManager;
25 import com.intellij.psi.util.PsiUtilBase;
26 import com.intellij.psi.util.PsiUtilCore;
27 import com.intellij.testFramework.TestModeFlags;
28 import com.intellij.util.PairProcessor;
29 import com.intellij.util.containers.ConcurrentFactoryMap;
30 import com.intellij.util.containers.ContainerUtil;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33 import org.jetbrains.annotations.TestOnly;
34
35 import java.util.*;
36 import java.util.concurrent.ConcurrentMap;
37
38 public class TemplateManagerImpl extends TemplateManager implements Disposable {
39   // called a lot of times on save/load, so, better to use ExtensionPoint instead of name
40   static final NotNullLazyValue<ExtensionPoint<TemplateContextType>> TEMPLATE_CONTEXT_EP =
41     NotNullLazyValue.createValue(() -> TemplateContextType.EP_NAME.getPoint(null));
42
43   private final Project myProject;
44   private static final Key<Boolean> ourTemplateTesting = Key.create("TemplateTesting");
45
46   private static final Key<TemplateState> TEMPLATE_STATE_KEY = Key.create("TEMPLATE_STATE_KEY");
47   private final TemplateManagerListener myEventPublisher;
48
49   public TemplateManagerImpl(@NotNull Project project) {
50     myProject = project;
51     myEventPublisher = project.getMessageBus().syncPublisher(TEMPLATE_STARTED_TOPIC);
52     EditorFactoryListener myEditorFactoryListener = new EditorFactoryListener() {
53       @Override
54       public void editorReleased(@NotNull EditorFactoryEvent event) {
55         Editor editor = event.getEditor();
56         if (editor.getProject() != null && editor.getProject() != myProject) return;
57         if (myProject.isDisposed() || !myProject.isOpen()) return;
58         TemplateState state = getTemplateState(editor);
59         if (state != null) {
60           state.gotoEnd();
61         }
62         clearTemplateState(editor);
63       }
64     };
65     EditorFactory.getInstance().addEditorFactoryListener(myEditorFactoryListener, myProject);
66   }
67
68   @Override
69   public void dispose() {
70   }
71
72   /**
73    * @deprecated use {@link #setTemplateTesting(Disposable)}
74    */
75   @TestOnly
76   @Deprecated
77   public static void setTemplateTesting(Project project, Disposable parentDisposable) {
78     setTemplateTesting(parentDisposable);
79   }
80
81   @TestOnly
82   public static void setTemplateTesting(Disposable parentDisposable) {
83     TestModeFlags.set(ourTemplateTesting, true, parentDisposable);
84   }
85
86   @Override
87   public Template createTemplate(@NotNull String key, String group) {
88     return new TemplateImpl(key, group);
89   }
90
91   @Override
92   public Template createTemplate(@NotNull String key, String group, String text) {
93     return new TemplateImpl(key, text, group);
94   }
95
96   @Nullable
97   public static TemplateState getTemplateState(@NotNull Editor editor) {
98     UserDataHolder stateHolder = InjectedLanguageUtil.getTopLevelEditor(editor);
99     TemplateState templateState = stateHolder.getUserData(TEMPLATE_STATE_KEY);
100     if (templateState != null && templateState.isDisposed()) {
101       stateHolder.putUserData(TEMPLATE_STATE_KEY, null);
102       return null;
103     }
104     return templateState;
105   }
106
107   static void clearTemplateState(@NotNull Editor editor) {
108     TemplateState prevState = getTemplateState(editor);
109     if (prevState != null) {
110       Editor stateEditor = prevState.getEditor();
111       if (stateEditor != null) {
112         stateEditor.putUserData(TEMPLATE_STATE_KEY, null);
113       }
114       Disposer.dispose(prevState);
115     }
116   }
117
118   @NotNull
119   private TemplateState initTemplateState(@NotNull Editor editor) {
120     Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(editor);
121     clearTemplateState(topLevelEditor);
122     TemplateState state = new TemplateState(myProject, topLevelEditor);
123     Disposer.register(this, state);
124     topLevelEditor.putUserData(TEMPLATE_STATE_KEY, state);
125     return state;
126   }
127
128   @Override
129   public boolean startTemplate(@NotNull Editor editor, char shortcutChar) {
130     Runnable runnable = prepareTemplate(editor, shortcutChar, null);
131     if (runnable != null) {
132       PsiDocumentManager.getInstance(myProject).commitDocument(editor.getDocument());
133       runnable.run();
134     }
135     return runnable != null;
136   }
137
138   @Override
139   public void startTemplate(@NotNull final Editor editor, @NotNull Template template) {
140     startTemplate(editor, template, null);
141   }
142
143   @Override
144   public void startTemplate(@NotNull Editor editor, String selectionString, @NotNull Template template) {
145     startTemplate(editor, selectionString, template, true, null, null, null);
146   }
147
148   @Override
149   public void startTemplate(@NotNull Editor editor,
150                             @NotNull Template template,
151                             TemplateEditingListener listener,
152                             final PairProcessor<? super String, ? super String> processor) {
153     startTemplate(editor, null, template, true, listener, processor, null);
154   }
155
156   private void startTemplate(final Editor editor,
157                              final String selectionString,
158                              final Template template,
159                              boolean inSeparateCommand,
160                              TemplateEditingListener listener,
161                              final PairProcessor<? super String, ? super String> processor,
162                              final Map<String, String> predefinedVarValues) {
163     final TemplateState templateState = initTemplateState(editor);
164
165     //noinspection unchecked
166     templateState.getProperties().put(ExpressionContext.SELECTION, selectionString);
167
168     if (listener != null) {
169       templateState.addTemplateStateListener(listener);
170     }
171     Runnable r = () -> {
172       if (selectionString != null) {
173         ApplicationManager.getApplication().runWriteAction(() -> EditorModificationUtil.deleteSelectedText(editor));
174       }
175       else {
176         editor.getSelectionModel().removeSelection();
177       }
178       templateState.start((TemplateImpl)template, processor, predefinedVarValues);
179       myEventPublisher.templateStarted(templateState);
180     };
181     if (inSeparateCommand) {
182       CommandProcessor.getInstance().executeCommand(myProject, r, CodeInsightBundle.message("insert.code.template.command"), null);
183     }
184     else {
185       r.run();
186     }
187
188     if (shouldSkipInTests()) {
189       if (!templateState.isFinished()) templateState.gotoEnd(false);
190     }
191   }
192
193   public boolean shouldSkipInTests() {
194     return ApplicationManager.getApplication().isUnitTestMode() && !TestModeFlags.is(ourTemplateTesting);
195   }
196
197   @Override
198   public void startTemplate(@NotNull final Editor editor, @NotNull final Template template, TemplateEditingListener listener) {
199     startTemplate(editor, null, template, true, listener, null, null);
200   }
201
202   @Override
203   public void startTemplate(@NotNull final Editor editor,
204                             @NotNull final Template template,
205                             boolean inSeparateCommand,
206                             Map<String, String> predefinedVarValues,
207                             TemplateEditingListener listener) {
208     startTemplate(editor, null, template, inSeparateCommand, listener, null, predefinedVarValues);
209   }
210
211   private static int passArgumentBack(CharSequence text, int caretOffset) {
212     int i = caretOffset - 1;
213     for (; i >= 0; i--) {
214       char c = text.charAt(i);
215       if (isDelimiter(c)) {
216         break;
217       }
218     }
219     return i + 1;
220   }
221
222   private static boolean isDelimiter(char c) {
223     return !Character.isJavaIdentifierPart(c);
224   }
225
226   private static <T, U> void addToMap(@NotNull Map<T, U> map, @NotNull Collection<? extends T> keys, U value) {
227     for (T key : keys) {
228       map.put(key, value);
229     }
230   }
231
232   private static boolean containsTemplateStartingBefore(Map<TemplateImpl, String> template2argument,
233                                                         int offset,
234                                                         int caretOffset,
235                                                         CharSequence text) {
236     for (TemplateImpl template : template2argument.keySet()) {
237       String argument = template2argument.get(template);
238       int templateStart = getTemplateStart(template, argument, caretOffset, text);
239       if (templateStart < offset) {
240         return true;
241       }
242     }
243     return false;
244   }
245
246   @Nullable
247   public Runnable prepareTemplate(final Editor editor, char shortcutChar, @Nullable final PairProcessor<? super String, ? super String> processor) {
248     if (editor.getSelectionModel().hasSelection()) {
249       return null;
250     }
251
252     PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, myProject);
253     if (file == null || file instanceof PsiCompiledElement) return null;
254
255     Map<TemplateImpl, String> template2argument = findMatchingTemplates(file, editor, shortcutChar, TemplateSettings.getInstance());
256     List<CustomLiveTemplate> customCandidates = ContainerUtil.findAll(CustomLiveTemplate.EP_NAME.getExtensions(), customLiveTemplate ->
257       shortcutChar == customLiveTemplate.getShortcut() &&
258       (editor.getCaretModel().getCaretCount() <= 1 || supportsMultiCaretMode(customLiveTemplate)) &&
259       isApplicable(customLiveTemplate, editor, file));
260     if (!customCandidates.isEmpty()) {
261       int caretOffset = editor.getCaretModel().getOffset();
262       CustomTemplateCallback templateCallback = new CustomTemplateCallback(editor, file);
263       for (CustomLiveTemplate customLiveTemplate : customCandidates) {
264         String key = customLiveTemplate.computeTemplateKey(templateCallback);
265         if (key != null) {
266           int offsetBeforeKey = caretOffset - key.length();
267           CharSequence text = editor.getDocument().getImmutableCharSequence();
268           if (template2argument == null || !containsTemplateStartingBefore(template2argument, offsetBeforeKey, caretOffset, text)) {
269             return () -> customLiveTemplate.expand(key, templateCallback);
270           }
271         }
272       }
273     }
274
275     return startNonCustomTemplates(template2argument, editor, processor);
276   }
277
278   private static boolean supportsMultiCaretMode(CustomLiveTemplate customLiveTemplate) {
279     return !(customLiveTemplate instanceof CustomLiveTemplateBase) || ((CustomLiveTemplateBase)customLiveTemplate).supportsMultiCaret();
280   }
281
282   public static boolean isApplicable(@NotNull CustomLiveTemplate customLiveTemplate,
283                                      @NotNull Editor editor,
284                                      @NotNull PsiFile file) {
285     return isApplicable(customLiveTemplate, editor, file, false);
286   }
287
288   public static boolean isApplicable(@NotNull CustomLiveTemplate customLiveTemplate,
289                                      @NotNull Editor editor,
290                                      @NotNull PsiFile file, boolean wrapping) {
291     CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
292     return customLiveTemplate.isApplicable(callback, callback.getOffset(), wrapping);
293   }
294
295   private static int getArgumentOffset(int caretOffset, String argument, CharSequence text) {
296     int argumentOffset = caretOffset - argument.length();
297     if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
298       if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
299         argumentOffset--;
300       }
301     }
302     return argumentOffset;
303   }
304
305   private static int getTemplateStart(TemplateImpl template, String argument, int caretOffset, CharSequence text) {
306     int templateStart;
307     if (argument == null) {
308       templateStart = caretOffset - template.getKey().length();
309     }
310     else {
311       int argOffset = getArgumentOffset(caretOffset, argument, text);
312       templateStart = argOffset - template.getKey().length();
313     }
314     return templateStart;
315   }
316
317   public Map<TemplateImpl, String> findMatchingTemplates(final PsiFile file,
318                                                           Editor editor,
319                                                           @Nullable Character shortcutChar,
320                                                           TemplateSettings templateSettings) {
321     final Document document = editor.getDocument();
322     CharSequence text = document.getCharsSequence();
323     final int caretOffset = editor.getCaretModel().getOffset();
324
325     List<TemplateImpl> candidatesWithoutArgument = findMatchingTemplates(text, caretOffset, shortcutChar, templateSettings, false);
326
327     int argumentOffset = passArgumentBack(text, caretOffset);
328     String argument = null;
329     if (argumentOffset >= 0) {
330       argument = text.subSequence(argumentOffset, caretOffset).toString();
331       if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
332         if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
333           argumentOffset--;
334         }
335       }
336     }
337     List<TemplateImpl> candidatesWithArgument = findMatchingTemplates(text, argumentOffset, shortcutChar, templateSettings, true);
338
339     if (candidatesWithArgument.isEmpty() && candidatesWithoutArgument.isEmpty()) {
340       return null;
341     }
342
343     candidatesWithoutArgument = filterApplicableCandidates(file, caretOffset, candidatesWithoutArgument);
344     candidatesWithArgument = filterApplicableCandidates(file, argumentOffset, candidatesWithArgument);
345     Map<TemplateImpl, String> candidate2Argument = new HashMap<>();
346     addToMap(candidate2Argument, candidatesWithoutArgument, null);
347     addToMap(candidate2Argument, candidatesWithArgument, argument);
348     return candidate2Argument;
349   }
350
351   @Nullable
352   public Runnable startNonCustomTemplates(final Map<TemplateImpl, String> template2argument,
353                                           final Editor editor,
354                                           @Nullable final PairProcessor<? super String, ? super String> processor) {
355     final int caretOffset = editor.getCaretModel().getOffset();
356     final Document document = editor.getDocument();
357     final CharSequence text = document.getCharsSequence();
358
359     if (template2argument == null || template2argument.isEmpty()) {
360       return null;
361     }
362
363     return () -> {
364       if (template2argument.size() == 1) {
365         TemplateImpl template = template2argument.keySet().iterator().next();
366         String argument = template2argument.get(template);
367         int templateStart = getTemplateStart(template, argument, caretOffset, text);
368         startTemplateWithPrefix(editor, template, templateStart, processor, argument);
369       }
370       else {
371         ListTemplatesHandler.showTemplatesLookup(myProject, editor, template2argument);
372       }
373     };
374   }
375
376   private static List<TemplateImpl> findMatchingTemplates(CharSequence text,
377                                                           int caretOffset,
378                                                           @Nullable Character shortcutChar,
379                                                           TemplateSettings settings,
380                                                           boolean hasArgument) {
381     List<TemplateImpl> candidates = Collections.emptyList();
382     for (int i = settings.getMaxKeyLength(); i >= 1; i--) {
383       int wordStart = caretOffset - i;
384       if (wordStart < 0) {
385         continue;
386       }
387       String key = text.subSequence(wordStart, caretOffset).toString();
388       if (Character.isJavaIdentifierStart(key.charAt(0))) {
389         if (wordStart > 0 && Character.isJavaIdentifierPart(text.charAt(wordStart - 1))) {
390           continue;
391         }
392       }
393
394       candidates = settings.collectMatchingCandidates(key, shortcutChar, hasArgument);
395       if (!candidates.isEmpty()) break;
396     }
397     return candidates;
398   }
399
400   public void startTemplateWithPrefix(final Editor editor,
401                                       final TemplateImpl template,
402                                       @Nullable final PairProcessor<? super String, ? super String> processor,
403                                       @Nullable String argument) {
404     final int caretOffset = editor.getCaretModel().getOffset();
405     String key = template.getKey();
406     int startOffset = caretOffset - key.length();
407     if (argument != null) {
408       if (!isDelimiter(key.charAt(key.length() - 1))) {
409         // pass space
410         startOffset--;
411       }
412       startOffset -= argument.length();
413     }
414     startTemplateWithPrefix(editor, template, startOffset, processor, argument);
415   }
416
417   public void startTemplateWithPrefix(final Editor editor,
418                                       final TemplateImpl template,
419                                       final int templateStart,
420                                       @Nullable final PairProcessor<? super String, ? super String> processor,
421                                       @Nullable final String argument) {
422     final int caretOffset = editor.getCaretModel().getOffset();
423     final TemplateState templateState = initTemplateState(editor);
424     CommandProcessor commandProcessor = CommandProcessor.getInstance();
425     commandProcessor.executeCommand(myProject, () -> {
426       editor.getDocument().deleteString(templateStart, caretOffset);
427       editor.getCaretModel().moveToOffset(templateStart);
428       editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
429       editor.getSelectionModel().removeSelection();
430       Map<String, String> predefinedVarValues = null;
431       if (argument != null) {
432         predefinedVarValues = new HashMap<>();
433         predefinedVarValues.put(TemplateImpl.ARG, argument);
434       }
435       templateState.start(template, processor, predefinedVarValues);
436       myEventPublisher.templateStarted(templateState);
437     }, CodeInsightBundle.message("insert.code.template.command"), null);
438   }
439
440   private static List<TemplateImpl> filterApplicableCandidates(PsiFile file, int caretOffset, List<TemplateImpl> candidates) {
441     if (candidates.isEmpty()) {
442       return candidates;
443     }
444
445     PsiFile copy = insertDummyIdentifierWithCache(file, caretOffset, caretOffset, CompletionUtil.DUMMY_IDENTIFIER_TRIMMED).getFile();
446
447     List<TemplateImpl> result = new ArrayList<>();
448     for (TemplateImpl candidate : candidates) {
449       if (isApplicable(copy, caretOffset - candidate.getKey().length(), candidate)) {
450         result.add(candidate);
451       }
452     }
453     return result;
454   }
455
456   private static List<TemplateContextType> getBases(TemplateContextType type) {
457     ArrayList<TemplateContextType> list = new ArrayList<>();
458     while (true) {
459       type = type.getBaseContextType();
460       if (type == null) return list;
461       list.add(type);
462     }
463   }
464
465   private static Set<TemplateContextType> getDirectlyApplicableContextTypes(@NotNull PsiFile file, int offset) {
466     LinkedHashSet<TemplateContextType> set = new LinkedHashSet<>();
467     LinkedList<TemplateContextType> contexts = buildOrderedContextTypes();
468     for (TemplateContextType contextType : contexts) {
469       if (contextType.isInContext(file, offset)) {
470         set.add(contextType);
471       }
472     }
473
474     removeBases:
475     while (true) {
476       for (TemplateContextType type : set) {
477         if (set.removeAll(getBases(type))) {
478           continue removeBases;
479         }
480       }
481
482       return set;
483     }
484   }
485
486   private static LinkedList<TemplateContextType> buildOrderedContextTypes() {
487     LinkedList<TemplateContextType> userDefinedExtensionsFirst = new LinkedList<>();
488     for (TemplateContextType contextType : getAllContextTypes()) {
489       if (contextType.getClass().getName().startsWith(Template.class.getPackage().getName())) {
490         userDefinedExtensionsFirst.addLast(contextType);
491       }
492       else {
493         userDefinedExtensionsFirst.addFirst(contextType);
494       }
495     }
496     return userDefinedExtensionsFirst;
497   }
498
499   @NotNull
500   public static List<TemplateContextType> getAllContextTypes() {
501     return TEMPLATE_CONTEXT_EP.getValue().getExtensionList();
502   }
503
504   @Override
505   @Nullable
506   public Template getActiveTemplate(@NotNull Editor editor) {
507     final TemplateState templateState = getTemplateState(editor);
508     return templateState != null ? templateState.getTemplate() : null;
509   }
510
511   @Override
512   public boolean finishTemplate(@NotNull Editor editor) {
513     TemplateState state = getTemplateState(editor);
514     if (state != null) {
515       state.gotoEnd();
516       return true;
517     }
518     return false;
519   }
520
521   public static boolean isApplicable(PsiFile file, int offset, TemplateImpl template) {
522     return isApplicable(template, getApplicableContextTypes(file, offset));
523   }
524
525   public static boolean isApplicable(TemplateImpl template, Set<? extends TemplateContextType> contextTypes) {
526     for (TemplateContextType type : contextTypes) {
527       if (template.getTemplateContext().isEnabled(type)) {
528         return true;
529       }
530     }
531     return false;
532   }
533
534   public static List<TemplateImpl> listApplicableTemplates(PsiFile file, int offset, boolean selectionOnly) {
535     Set<TemplateContextType> contextTypes = getApplicableContextTypes(file, offset);
536
537     final ArrayList<TemplateImpl> result = new ArrayList<>();
538     for (final TemplateImpl template : TemplateSettings.getInstance().getTemplates()) {
539       if (!template.isDeactivated() && (!selectionOnly || template.isSelectionTemplate()) && isApplicable(template, contextTypes)) {
540         result.add(template);
541       }
542     }
543     return result;
544   }
545
546   public static List<TemplateImpl> listApplicableTemplateWithInsertingDummyIdentifier(Editor editor, PsiFile file, boolean selectionOnly) {
547     int startOffset = editor.getSelectionModel().getSelectionStart();
548     int endOffset = editor.getSelectionModel().getSelectionEnd();
549     OffsetsInFile offsets = insertDummyIdentifierWithCache(file, startOffset, endOffset, CompletionUtil.DUMMY_IDENTIFIER_TRIMMED);
550     return listApplicableTemplates(offsets.getFile(), getStartOffset(offsets), selectionOnly);
551   }
552
553   public static List<CustomLiveTemplate> listApplicableCustomTemplates(@NotNull Editor editor, @NotNull PsiFile file, boolean selectionOnly) {
554     List<CustomLiveTemplate> result = new ArrayList<>();
555     for (CustomLiveTemplate template : CustomLiveTemplate.EP_NAME.getExtensions()) {
556       if ((!selectionOnly || template.supportsWrapping()) && isApplicable(template, editor, file, selectionOnly)) {
557         result.add(template);
558       }
559     }
560     return result;
561   }
562
563   public static Set<TemplateContextType> getApplicableContextTypes(PsiFile file, int offset) {
564     Set<TemplateContextType> result = getDirectlyApplicableContextTypes(file, offset);
565
566     Language baseLanguage = file.getViewProvider().getBaseLanguage();
567     if (baseLanguage != file.getLanguage()) {
568       PsiFile basePsi = file.getViewProvider().getPsi(baseLanguage);
569       if (basePsi != null) {
570         result.addAll(getDirectlyApplicableContextTypes(basePsi, offset));
571       }
572     }
573
574     // if we have, for example, a Ruby fragment in RHTML selected with its exact bounds, the file language and the base
575     // language will be ERb, so we won't match HTML templates for it. but they're actually valid
576     Language languageAtOffset = PsiUtilCore.getLanguageAtOffset(file, offset);
577     if (languageAtOffset != file.getLanguage() && languageAtOffset != baseLanguage) {
578       PsiFile basePsi = file.getViewProvider().getPsi(languageAtOffset);
579       if (basePsi != null) {
580         result.addAll(getDirectlyApplicableContextTypes(basePsi, offset));
581       }
582     }
583
584     return result;
585   }
586
587   private static final OffsetKey START_OFFSET = OffsetKey.create("start", false);
588   private static final OffsetKey END_OFFSET = OffsetKey.create("end", true);
589
590   private static int getStartOffset(OffsetsInFile offsets) {
591     return offsets.getOffsets().getOffset(START_OFFSET);
592   }
593
594   private static int getEndOffset(OffsetsInFile offsets) {
595     return offsets.getOffsets().getOffset(END_OFFSET);
596   }
597
598   private static OffsetsInFile insertDummyIdentifierWithCache(PsiFile file, int startOffset, int endOffset, String replacement) {
599     ProperTextRange editRange = ProperTextRange.create(startOffset, endOffset);
600     assertRangeWithinDocument(editRange, file.getViewProvider().getDocument());
601
602     ConcurrentMap<Pair<ProperTextRange, String>, OffsetsInFile> map = CachedValuesManager.getCachedValue(file, () ->
603       CachedValueProvider.Result.create(
604         ConcurrentFactoryMap.createMap(
605           key -> copyWithDummyIdentifier(new OffsetsInFile(file), key.first.getStartOffset(), key.first.getEndOffset(), key.second)),
606         file, file.getViewProvider().getDocument()));
607     return map.get(Pair.create(editRange, replacement));
608   }
609
610   private static void assertRangeWithinDocument(ProperTextRange editRange, Document document) {
611     TextRange docRange = TextRange.from(0, document.getTextLength());
612     assert docRange.contains(editRange) : docRange + " doesn't contain " + editRange;
613   }
614
615   @NotNull
616   public static OffsetsInFile copyWithDummyIdentifier(OffsetsInFile offsetMap, int startOffset, int endOffset, String replacement) {
617     offsetMap.getOffsets().addOffset(START_OFFSET, startOffset);
618     offsetMap.getOffsets().addOffset(END_OFFSET, endOffset);
619
620     Document document = offsetMap.getFile().getViewProvider().getDocument();
621     assert document != null;
622     if (replacement.isEmpty() &&
623         startOffset == endOffset &&
624         PsiDocumentManager.getInstance(offsetMap.getFile().getProject()).isCommitted(document)) {
625       return offsetMap;
626     }
627
628     OffsetsInFile hostOffsets = offsetMap.toTopLevelFile();
629     OffsetsInFile hostCopy = hostOffsets.copyWithReplacement(getStartOffset(hostOffsets), getEndOffset(hostOffsets), replacement);
630     return hostCopy.toInjectedIfAny(getStartOffset(hostCopy));
631   }
632 }