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