/* * 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 pdm = PsiDocumentManager.getInstance(offsetMap.getFile().getProject()); if (ApplicationManager.getApplication().isDispatchThread()) { pdm.commitDocument(document); } if (pdm.isCommitted(document)) { return offsetMap; } } OffsetsInFile hostOffsets = offsetMap.toTopLevelFile(); OffsetsInFile hostCopy = hostOffsets.copyWithReplacement(getStartOffset(hostOffsets), getEndOffset(hostOffsets), replacement); return hostCopy.toInjectedIfAny(getStartOffset(hostCopy)); } }