blob: ad77eb4967c172910e72467639d56ec6394144d2 [file] [log] [blame]
/*
* Copyright 2000-2014 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.CodeInsightActionHandler;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.CodeInsightUtilBase;
import com.intellij.codeInsight.completion.PlainPrefixMatcher;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.lookup.*;
import com.intellij.codeInsight.lookup.impl.LookupImpl;
import com.intellij.codeInsight.template.CustomLiveTemplate;
import com.intellij.codeInsight.template.CustomLiveTemplateBase;
import com.intellij.codeInsight.template.CustomTemplateCallback;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.diagnostic.AttachmentFactory;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Pattern;
public class ListTemplatesHandler implements CodeInsightActionHandler {
private static final Logger LOG = Logger.getInstance(ListTemplatesHandler.class);
@Override
public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull PsiFile file) {
if (!CodeInsightUtilBase.prepareEditorForWrite(editor)) return;
if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), project)) {
return;
}
EditorUtil.fillVirtualSpaceUntilCaret(editor);
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
int offset = editor.getCaretModel().getOffset();
List<TemplateImpl> applicableTemplates = TemplateManagerImpl.listApplicableTemplateWithInsertingDummyIdentifier(editor, file, false);
Map<TemplateImpl, String> matchingTemplates = filterTemplatesByPrefix(applicableTemplates, editor, offset, false, true);
MultiMap<String, CustomLiveTemplateLookupElement> customTemplatesLookupElements = getCustomTemplatesLookupItems(editor, file, offset);
if (matchingTemplates.isEmpty()) {
for (TemplateImpl template : applicableTemplates) {
matchingTemplates.put(template, null);
}
}
if (matchingTemplates.isEmpty() && customTemplatesLookupElements.isEmpty()) {
HintManager.getInstance().showErrorHint(editor, CodeInsightBundle.message("templates.no.defined"));
return;
}
showTemplatesLookup(project, editor, file, matchingTemplates, customTemplatesLookupElements);
}
public static Map<TemplateImpl, String> filterTemplatesByPrefix(@NotNull Collection<TemplateImpl> templates, @NotNull Editor editor,
int offset, boolean fullMatch, boolean searchInDescription) {
if (offset > editor.getDocument().getTextLength()) {
LOG.error("Cannot filter templates, index out of bounds. Offset: " + offset,
AttachmentFactory.createAttachment(editor.getDocument()));
}
CharSequence documentText = editor.getDocument().getCharsSequence().subSequence(0, offset);
String prefixWithoutDots = computeDescriptionMatchingPrefix(editor.getDocument(), offset);
Pattern prefixSearchPattern = Pattern.compile(".*\\b" + prefixWithoutDots + ".*");
Map<TemplateImpl, String> matchingTemplates = new TreeMap<TemplateImpl, String>(TemplateListPanel.TEMPLATE_COMPARATOR);
for (TemplateImpl template : templates) {
String templateKey = template.getKey();
if (fullMatch) {
int startOffset = documentText.length() - templateKey.length();
if (startOffset <= 0 || !Character.isJavaIdentifierPart(documentText.charAt(startOffset - 1))) {
// after non-identifier
if (StringUtil.endsWith(documentText, templateKey)) {
matchingTemplates.put(template, templateKey);
}
}
}
else {
for (int i = templateKey.length(); i > 0; i--) {
String prefix = templateKey.substring(0, i);
int startOffset = documentText.length() - i;
if (startOffset > 0 && Character.isJavaIdentifierPart(documentText.charAt(startOffset - 1))) {
// after java identifier
continue;
}
if (StringUtil.endsWith(documentText, prefix)) {
matchingTemplates.put(template, prefix);
break;
}
}
}
if (searchInDescription) {
String templateDescription = template.getDescription();
if (!prefixWithoutDots.isEmpty() && templateDescription != null && prefixSearchPattern.matcher(templateDescription).matches()) {
matchingTemplates.put(template, prefixWithoutDots);
}
}
}
return matchingTemplates;
}
private static void showTemplatesLookup(final Project project,
final Editor editor,
final PsiFile file,
@NotNull Map<TemplateImpl, String> matchingTemplates,
@NotNull MultiMap<String, CustomLiveTemplateLookupElement> customTemplatesLookupElements) {
LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "", new TemplatesArranger());
for (Map.Entry<TemplateImpl, String> entry : matchingTemplates.entrySet()) {
TemplateImpl template = entry.getKey();
lookup.addItem(createTemplateElement(template), new PlainPrefixMatcher(StringUtil.notNullize(entry.getValue())));
}
for (Map.Entry<String, Collection<CustomLiveTemplateLookupElement>> entry : customTemplatesLookupElements.entrySet()) {
for (CustomLiveTemplateLookupElement lookupElement : entry.getValue()) {
lookup.addItem(lookupElement, new PlainPrefixMatcher(entry.getKey()));
}
}
showLookup(lookup, file);
}
public static MultiMap<String, CustomLiveTemplateLookupElement> getCustomTemplatesLookupItems(@NotNull Editor editor,
@NotNull PsiFile file,
int offset) {
final MultiMap<String, CustomLiveTemplateLookupElement> result = MultiMap.create();
CustomTemplateCallback customTemplateCallback = new CustomTemplateCallback(editor, file);
for (CustomLiveTemplate customLiveTemplate : TemplateManagerImpl.listApplicableCustomTemplates(editor, file, false)) {
if (customLiveTemplate instanceof CustomLiveTemplateBase) {
String customTemplatePrefix = ((CustomLiveTemplateBase)customLiveTemplate).computeTemplateKeyWithoutContextChecking(customTemplateCallback);
if (customTemplatePrefix != null) {
result.putValues(customTemplatePrefix, ((CustomLiveTemplateBase)customLiveTemplate).getLookupElements(file, editor, offset));
}
}
}
return result;
}
private static LiveTemplateLookupElement createTemplateElement(final TemplateImpl template) {
return new LiveTemplateLookupElementImpl(template, false) {
@Override
public Set<String> getAllLookupStrings() {
String description = template.getDescription();
if (description == null) {
return super.getAllLookupStrings();
}
return ContainerUtil.newHashSet(getLookupString(), description);
}
};
}
private static String computePrefix(TemplateImpl template, String argument) {
String key = template.getKey();
if (argument == null) {
return key;
}
if (key.length() > 0 && Character.isJavaIdentifierPart(key.charAt(key.length() - 1))) {
return key + ' ' + argument;
}
return key + argument;
}
public static void showTemplatesLookup(final Project project, final Editor editor, Map<TemplateImpl, String> template2Argument) {
final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "",
new LookupArranger.DefaultArranger());
for (TemplateImpl template : template2Argument.keySet()) {
String prefix = computePrefix(template, template2Argument.get(template));
lookup.addItem(createTemplateElement(template), new PlainPrefixMatcher(prefix));
}
showLookup(lookup, template2Argument);
}
private static void showLookup(LookupImpl lookup, @Nullable Map<TemplateImpl, String> template2Argument) {
Editor editor = lookup.getEditor();
Project project = editor.getProject();
lookup.addLookupListener(new MyLookupAdapter(project, editor, template2Argument));
lookup.refreshUi(false, true);
lookup.showLookup();
}
private static void showLookup(LookupImpl lookup, @NotNull PsiFile file) {
Editor editor = lookup.getEditor();
Project project = editor.getProject();
lookup.addLookupListener(new MyLookupAdapter(project, editor, file));
lookup.refreshUi(false, true);
lookup.showLookup();
}
@Override
public boolean startInWriteAction() {
return true;
}
private static String computeDescriptionMatchingPrefix(Document document, int offset) {
CharSequence chars = document.getCharsSequence();
int start = offset;
while (true) {
if (start == 0) break;
char c = chars.charAt(start - 1);
if (!(Character.isJavaIdentifierPart(c))) break;
start--;
}
return chars.subSequence(start, offset).toString();
}
private static class MyLookupAdapter extends LookupAdapter {
private final Project myProject;
private final Editor myEditor;
private final Map<TemplateImpl, String> myTemplate2Argument;
private final PsiFile myFile;
public MyLookupAdapter(Project project, Editor editor, @Nullable Map<TemplateImpl, String> template2Argument) {
myProject = project;
myEditor = editor;
myTemplate2Argument = template2Argument;
myFile = null;
}
public MyLookupAdapter(Project project, Editor editor, @Nullable PsiFile file) {
myProject = project;
myEditor = editor;
myTemplate2Argument = null;
myFile = file;
}
@Override
public void itemSelected(final LookupEvent event) {
FeatureUsageTracker.getInstance().triggerFeatureUsed("codeassists.liveTemplates");
final LookupElement item = event.getItem();
if (item instanceof LiveTemplateLookupElementImpl) {
final TemplateImpl template = ((LiveTemplateLookupElementImpl)item).getTemplate();
final String argument = myTemplate2Argument != null ? myTemplate2Argument.get(template) : null;
new WriteCommandAction(myProject) {
@Override
protected void run(@NotNull Result result) throws Throwable {
((TemplateManagerImpl)TemplateManager.getInstance(myProject)).startTemplateWithPrefix(myEditor, template, null, argument);
}
}.execute();
}
else if (item instanceof CustomLiveTemplateLookupElement) {
if (myFile != null) {
new WriteCommandAction(myProject) {
@Override
protected void run(@NotNull Result result) throws Throwable {
((CustomLiveTemplateLookupElement)item).expandTemplate(myEditor, myFile);
}
}.execute();
}
}
}
}
private static class TemplatesArranger extends LookupArranger {
@Override
public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup, boolean onExplicitAction) {
LinkedHashSet<LookupElement> result = new LinkedHashSet<LookupElement>();
List<LookupElement> items = getMatchingItems();
for (LookupElement item : items) {
if (item.getLookupString().startsWith(lookup.itemPattern(item))) {
result.add(item);
}
}
result.addAll(items);
ArrayList<LookupElement> list = new ArrayList<LookupElement>(result);
int selected = lookup.isSelectionTouched() ? list.indexOf(lookup.getCurrentItem()) : 0;
return new Pair<List<LookupElement>, Integer>(list, selected >= 0 ? selected : 0);
}
@Override
public LookupArranger createEmptyCopy() {
return new TemplatesArranger();
}
}
}