package fr.adrienbrault.idea.symfony2plugin.action; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlTokenType; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.twig.TwigFile; import com.jetbrains.twig.TwigTokenTypes; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.TwigHelper; import fr.adrienbrault.idea.symfony2plugin.action.comparator.ValueComparator; import fr.adrienbrault.idea.symfony2plugin.action.dict.TranslationFileModel; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import fr.adrienbrault.idea.symfony2plugin.translation.dict.TranslationUtil; import fr.adrienbrault.idea.symfony2plugin.translation.form.TranslatorKeyExtractorDialog; import fr.adrienbrault.idea.symfony2plugin.translation.util.TranslationInsertUtil; import fr.adrienbrault.idea.symfony2plugin.util.IdeHelper; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import org.apache.commons.lang.StringUtils; import java.awt.*; import java.util.*; import java.util.List; import java.util.stream.Collectors; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class TwigExtractLanguageAction extends DumbAwareAction { public TwigExtractLanguageAction() { super("Extract Translation", "Extract Translation Key", Symfony2Icons.SYMFONY); } public void update(AnActionEvent event) { Project project = event.getData(PlatformDataKeys.PROJECT); if (project == null || !Symfony2ProjectComponent.isEnabled(project)) { this.setStatus(event, false); return; } PsiFile psiFile = event.getData(PlatformDataKeys.PSI_FILE); if(!(psiFile instanceof TwigFile)) { this.setStatus(event, false); return; } Editor editor = event.getData(PlatformDataKeys.EDITOR); if(editor == null) { this.setStatus(event, false); return; } // find valid PsiElement context, because only html text is a valid extractor action PsiElement psiElement; if(editor.getSelectionModel().hasSelection()) { psiElement = psiFile.findElementAt(editor.getSelectionModel().getSelectionStart()); } else { psiElement = psiFile.findElementAt(editor.getCaretModel().getOffset()); } if(psiElement == null) { this.setStatus(event, false); return; } // <a title="TEXT">TEXT</a> IElementType elementType = psiElement.getNode().getElementType(); if(elementType == XmlTokenType.XML_DATA_CHARACTERS || elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) { this.setStatus(event, true); } else { this.setStatus(event, false); } } private void setStatus(AnActionEvent event, boolean status) { event.getPresentation().setVisible(status); event.getPresentation().setEnabled(status); } public void actionPerformed(AnActionEvent event) { final Editor editor = event.getData(PlatformDataKeys.EDITOR); if(editor == null) { return; } PsiFile psiFile = event.getData(PlatformDataKeys.PSI_FILE); if(!(psiFile instanceof TwigFile)) { return; } final Project project = ((TwigFile) psiFile).getProject(); String translationText = editor.getSelectionModel().getSelectedText(); int startOffset; int endOffset; int caretOffset = editor.getCaretModel().getOffset(); if(translationText != null) { startOffset = editor.getSelectionModel().getSelectionStart(); endOffset = editor.getSelectionModel().getSelectionEnd(); } else { // use dont selected text, so find common PsiElement PsiElement psiElement = psiFile.findElementAt(caretOffset); if(psiElement == null) { return; } IElementType elementType = psiElement.getNode().getElementType(); if(!(elementType == XmlTokenType.XML_DATA_CHARACTERS || elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)) { return; } startOffset = psiElement.getTextRange().getStartOffset(); endOffset = psiElement.getTextRange().getEndOffset(); translationText = psiElement.getText(); } final Set<String> domainNames = TranslationUtil.getTranslationDomainLookupElements(project).stream() .map(LookupElement::getLookupString) .collect(Collectors.toCollection(TreeSet::new)); // get default domain on twig tag // also pipe it to insert handler; to append it as parameter // scope to search translation domain PsiElement transDefaultScope = psiFile.findElementAt(caretOffset); if(transDefaultScope == null) { transDefaultScope = psiFile; } String defaultDomain = TwigUtil.getTransDefaultDomainOnScopeOrInjectedElement(transDefaultScope, caretOffset); if(defaultDomain == null) { defaultDomain = "messages"; } TreeMap<String, Integer> sortedMap = getPossibleDomainTreeMap((TwigFile) psiFile, domainNames); // we want to have mostly used domain preselected String reselectedDomain = defaultDomain; if(sortedMap.size() > 0) { reselectedDomain = sortedMap.firstKey(); } String defaultKey = null; if(translationText.length() < 15) { defaultKey = translationText.toLowerCase().replace(" ", "."); } final String finalDefaultDomain = defaultDomain; final int finalStartOffset = startOffset; final int finalEndOffset = endOffset; final String finalTranslationText = translationText; TranslatorKeyExtractorDialog extractorDialog = new TranslatorKeyExtractorDialog(project, psiFile, domainNames, defaultKey, reselectedDomain, new MyOnOkCallback(project, editor, finalDefaultDomain, finalStartOffset, finalEndOffset, finalTranslationText)); extractorDialog.setTitle("Symfony: Extract Translation Key"); extractorDialog.setMinimumSize(new Dimension(600, 200)); extractorDialog.pack(); extractorDialog.setLocationRelativeTo(editor.getComponent()); extractorDialog.setVisible(true); extractorDialog.setIconImage(Symfony2Icons.getImage(Symfony2Icons.SYMFONY)); } private TreeMap<String, Integer> getPossibleDomainTreeMap(TwigFile psiFile, final Set<String> domainNames) { final Map<String, Integer> found = new HashMap<>(); // visit every trans or transchoice to get possible domain names PsiTreeUtil.collectElements(psiFile, psiElement -> { if (TwigHelper.getTransDomainPattern().accepts(psiElement)) { PsiElement psiElementTrans = PsiElementUtils.getPrevSiblingOfType(psiElement, PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans", "transchoice"))); if (psiElementTrans != null && TwigHelper.getTwigMethodString(psiElementTrans) != null) { String text = psiElement.getText(); if (StringUtils.isNotBlank(text) && domainNames.contains(text)) { if (found.containsKey(text)) { found.put(text, found.get(text) + 1); } else { found.put(text, 1); } } } } return false; }); // sort in found integer value ValueComparator vc = new ValueComparator(found); TreeMap<String, Integer> sortedMap = new TreeMap<>(vc); sortedMap.putAll(found); return sortedMap; } private static class MyOnOkCallback implements TranslatorKeyExtractorDialog.OnOkCallback { private final Project project; private final Editor editor; private final String finalDefaultDomain; private final int finalStartOffset; private final int finalEndOffset; private final String finalTranslationText; MyOnOkCallback(Project project, Editor editor, String finalDefaultDomain, int finalStartOffset, int finalEndOffset, String finalTranslationText) { this.project = project; this.editor = editor; this.finalDefaultDomain = finalDefaultDomain; this.finalStartOffset = finalStartOffset; this.finalEndOffset = finalEndOffset; this.finalTranslationText = finalTranslationText; } @Override public void onClick(List<TranslationFileModel> files, final String keyName, final String domain, boolean navigateTo) { PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument()); // insert Twig trans key CommandProcessor.getInstance().executeCommand(project, () -> ApplicationManager.getApplication().runWriteAction(() -> { String insertString; // check for file context domain if(finalDefaultDomain.equals(domain)) { insertString = String.format("{{ '%s'|trans }}", keyName); } else { insertString = String.format("{{ '%s'|trans({}, '%s') }}", keyName, domain); } editor.getDocument().replaceString(finalStartOffset, finalEndOffset, insertString); editor.getCaretModel().moveToOffset(finalEndOffset); }), "Twig Translation Insert " + keyName, null); Collection<PsiElement> targets = new ArrayList<>(); // so finally insert it; first file can be a navigation target for(TranslationFileModel transPsiFile: files) { PsiFile psiFile = transPsiFile.getPsiFile(); CommandProcessor.getInstance().executeCommand(psiFile.getProject(), () -> ApplicationManager.getApplication().runWriteAction(() -> ContainerUtil.addIfNotNull(targets, TranslationInsertUtil.invokeTranslation(psiFile, keyName, finalTranslationText))), "Translation Insert " + psiFile.getName(), null ); } if(navigateTo && targets.size() > 0) { PsiDocumentManager.getInstance(project).commitAndRunReadAction(() -> IdeHelper.navigateToPsiElement(targets.iterator().next()) ); } } } }