/*
* Copyright 2000-2016 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.template.impl;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.codeInsight.completion.OffsetKey;
import com.intellij.codeInsight.completion.OffsetsInFile;
import com.intellij.codeInsight.template.*;
import com.intellij.lang.Language;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.event.EditorFactoryAdapter;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.editor.event.EditorFactoryListener;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.PairProcessor;
import com.intellij.util.containers.ConcurrentFactoryMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
public class TemplateManagerImpl extends TemplateManager implements Disposable {
private static final TemplateContextType[] ourContextTypes = Extensions.getExtensions(TemplateContextType.EP_NAME);
private final Project myProject;
private boolean myTemplateTesting;
private static final Key<TemplateState> TEMPLATE_STATE_KEY = Key.create("TEMPLATE_STATE_KEY");
public TemplateManagerImpl(Project project) {
myProject = project;
final EditorFactoryListener myEditorFactoryListener = new EditorFactoryAdapter() {
@Override
public void editorReleased(@NotNull EditorFactoryEvent event) {
Editor editor = event.getEditor();
if (editor.getProject() != null && editor.getProject() != myProject) return;
if (myProject.isDisposed() || !myProject.isOpen()) return;
TemplateState state = getTemplateState(editor);
if (state != null) {
state.gotoEnd();
}
clearTemplateState(editor);
}
};
EditorFactory.getInstance().addEditorFactoryListener(myEditorFactoryListener, myProject);
}
@Override
public void dispose() {
}
/**
* @deprecated Use {@link #setTemplateTesting(Project, Disposable)} instead
*/
@TestOnly
@Deprecated
public void setTemplateTesting(final boolean templateTesting) {
myTemplateTesting = templateTesting;
}
@TestOnly
public static void setTemplateTesting(Project project, Disposable parentDisposable) {
final TemplateManagerImpl instance = (TemplateManagerImpl)getInstance(project);
instance.myTemplateTesting = true;
Disposer.register(parentDisposable, () -> instance.myTemplateTesting = false);
}
private static void disposeState(@NotNull TemplateState state) {
Disposer.dispose(state);
}
@Override
public Template createTemplate(@NotNull String key, String group) {
return new TemplateImpl(key, group);
}
@Override
public Template createTemplate(@NotNull String key, String group, String text) {
return new TemplateImpl(key, text, group);
}
@Nullable
public static TemplateState getTemplateState(@NotNull Editor editor) {
TemplateState templateState = editor.getUserData(TEMPLATE_STATE_KEY);
if (templateState != null && templateState.isDisposed()) {
editor.putUserData(TEMPLATE_STATE_KEY, null);
return null;
}
return templateState;
}
static void clearTemplateState(@NotNull Editor editor) {
TemplateState prevState = getTemplateState(editor);
if (prevState != null) {
disposeState(prevState);
}
editor.putUserData(TEMPLATE_STATE_KEY, null);
}
private TemplateState initTemplateState(@NotNull Editor editor) {
clearTemplateState(editor);
TemplateState state = new TemplateState(myProject, editor);
Disposer.register(this, state);
editor.putUserData(TEMPLATE_STATE_KEY, state);
return state;
}
@Override
public boolean startTemplate(@NotNull Editor editor, char shortcutChar) {
Runnable runnable = prepareTemplate(editor, shortcutChar, null);
if (runnable != null) {
PsiDocumentManager.getInstance(myProject).commitDocument(editor.getDocument());
runnable.run();
}
return runnable != null;
}
@Override
public void startTemplate(@NotNull final Editor editor, @NotNull Template template) {
startTemplate(editor, template, null);
}
@Override
public void startTemplate(@NotNull Editor editor, String selectionString, @NotNull Template template) {
startTemplate(editor, selectionString, template, true, null, null, null);
}
@Override
public void startTemplate(@NotNull Editor editor,
@NotNull Template template,
TemplateEditingListener listener,
final PairProcessor<String, String> processor) {
startTemplate(editor, null, template, true, listener, processor, null);
}
private void startTemplate(final Editor editor,
final String selectionString,
final Template template,
boolean inSeparateCommand,
TemplateEditingListener listener,
final PairProcessor<String, String> processor,
final Map<String, String> predefinedVarValues) {
final TemplateState templateState = initTemplateState(editor);
//noinspection unchecked
templateState.getProperties().put(ExpressionContext.SELECTION, selectionString);
if (listener != null) {
templateState.addTemplateStateListener(listener);
}
Runnable r = () -> {
if (selectionString != null) {
ApplicationManager.getApplication().runWriteAction(() -> EditorModificationUtil.deleteSelectedText(editor));
}
else {
editor.getSelectionModel().removeSelection();
}
templateState.start((TemplateImpl)template, processor, predefinedVarValues);
};
if (inSeparateCommand) {
CommandProcessor.getInstance().executeCommand(myProject, r, CodeInsightBundle.message("insert.code.template.command"), null);
}
else {
r.run();
}
if (shouldSkipInTests()) {
if (!templateState.isFinished()) templateState.gotoEnd(false);
}
}
public boolean shouldSkipInTests() {
return ApplicationManager.getApplication().isUnitTestMode() && !myTemplateTesting;
}
@Override
public void startTemplate(@NotNull final Editor editor, @NotNull final Template template, TemplateEditingListener listener) {
startTemplate(editor, null, template, true, listener, null, null);
}
@Override
public void startTemplate(@NotNull final Editor editor,
@NotNull final Template template,
boolean inSeparateCommand,
Map<String, String> predefinedVarValues,
TemplateEditingListener listener) {
startTemplate(editor, null, template, inSeparateCommand, listener, null, predefinedVarValues);
}
private static int passArgumentBack(CharSequence text, int caretOffset) {
int i = caretOffset - 1;
for (; i >= 0; i--) {
char c = text.charAt(i);
if (isDelimiter(c)) {
break;
}
}
return i + 1;
}
private static boolean isDelimiter(char c) {
return !Character.isJavaIdentifierPart(c);
}
private static <T, U> void addToMap(@NotNull Map<T, U> map, @NotNull Collection<? extends T> keys, U value) {
for (T key : keys) {
map.put(key, value);
}
}
private static boolean containsTemplateStartingBefore(Map<TemplateImpl, String> template2argument,
int offset,
int caretOffset,
CharSequence text) {
for (TemplateImpl template : template2argument.keySet()) {
String argument = template2argument.get(template);
int templateStart = getTemplateStart(template, argument, caretOffset, text);
if (templateStart < offset) {
return true;
}
}
return false;
}
@Nullable
public Runnable prepareTemplate(final Editor editor, char shortcutChar, @Nullable final PairProcessor<String, String> processor) {
if (editor.getSelectionModel().hasSelection()) {
return null;
}
PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, myProject);
if (file == null) return null;
Map<TemplateImpl, String> template2argument = findMatchingTemplates(file, editor, shortcutChar, TemplateSettings.getInstance());
List<CustomLiveTemplate> customCandidates = ContainerUtil.findAll(CustomLiveTemplate.EP_NAME.getExtensions(), customLiveTemplate ->
shortcutChar == customLiveTemplate.getShortcut() &&
(editor.getCaretModel().getCaretCount() <= 1 || supportsMultiCaretMode(customLiveTemplate)));
if (!customCandidates.isEmpty()) {
int caretOffset = editor.getCaretModel().getOffset();
PsiFile fileCopy = insertDummyIdentifierWithCache(file, caretOffset, caretOffset, "").getFile();
Document document = editor.getDocument();
for (final CustomLiveTemplate customLiveTemplate : customCandidates) {
if (isApplicable(customLiveTemplate, editor, fileCopy)) {
final String key = customLiveTemplate.computeTemplateKey(new CustomTemplateCallback(editor, fileCopy));
if (key != null) {
int offsetBeforeKey = caretOffset - key.length();
CharSequence text = document.getImmutableCharSequence();
if (template2argument == null || !containsTemplateStartingBefore(template2argument, offsetBeforeKey, caretOffset, text)) {
return () -> customLiveTemplate.expand(key, new CustomTemplateCallback(editor, file));
}
}
}
}
}
return startNonCustomTemplates(template2argument, editor, processor);
}
private static boolean supportsMultiCaretMode(CustomLiveTemplate customLiveTemplate) {
return !(customLiveTemplate instanceof CustomLiveTemplateBase) || ((CustomLiveTemplateBase)customLiveTemplate).supportsMultiCaret();
}
public static boolean isApplicable(@NotNull CustomLiveTemplate customLiveTemplate,
@NotNull Editor editor,
@NotNull PsiFile file) {
return isApplicable(customLiveTemplate, editor, file, false);
}
public static boolean isApplicable(@NotNull CustomLiveTemplate customLiveTemplate,
@NotNull Editor editor,
@NotNull PsiFile file, boolean wrapping) {
return customLiveTemplate.isApplicable(file, CustomTemplateCallback.getOffset(editor), wrapping);
}
private static int getArgumentOffset(int caretOffset, String argument, CharSequence text) {
int argumentOffset = caretOffset - argument.length();
if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
argumentOffset--;
}
}
return argumentOffset;
}
private static int getTemplateStart(TemplateImpl template, String argument, int caretOffset, CharSequence text) {
int templateStart;
if (argument == null) {
templateStart = caretOffset - template.getKey().length();
}
else {
int argOffset = getArgumentOffset(caretOffset, argument, text);
templateStart = argOffset - template.getKey().length();
}
return templateStart;
}
public Map<TemplateImpl, String> findMatchingTemplates(final PsiFile file,
Editor editor,
@Nullable Character shortcutChar,
TemplateSettings templateSettings) {
final Document document = editor.getDocument();
CharSequence text = document.getCharsSequence();
final int caretOffset = editor.getCaretModel().getOffset();
List<TemplateImpl> candidatesWithoutArgument = findMatchingTemplates(text, caretOffset, shortcutChar, templateSettings, false);
int argumentOffset = passArgumentBack(text, caretOffset);
String argument = null;
if (argumentOffset >= 0) {
argument = text.subSequence(argumentOffset, caretOffset).toString();
if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
argumentOffset--;
}
}
}
List<TemplateImpl> candidatesWithArgument = findMatchingTemplates(text, argumentOffset, shortcutChar, templateSettings, true);
if (candidatesWithArgument.isEmpty() && candidatesWithoutArgument.isEmpty()) {
return null;
}
candidatesWithoutArgument = filterApplicableCandidates(file, caretOffset, candidatesWithoutArgument);
candidatesWithArgument = filterApplicableCandidates(file, argumentOffset, candidatesWithArgument);
Map<TemplateImpl, String> candidate2Argument = new HashMap<>();
addToMap(candidate2Argument, candidatesWithoutArgument, null);
addToMap(candidate2Argument, candidatesWithArgument, argument);
return candidate2Argument;
}
@Nullable
public Runnable startNonCustomTemplates(final Map<TemplateImpl, String> template2argument,
final Editor editor,
@Nullable final PairProcessor<String, String> processor) {
final int caretOffset = editor.getCaretModel().getOffset();
final Document document = editor.getDocument();
final CharSequence text = document.getCharsSequence();
if (template2argument == null || template2argument.isEmpty()) {
return null;
}
return () -> {
if (template2argument.size() == 1) {
TemplateImpl template = template2argument.keySet().iterator().next();
String argument = template2argument.get(template);
int templateStart = getTemplateStart(template, argument, caretOffset, text);
startTemplateWithPrefix(editor, template, templateStart, processor, argument);
}
else {
ListTemplatesHandler.showTemplatesLookup(myProject, editor, template2argument);
}
};
}
private static List<TemplateImpl> findMatchingTemplates(CharSequence text,
int caretOffset,
@Nullable Character shortcutChar,
TemplateSettings settings,
boolean hasArgument) {
List<TemplateImpl> candidates = Collections.emptyList();
for (int i = settings.getMaxKeyLength(); i >= 1; i--) {
int wordStart = caretOffset - i;
if (wordStart < 0) {
continue;
}
String key = text.subSequence(wordStart, caretOffset).toString();
if (Character.isJavaIdentifierStart(key.charAt(0))) {
if (wordStart > 0 && Character.isJavaIdentifierPart(text.charAt(wordStart - 1))) {
continue;
}
}
candidates = settings.collectMatchingCandidates(key, shortcutChar, hasArgument);
if (!candidates.isEmpty()) break;
}
return candidates;
}
public void startTemplateWithPrefix(final Editor editor,
final TemplateImpl template,
@Nullable final PairProcessor<String, String> processor,
@Nullable String argument) {
final int caretOffset = editor.getCaretModel().getOffset();
String key = template.getKey();
int startOffset = caretOffset - key.length();
if (argument != null) {
if (!isDelimiter(key.charAt(key.length() - 1))) {
// pass space
startOffset--;
}
startOffset -= argument.length();
}
startTemplateWithPrefix(editor, template, startOffset, processor, argument);
}
public void startTemplateWithPrefix(final Editor editor,
final TemplateImpl template,
final int templateStart,
@Nullable final PairProcessor<String, String> processor,
@Nullable final String argument) {
final int caretOffset = editor.getCaretModel().getOffset();
final TemplateState templateState = initTemplateState(editor);
CommandProcessor commandProcessor = CommandProcessor.getInstance();
commandProcessor.executeCommand(myProject, () -> {
editor.getDocument().deleteString(templateStart, caretOffset);
editor.getCaretModel().moveToOffset(templateStart);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
Map<String, String> predefinedVarValues = null;
if (argument != null) {
predefinedVarValues = new HashMap<>();
predefinedVarValues.put(TemplateImpl.ARG, argument);
}
templateState.start(template, processor, predefinedVarValues);
}, CodeInsightBundle.message("insert.code.template.command"), null);
}
private static List<TemplateImpl> filterApplicableCandidates(PsiFile file, int caretOffset, List<TemplateImpl> candidates) {
if (candidates.isEmpty()) {
return candidates;
}
PsiFile copy = insertDummyIdentifierWithCache(file, caretOffset, caretOffset, CompletionUtil.DUMMY_IDENTIFIER_TRIMMED).getFile();
List<TemplateImpl> result = new ArrayList<>();
for (TemplateImpl candidate : candidates) {
if (isApplicable(copy, caretOffset - candidate.getKey().length(), candidate)) {
result.add(candidate);
}
}
return result;
}
private static List<TemplateContextType> getBases(TemplateContextType type) {
ArrayList<TemplateContextType> list = new ArrayList<>();
while (true) {
type = type.getBaseContextType();
if (type == null) return list;
list.add(type);
}
}
private static Set<TemplateContextType> getDirectlyApplicableContextTypes(@NotNull PsiFile file, int offset) {
LinkedHashSet<TemplateContextType> set = new LinkedHashSet<>();
LinkedList<TemplateContextType> contexts = buildOrderedContextTypes();
for (TemplateContextType contextType : contexts) {
if (contextType.isInContext(file, offset)) {
set.add(contextType);
}
}
removeBases:
while (true) {
for (TemplateContextType type : set) {
if (set.removeAll(getBases(type))) {
continue removeBases;
}
}
return set;
}
}
private static LinkedList<TemplateContextType> buildOrderedContextTypes() {
final TemplateContextType[] typeCollection = getAllContextTypes();
LinkedList<TemplateContextType> userDefinedExtensionsFirst = new LinkedList<>();
for (TemplateContextType contextType : typeCollection) {
if (contextType.getClass().getName().startsWith(Template.class.getPackage().getName())) {
userDefinedExtensionsFirst.addLast(contextType);
}
else {
userDefinedExtensionsFirst.addFirst(contextType);
}
}
return userDefinedExtensionsFirst;
}
public static TemplateContextType[] getAllContextTypes() {
return ourContextTypes;
}
@Override
@Nullable
public Template getActiveTemplate(@NotNull Editor editor) {
final TemplateState templateState = getTemplateState(editor);
return templateState != null ? templateState.getTemplate() : null;
}
@Override
public boolean finishTemplate(@NotNull Editor editor) {
TemplateState state = getTemplateState(editor);
if (state != null) {
state.gotoEnd();
return true;
}
return false;
}
public static boolean isApplicable(PsiFile file, int offset, TemplateImpl template) {
return isApplicable(template, getApplicableContextTypes(file, offset));
}
public static boolean isApplicable(TemplateImpl template, Set<TemplateContextType> contextTypes) {
for (TemplateContextType type : contextTypes) {
if (template.getTemplateContext().isEnabled(type)) {
return true;
}
}
return false;
}
public static List<TemplateImpl> listApplicableTemplates(PsiFile file, int offset, boolean selectionOnly) {
Set<TemplateContextType> contextTypes = getApplicableContextTypes(file, offset);
final ArrayList<TemplateImpl> result = ContainerUtil.newArrayList();
for (final TemplateImpl template : TemplateSettings.getInstance().getTemplates()) {
if (!template.isDeactivated() && (!selectionOnly || template.isSelectionTemplate()) && isApplicable(template, contextTypes)) {
result.add(template);
}
}
return result;
}
public static List<TemplateImpl> listApplicableTemplateWithInsertingDummyIdentifier(Editor editor, PsiFile file, boolean selectionOnly) {
int startOffset = editor.getSelectionModel().getSelectionStart();
int endOffset = editor.getSelectionModel().getSelectionEnd();
OffsetsInFile offsets = insertDummyIdentifierWithCache(file, startOffset, endOffset, CompletionUtil.DUMMY_IDENTIFIER_TRIMMED);
return listApplicableTemplates(offsets.getFile(), getStartOffset(offsets), selectionOnly);
}
public static List<CustomLiveTemplate> listApplicableCustomTemplates(@NotNull Editor editor, @NotNull PsiFile file, boolean selectionOnly) {
List<CustomLiveTemplate> result = new ArrayList<>();
for (CustomLiveTemplate template : CustomLiveTemplate.EP_NAME.getExtensions()) {
if ((!selectionOnly || template.supportsWrapping()) && isApplicable(template, editor, file, selectionOnly)) {
result.add(template);
}
}
return result;
}
public static Set<TemplateContextType> getApplicableContextTypes(PsiFile file, int offset) {
Set<TemplateContextType> result = getDirectlyApplicableContextTypes(file, offset);
Language baseLanguage = file.getViewProvider().getBaseLanguage();
if (baseLanguage != file.getLanguage()) {
PsiFile basePsi = file.getViewProvider().getPsi(baseLanguage);
if (basePsi != null) {
result.addAll(getDirectlyApplicableContextTypes(basePsi, offset));
}
}
// if we have, for example, a Ruby fragment in RHTML selected with its exact bounds, the file language and the base
// language will be ERb, so we won't match HTML templates for it. but they're actually valid
Language languageAtOffset = PsiUtilCore.getLanguageAtOffset(file, offset);
if (languageAtOffset != file.getLanguage() && languageAtOffset != baseLanguage) {
PsiFile basePsi = file.getViewProvider().getPsi(languageAtOffset);
if (basePsi != null) {
result.addAll(getDirectlyApplicableContextTypes(basePsi, offset));
}
}
return result;
}
private static final OffsetKey START_OFFSET = OffsetKey.create("start", false);
private static final OffsetKey END_OFFSET = OffsetKey.create("end", true);
private static int getStartOffset(OffsetsInFile offsets) {
return offsets.getOffsets().getOffset(START_OFFSET);
}
private static int getEndOffset(OffsetsInFile offsets) {
return offsets.getOffsets().getOffset(END_OFFSET);
}
private static OffsetsInFile insertDummyIdentifierWithCache(PsiFile file, int startOffset, int endOffset, String replacement) {
ProperTextRange editRange = ProperTextRange.create(startOffset, endOffset);
assertRangeWithinDocument(editRange, file.getViewProvider().getDocument());
ConcurrentFactoryMap<Pair<ProperTextRange, String>, OffsetsInFile> map = CachedValuesManager.getCachedValue(file, () ->
CachedValueProvider.Result.create(
ConcurrentFactoryMap.createConcurrentMap(
key -> copyWithDummyIdentifier(new OffsetsInFile(file), key.first.getStartOffset(), key.first.getEndOffset(), key.second)),
file, file.getViewProvider().getDocument()));
return map.get(Pair.create(editRange, replacement));
}
private static void assertRangeWithinDocument(ProperTextRange editRange, Document document) {
TextRange docRange = TextRange.from(0, document.getTextLength());
assert docRange.contains(editRange) : docRange + " doesn't contain " + editRange;
}
@NotNull
public static OffsetsInFile copyWithDummyIdentifier(OffsetsInFile offsetMap, int startOffset, int endOffset, String replacement) {
offsetMap.getOffsets().addOffset(START_OFFSET, startOffset);
offsetMap.getOffsets().addOffset(END_OFFSET, endOffset);
Document document = offsetMap.getFile().getViewProvider().getDocument();
assert document != null;
if (replacement.isEmpty() && startOffset == endOffset &&
PsiDocumentManager.getInstance(offsetMap.getFile().getProject()).isCommitted(document)) {
return offsetMap;
}
OffsetsInFile hostOffsets = offsetMap.toTopLevelFile();
OffsetsInFile hostCopy = hostOffsets.copyWithReplacement(getStartOffset(hostOffsets), getEndOffset(hostOffsets), replacement);
return hostCopy.toInjectedIfAny(getStartOffset(hostCopy));
}
}