Skip to content

Commit 749d3da

Browse files
committed
migrate Twig translation annotator to inspection; drop possible memory leaks with a popover bridge #832; prepare persistent translation annotator for #506
1 parent 0d99964 commit 749d3da

File tree

13 files changed

+386
-87
lines changed

13 files changed

+386
-87
lines changed

META-INF/plugin.xml

+12
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,18 @@
488488
enabledByDefault="true" level="WARNING"
489489
implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.inspection.ContainerConstantInspection"/>
490490

491+
<localInspection groupPath="Symfony" shortName="TwigTranslationDomain" displayName="Twig: Missing translation domain"
492+
groupName="Translation"
493+
enabledByDefault="true" level="WARNING"
494+
language="Twig"
495+
implementationClass="fr.adrienbrault.idea.symfony2plugin.translation.inspection.TwigTranslationDomainInspection"/>
496+
497+
<localInspection groupPath="Symfony" shortName="TwigTranslationDomain" displayName="Twig: Missing translation key"
498+
groupName="Translation"
499+
enabledByDefault="true" level="WARNING"
500+
language="Twig"
501+
implementationClass="fr.adrienbrault.idea.symfony2plugin.translation.inspection.TwigTranslationKeyInspection"/>
502+
491503
<intentionAction>
492504
<className>fr.adrienbrault.idea.symfony2plugin.intentions.php.PhpServiceIntention</className>
493505
<category>PHP</category>

src/fr/adrienbrault/idea/symfony2plugin/Settings.java

-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ public class Settings implements PersistentStateComponent<Settings> {
6767
public boolean twigAnnotateAsset = true;
6868
public boolean twigAnnotateAssetTags = true;
6969
public boolean twigAnnotateRoute = true;
70-
public boolean twigAnnotateTranslation = true;
7170

7271
public boolean phpAnnotateTemplate = true;
7372
public boolean phpAnnotateService = true;

src/fr/adrienbrault/idea/symfony2plugin/SettingsForm.form

-9
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,6 @@
391391
<text value="Asset Tags"/>
392392
</properties>
393393
</component>
394-
<component id="cb3c8" class="javax.swing.JCheckBox" binding="twigAnnotateTranslation">
395-
<constraints>
396-
<grid row="36" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
397-
<forms/>
398-
</constraints>
399-
<properties>
400-
<text value="Translation"/>
401-
</properties>
402-
</component>
403394
<component id="c828d" class="javax.swing.JCheckBox" binding="codeFoldingTwigConstant">
404395
<constraints>
405396
<grid row="24" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>

src/fr/adrienbrault/idea/symfony2plugin/SettingsForm.java

-4
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public class SettingsForm implements Configurable {
6262
private TextFieldWithBrowseButton pathToTranslationRootTextField;
6363
private JButton buttonHelp;
6464
private JCheckBox highlightServices;
65-
private JCheckBox twigAnnotateTranslation;
6665
private JCheckBox phpAnnotateTranslation;
6766

6867
private JCheckBox codeFoldingPhpRoute;
@@ -142,7 +141,6 @@ public boolean isModified() {
142141
|| !twigAnnotateTemplate.isSelected() == getSettings().twigAnnotateTemplate
143142
|| !twigAnnotateAsset.isSelected() == getSettings().twigAnnotateAsset
144143
|| !twigAnnotateAssetTags.isSelected() == getSettings().twigAnnotateAssetTags
145-
|| !twigAnnotateTranslation.isSelected() == getSettings().twigAnnotateTranslation
146144

147145
|| !phpAnnotateTemplate.isSelected() == getSettings().phpAnnotateTemplate
148146
|| !phpAnnotateService.isSelected() == getSettings().phpAnnotateService
@@ -183,7 +181,6 @@ public void apply() throws ConfigurationException {
183181
getSettings().twigAnnotateTemplate = twigAnnotateTemplate.isSelected();
184182
getSettings().twigAnnotateAsset = twigAnnotateAsset.isSelected();
185183
getSettings().twigAnnotateAssetTags = twigAnnotateAssetTags.isSelected();
186-
getSettings().twigAnnotateTranslation = twigAnnotateTranslation.isSelected();
187184

188185
getSettings().phpAnnotateTemplate = phpAnnotateTemplate.isSelected();
189186
getSettings().phpAnnotateService = phpAnnotateService.isSelected();
@@ -235,7 +232,6 @@ private void updateUIFromSettings() {
235232
twigAnnotateTemplate.setSelected(getSettings().twigAnnotateTemplate);
236233
twigAnnotateAsset.setSelected(getSettings().twigAnnotateAsset);
237234
twigAnnotateAssetTags.setSelected(getSettings().twigAnnotateAssetTags);
238-
twigAnnotateTranslation.setSelected(getSettings().twigAnnotateTranslation);
239235

240236
phpAnnotateTemplate.setSelected(getSettings().phpAnnotateTemplate);
241237
phpAnnotateService.setSelected(getSettings().phpAnnotateService);

src/fr/adrienbrault/idea/symfony2plugin/templating/TwigAnnotator.java

-73
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package fr.adrienbrault.idea.symfony2plugin.templating;
22

3-
import com.intellij.lang.annotation.Annotation;
43
import com.intellij.lang.annotation.AnnotationHolder;
54
import com.intellij.lang.annotation.Annotator;
6-
import com.intellij.patterns.PlatformPatterns;
75
import com.intellij.psi.PsiElement;
8-
import com.intellij.psi.PsiFile;
9-
import com.jetbrains.twig.TwigTokenTypes;
106
import fr.adrienbrault.idea.symfony2plugin.Settings;
117
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
128
import fr.adrienbrault.idea.symfony2plugin.TwigHelper;
@@ -15,15 +11,10 @@
1511
import fr.adrienbrault.idea.symfony2plugin.routing.PhpRoutingAnnotator;
1612
import fr.adrienbrault.idea.symfony2plugin.templating.assets.TwigNamedAssetsServiceParser;
1713
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil;
18-
import fr.adrienbrault.idea.symfony2plugin.translation.TranslationKeyIntentionAction;
19-
import fr.adrienbrault.idea.symfony2plugin.translation.dict.TranslationUtil;
20-
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
2114
import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory;
2215
import org.apache.commons.lang.StringUtils;
2316
import org.jetbrains.annotations.NotNull;
24-
import org.jetbrains.yaml.psi.YAMLFile;
2517

26-
import java.util.List;
2718
import java.util.Set;
2819

2920
/**
@@ -53,70 +44,6 @@ public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolde
5344
if(Settings.getInstance(element.getProject()).twigAnnotateTemplate) {
5445
this.annotateTemplate(element, holder);
5546
}
56-
57-
if(Settings.getInstance(element.getProject()).twigAnnotateTranslation) {
58-
this.annotateTranslationKey(element, holder);
59-
this.annotateTranslationDomain(element, holder);
60-
}
61-
62-
63-
}
64-
65-
private void annotateTranslationKey(@NotNull final PsiElement psiElement, @NotNull AnnotationHolder holder) {
66-
67-
if(!TwigHelper.getTranslationPattern("trans", "transchoice").accepts(psiElement)) {
68-
return;
69-
}
70-
71-
String text = psiElement.getText();
72-
if(StringUtils.isBlank(text)) {
73-
return;
74-
}
75-
76-
// get domain on file scope or method parameter
77-
String domainName = TwigUtil.getPsiElementTranslationDomain(psiElement);
78-
79-
if(!TranslationUtil.hasTranslationKey(psiElement.getProject(), text, domainName)) {
80-
Annotation annotationHolder = holder.createWarningAnnotation(psiElement, "Missing Translation");
81-
List<PsiFile> psiElements = TranslationUtil.getDomainPsiFiles(psiElement.getProject(), domainName);
82-
for(PsiFile psiFile: psiElements) {
83-
if(psiFile instanceof YAMLFile || TranslationUtil.isSupportedXlfFile(psiFile)) {
84-
annotationHolder.registerFix(new TranslationKeyIntentionAction(psiFile, text));
85-
}
86-
}
87-
}
88-
89-
}
90-
91-
private void annotateTranslationDomain(@NotNull final PsiElement psiElement, @NotNull AnnotationHolder holder) {
92-
93-
if(!TwigHelper.getTransDomainPattern().accepts(psiElement)) {
94-
return;
95-
}
96-
97-
// @TODO: move to pattern, dont allow nested filters: eg "'form.tab.profile'|trans|desc('Interchange')"
98-
final PsiElement[] psiElementTrans = new PsiElement[1];
99-
PsiElementUtils.getPrevSiblingOnCallback(psiElement, psiElement1 -> {
100-
if(psiElement1.getNode().getElementType() == TwigTokenTypes.FILTER) {
101-
return false;
102-
} else {
103-
if(PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans", "transchoice")).accepts(psiElement1)) {
104-
psiElementTrans[0] = psiElement1;
105-
}
106-
}
107-
108-
return true;
109-
});
110-
111-
//PsiElement psiElementTrans = PsiElementUtils.getPrevSiblingOfType(psiElement, PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans", "transchoice")));
112-
113-
if(psiElementTrans[0] != null && TwigHelper.getTwigMethodString(psiElementTrans[0]) != null) {
114-
String text = psiElement.getText();
115-
if(StringUtils.isNotBlank(text) && !TranslationUtil.hasDomain(psiElement.getProject(), text)) {
116-
holder.createWarningAnnotation(psiElement, "Missing Translation Domain");
117-
}
118-
}
119-
12047
}
12148

12249
private void annotateRoute(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package fr.adrienbrault.idea.symfony2plugin.translation;
2+
3+
import com.intellij.codeInspection.IntentionAndQuickFixAction;
4+
import com.intellij.openapi.application.ApplicationManager;
5+
import com.intellij.openapi.command.CommandProcessor;
6+
import com.intellij.openapi.editor.Editor;
7+
import com.intellij.openapi.project.Project;
8+
import com.intellij.openapi.ui.popup.JBPopupFactory;
9+
import com.intellij.openapi.vfs.VfsUtil;
10+
import com.intellij.openapi.vfs.VirtualFile;
11+
import com.intellij.psi.PsiFile;
12+
import com.intellij.ui.components.JBList;
13+
import fr.adrienbrault.idea.symfony2plugin.translation.dict.TranslationUtil;
14+
import fr.adrienbrault.idea.symfony2plugin.translation.util.TranslationInsertUtil;
15+
import org.apache.commons.lang.StringUtils;
16+
import org.jetbrains.annotations.Nls;
17+
import org.jetbrains.annotations.NotNull;
18+
import org.jetbrains.annotations.Nullable;
19+
import org.jetbrains.yaml.psi.YAMLFile;
20+
21+
import javax.swing.*;
22+
import java.awt.*;
23+
import java.util.ArrayList;
24+
import java.util.Collection;
25+
import java.util.List;
26+
27+
/**
28+
* @author Daniel Espendiller <daniel@espendiller.net>
29+
*/
30+
public class TranslationKeyIntentionAndQuickFixAction extends IntentionAndQuickFixAction {
31+
@NotNull
32+
private final String key;
33+
34+
@NotNull
35+
private final String domain;
36+
37+
@NotNull
38+
private final DomainCollector domainCollector;
39+
40+
public TranslationKeyIntentionAndQuickFixAction(@NotNull String key, @NotNull String domain) {
41+
this(key, domain, new AllDomainCollector());
42+
}
43+
44+
public TranslationKeyIntentionAndQuickFixAction(@NotNull String key, @NotNull String domain, @NotNull DomainCollector domainCollector) {
45+
this.key = key;
46+
this.domain = domain;
47+
this.domainCollector = domainCollector;
48+
}
49+
50+
@NotNull
51+
@Override
52+
public String getName() {
53+
return "Symfony: add translations";
54+
}
55+
56+
@Nls
57+
@NotNull
58+
@Override
59+
public String getFamilyName() {
60+
return "Symfony";
61+
}
62+
63+
@NotNull
64+
private String getPresentableName(@NotNull Project project, @NotNull VirtualFile virtualFile) {
65+
// try to find suitable presentable filename
66+
String filename = virtualFile.getPath();
67+
68+
String relativePath = VfsUtil.getRelativePath(virtualFile, project.getBaseDir(), '/');
69+
if(relativePath != null) {
70+
filename = relativePath;
71+
}
72+
73+
return StringUtils.abbreviate(filename, 180);
74+
}
75+
76+
@Override
77+
public void applyFix(@NotNull Project project, @NotNull PsiFile psiFile, @Nullable Editor editor) {
78+
if(editor == null) {
79+
return;
80+
}
81+
82+
List<PsiFile> files = new ArrayList<>();
83+
84+
for(PsiFile translationPsiFile: this.domainCollector.collect(project, key, domain)) {
85+
if(translationPsiFile instanceof YAMLFile || TranslationUtil.isSupportedXlfFile(translationPsiFile)) {
86+
String relativePath = VfsUtil.getRelativePath(translationPsiFile.getVirtualFile(), project.getBaseDir(), '/');
87+
88+
// sort collection. eg vendor last
89+
if(relativePath != null && (relativePath.startsWith("app") || relativePath.startsWith("src"))) {
90+
files.add(0, translationPsiFile);
91+
} else {
92+
files.add(translationPsiFile);
93+
}
94+
}
95+
}
96+
97+
JBList<PsiFile> list = new JBList<>(files);
98+
99+
list.setCellRenderer(new JBList.StripedListCellRenderer() {
100+
@Override
101+
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
102+
Component renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
103+
if (renderer instanceof JLabel && value instanceof PsiFile) {
104+
((JLabel) renderer).setText(getPresentableName(project, ((PsiFile) value).getVirtualFile()));
105+
}
106+
107+
return renderer;
108+
}
109+
});
110+
111+
JBPopupFactory.getInstance().createListPopupBuilder(list)
112+
.setTitle("Symfony: Translation files")
113+
.setItemChoosenCallback(() -> {
114+
PsiFile selectedFile = list.getSelectedValue();
115+
116+
CommandProcessor.getInstance().executeCommand(selectedFile.getProject(), () -> ApplicationManager.getApplication().runWriteAction(() -> {
117+
TranslationInsertUtil.invokeTranslation(selectedFile, key, domain);
118+
}), "Translation insert " + selectedFile.getName(), null);
119+
})
120+
.createPopup()
121+
.showInBestPositionFor(editor);
122+
}
123+
124+
public interface DomainCollector {
125+
@NotNull
126+
Collection<PsiFile> collect(@NotNull Project project, @NotNull String key, @NotNull String domain);
127+
}
128+
129+
private static class AllDomainCollector implements DomainCollector {
130+
@Override
131+
@NotNull
132+
public Collection<PsiFile> collect(@NotNull Project project, @NotNull String key, @NotNull String domain) {
133+
return TranslationUtil.getDomainPsiFiles(project, domain);
134+
}
135+
}
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package fr.adrienbrault.idea.symfony2plugin.translation.inspection;
2+
3+
import com.intellij.codeInspection.LocalInspectionTool;
4+
import com.intellij.codeInspection.ProblemHighlightType;
5+
import com.intellij.codeInspection.ProblemsHolder;
6+
import com.intellij.patterns.PlatformPatterns;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.PsiElementVisitor;
9+
import com.jetbrains.twig.TwigTokenTypes;
10+
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
11+
import fr.adrienbrault.idea.symfony2plugin.TwigHelper;
12+
import fr.adrienbrault.idea.symfony2plugin.translation.dict.TranslationUtil;
13+
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
14+
import org.apache.commons.lang.StringUtils;
15+
import org.jetbrains.annotations.NotNull;
16+
17+
/**
18+
* @author Daniel Espendiller <daniel@espendiller.net>
19+
*/
20+
public class TwigTranslationDomainInspection extends LocalInspectionTool {
21+
@NotNull
22+
public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, boolean isOnTheFly) {
23+
if(!Symfony2ProjectComponent.isEnabled(holder.getProject())) {
24+
return super.buildVisitor(holder, isOnTheFly);
25+
}
26+
27+
return new MyTranslationDomainPsiElementVisitor(holder);
28+
}
29+
30+
/**
31+
* 'foo'|trans({}, 'foobar')
32+
* 'foo'|transchoice({}, null, 'foobar')
33+
*/
34+
private static class MyTranslationDomainPsiElementVisitor extends PsiElementVisitor {
35+
private final ProblemsHolder holder;
36+
37+
MyTranslationDomainPsiElementVisitor(ProblemsHolder holder) {
38+
this.holder = holder;
39+
}
40+
41+
@Override
42+
public void visitElement(PsiElement psiElement) {
43+
if(!TwigHelper.getTransDomainPattern().accepts(psiElement)) {
44+
return;
45+
}
46+
47+
// @TODO: move to pattern, dont allow nested filters: eg "'form.tab.profile'|trans|desc('Interchange')"
48+
final PsiElement[] psiElementTrans = new PsiElement[1];
49+
PsiElementUtils.getPrevSiblingOnCallback(psiElement, psiElement1 -> {
50+
if(psiElement1.getNode().getElementType() == TwigTokenTypes.FILTER) {
51+
return false;
52+
} else {
53+
if(PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans", "transchoice")).accepts(psiElement1)) {
54+
psiElementTrans[0] = psiElement1;
55+
}
56+
}
57+
58+
return true;
59+
});
60+
61+
if(psiElementTrans[0] != null && TwigHelper.getTwigMethodString(psiElementTrans[0]) != null) {
62+
String text = psiElement.getText();
63+
if(StringUtils.isNotBlank(text) && !TranslationUtil.hasDomain(psiElement.getProject(), text)) {
64+
holder.registerProblem(
65+
psiElement,
66+
"Missing translation domain",
67+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING
68+
);
69+
}
70+
}
71+
72+
super.visitElement(psiElement);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)