reengineer editor assistance

Tasks Left
- language support (vue, js / ts, kotlin, php)
- Intention system is bounded to Java ATM
This commit is contained in:
marhali 2022-04-14 20:50:45 +02:00
parent 0a8a92d3f8
commit 6e9d0671ca
30 changed files with 908 additions and 530 deletions

View File

@ -0,0 +1,16 @@
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiLiteralValue;
/**
* Java specific completion contributor
* @author marhali
*/
public class JavaCompletionContributor extends CompletionContributor {
public JavaCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(PsiLiteralValue.class), new KeyCompletionProvider());
}
}

View File

@ -0,0 +1,60 @@
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.IconLoader;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.Set;
/**
* Provides existing translation keys for code completion.
* @author marhali
*/
class KeyCompletionProvider extends CompletionProvider<CompletionParameters> implements OptionalAssistance {
private static final Icon icon = IconLoader.getIcon("/icons/translate13.svg", KeyCompletionProvider.class);
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
Project project = parameters.getOriginalFile().getProject();
if(!isAssistance(project)) {
return;
}
ProjectSettings settings = ProjectSettingsService.get(project).getState();
TranslationData data = InstanceManager.get(project).store().getData();
Set<KeyPath> fullKeys = data.getFullKeys();
for (KeyPath key : fullKeys) {
result.addElement(constructLookup(new Translation(key, data.getTranslation(key)), settings));
}
}
private LookupElement constructLookup(Translation translation, ProjectSettings settings) {
KeyPathConverter converter = new KeyPathConverter(settings);
return LookupElementBuilder
.create(converter.toString(translation.getKey()))
.withTailText(" " + translation.getValue().get(settings.getPreviewLocale()), true)
.withIcon(icon);
}
}

View File

@ -1,19 +1,16 @@
package de.marhali.easyi18n.editor.kotlin;
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.PlatformPatterns;
import de.marhali.easyi18n.editor.KeyCompletionProvider;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
/**
* Kotlin specific translation key completion contributor.
* Kotlin specific completion contributor.
* @author marhali
*/
public class KotlinKeyCompletionContributor extends CompletionContributor {
public KotlinKeyCompletionContributor() {
public class KtCompletionContributor extends CompletionContributor {
public KtCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(KtLiteralStringTemplateEntry.class),
new KeyCompletionProvider());
}

View File

@ -0,0 +1,104 @@
package de.marhali.easyi18n.assistance.documentation;
import com.intellij.lang.documentation.DocumentationMarkup;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.ResourceBundle;
/**
* Provides locale values as documentation for translation keys.
* @author marhali
*/
abstract class AbstractDocumentationProvider implements DocumentationProvider, OptionalAssistance {
private static final ResourceBundle bundle = ResourceBundle.getBundle("messages");
/**
* Checks if the provided key is a valid translation-key and generates the equivalent documentation for it.
* @param project Opened project
* @param key Designated translation key
* @return Generated documentation or null if not responsible
*/
protected @Nullable String generateDoc(@NotNull Project project, @Nullable String key) {
if(key == null || !isAssistance(project)) {
return null;
}
ProjectSettings settings = ProjectSettingsService.get(project).getState();
KeyPathConverter converter = new KeyPathConverter(settings);
KeyPath path = converter.fromString(key);
// So we want to take care of context and pluralization here
// we should check the last key section for plural / context delims and if so provide all leafs within the last node
if(path.isEmpty()) {
return null;
}
TranslationData data = InstanceManager.get(project).store().getData();
String leaf = path.remove(path.size() - 1);
TranslationNode leafNode = data.getRootNode();
for(String section : path) {
leafNode = leafNode.getChildren().get(section);
if(leafNode == null) { // Cannot resolve last node before leaf
return null;
}
}
Map<String, String> results = new LinkedHashMap<>();
// Filter results for matching leafs (contextual and pluralization support)
for (Map.Entry<String, TranslationNode> entry : leafNode.getChildren().entrySet()) {
if(entry.getKey().startsWith(leaf) && entry.getValue().isLeaf()) {
results.put(entry.getKey(), entry.getValue().getValue().get(settings.getPreviewLocale()));
}
}
if(results.isEmpty()) { // No results to show
return null;
}
StringBuilder builder = new StringBuilder();
builder.append(DocumentationMarkup.DEFINITION_START);
builder.append(bundle.getString("documentation"));
builder.append(DocumentationMarkup.DEFINITION_END);
if(results.size() == 1) { // Single value
builder.append(DocumentationMarkup.CONTENT_START);
builder.append("<strong>").append(results.values().toArray()[0]).append("</strong>");
builder.append(DocumentationMarkup.CONTENT_END);
} else { // Pluralization | Contextual relevant values
builder.append(DocumentationMarkup.SECTIONS_START);
for (Map.Entry<String, String> entry : results.entrySet()) {
builder.append(DocumentationMarkup.SECTION_HEADER_START);
builder.append(entry.getKey()).append(":");
builder.append(DocumentationMarkup.SECTION_SEPARATOR);
builder.append("<p>");
builder.append("<strong>").append(entry.getValue()).append("</strong>");
}
builder.append(DocumentationMarkup.SECTIONS_END);
}
return builder.toString();
}
}

View File

@ -0,0 +1,26 @@
package de.marhali.easyi18n.assistance.documentation;
import com.intellij.psi.PsiElement;
import de.marhali.easyi18n.assistance.reference.PsiKeyReference;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable;
/**
* Language unspecific documentation provider. Every supported language should register an extension to this EP.
* @author marhali
*/
public class CommonDocumentationProvider extends AbstractDocumentationProvider {
@Override
public @Nullable
@Nls String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
if(!(element instanceof PsiKeyReference.TranslationReference)) {
return null;
}
PsiKeyReference.TranslationReference keyReference = (PsiKeyReference.TranslationReference) element;
String value = keyReference.getName();
return generateDoc(element.getProject(), value);
}
}

View File

@ -0,0 +1,53 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.openapi.editor.FoldingGroup;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Language specific folding of translation key with preferred locale value.
* @author marhali
*/
abstract class AbstractFoldingBuilder extends FoldingBuilderEx implements OptionalAssistance {
protected static final FoldingGroup group = FoldingGroup.newGroup("EasyI18n key folding");
/**
* Constructs the folding text for the provided text.
* @param project Opened project
* @param text Designated translation key
* @return Preferred locale value or null if translation does not exists
*/
protected @Nullable String getPlaceholderText(@NotNull Project project, @Nullable String text) {
if(text == null) {
return null;
}
DataStore store = InstanceManager.get(project).store();
KeyPathConverter converter = new KeyPathConverter(project);
TranslationValue localeValues = store.getData().getTranslation(converter.fromString(text));
if(localeValues == null) {
return null;
}
String previewLocale = ProjectSettingsService.get(project).getState().getPreviewLocale();
return localeValues.get(previewLocale);
}
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
return true;
}
}

View File

@ -0,0 +1,66 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.util.PsiTreeUtil;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Java specific translation key folding.
* @author marhali
*/
public class JavaFoldingBuilder extends AbstractFoldingBuilder {
@Override
public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
List<FoldingDescriptor> descriptors = new ArrayList<>();
if(!isAssistance(root.getProject())) {
return FoldingDescriptor.EMPTY;
}
Collection<PsiLiteralExpression> literalExpressions =
PsiTreeUtil.findChildrenOfType(root, PsiLiteralExpression.class);
DataStore store = InstanceManager.get(root.getProject()).store();
KeyPathConverter converter = new KeyPathConverter(root.getProject());
for(final PsiLiteralExpression literalExpression : literalExpressions) {
String value = literalExpression.getValue() instanceof String
? (String) literalExpression.getValue() : null;
if(value == null || store.getData().getTranslation(converter.fromString(value)) == null) {
continue;
}
TextRange range = literalExpression.getTextRange();
FoldingDescriptor descriptor = new FoldingDescriptor(literalExpression.getNode(),
new TextRange(range.getStartOffset() + 1, range.getEndOffset() - 1), group);
descriptors.add(descriptor);
}
return descriptors.toArray(new FoldingDescriptor[0]);
}
@Override
public @Nullable String getPlaceholderText(@NotNull ASTNode node) {
PsiLiteralExpression literalExpression = node.getPsi(PsiLiteralExpression.class);
String value = literalExpression.getValue() instanceof String ? (String) literalExpression.getValue() : null;
return getPlaceholderText(literalExpression.getProject(), value);
}
}

View File

@ -0,0 +1,63 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.psi.KtStringTemplateEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Kotlin specific translation-key folding.
* @author marhali
*/
public class KtFoldingBuilder extends AbstractFoldingBuilder {
@Override
public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
List<FoldingDescriptor> descriptors = new ArrayList<>();
if(!isAssistance(root.getProject())) {
return FoldingDescriptor.EMPTY;
}
Collection<KtStringTemplateEntry> templateEntries =
PsiTreeUtil.findChildrenOfType(root, KtStringTemplateEntry.class);
DataStore store = InstanceManager.get(root.getProject()).store();
KeyPathConverter converter = new KeyPathConverter(root.getProject());
for (KtStringTemplateEntry templateEntry : templateEntries) {
String value = templateEntry.getText();
if(value == null || store.getData().getTranslation(converter.fromString(value)) == null) {
continue;
}
TextRange range = templateEntry.getTextRange();
FoldingDescriptor descriptor = new FoldingDescriptor(templateEntry.getNode(),
new TextRange(range.getStartOffset(), range.getEndOffset()), group);
descriptors.add(descriptor);
}
return descriptors.toArray(new FoldingDescriptor[0]);
}
@Override
public @Nullable String getPlaceholderText(@NotNull ASTNode node) {
KtStringTemplateEntry templateEntry = node.getPsi(KtStringTemplateEntry.class);
return getPlaceholderText(templateEntry.getProject(), templateEntry.getText());
}
}

View File

@ -0,0 +1,93 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.codeInsight.intention.FileModifier;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiEditorUtil;
import com.siyeh.ipp.base.MutablyNamedIntention;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ResourceBundle;
/**
* Intention for extracting a translation. Either by translation key or preferred locale value
* @author marhali
*/
abstract class AbstractExtractIntention extends MutablyNamedIntention implements OptionalAssistance {
protected static final ResourceBundle bundle = ResourceBundle.getBundle("messages");
@Override
public @NotNull String getFamilyName() {
return "EasyI18n";
}
@Override
public boolean startInWriteAction() {
return false;
}
protected @NotNull String getTextForElement(@NotNull Project project, @Nullable String key) {
KeyPathConverter converter = new KeyPathConverter(project);
TranslationData data = InstanceManager.get(project).store().getData();
return key != null && data.getTranslation(converter.fromString(key)) != null
? bundle.getString("action.edit")
: bundle.getString("action.extract");
}
protected void extractTranslation(@NotNull Project project, @NotNull String text, PsiElement psi) {
ProjectSettings settings = ProjectSettingsService.get(project).getState();
KeyPathConverter converter = new KeyPathConverter(settings);
// Extract translation key
// We assume that a text is a translation-key if it contains section delimiters and does not end with them
if(text.contains(settings.getSectionDelimiter()) && !text.endsWith(settings.getSectionDelimiter())) {
new AddDialog(project, converter.fromString(text), null).showAndHandle();
} else { // Extract translation value (here preview locale value)
AddDialog dialog = new AddDialog(project, new KeyPath(), text);
// Replace editor caret with chosen translation key
dialog.registerCallback(translationUpdate -> {
Editor editor = PsiEditorUtil.findEditor(psi);
if(editor != null) {
Document doc = editor.getDocument();
Caret caret = editor.getCaretModel().getPrimaryCaret();
int start = psi.getTextOffset() + 1;
int end = start + text.length();
WriteCommandAction.runWriteCommandAction(project, () ->
doc.replaceString(start, end, converter.toString(translationUpdate.getChange().getKey())));
caret.removeSelection();
}
});
dialog.showAndHandle();;
}
}
@Override
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
return this;
}
}

View File

@ -0,0 +1,35 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.psi.*;
import com.siyeh.ipp.base.PsiElementPredicate;
import org.jetbrains.annotations.NotNull;
/**
* Java specific translation key intention.
* @author marhali
*/
public class JavaExtractIntention extends AbstractExtractIntention {
@Override
protected @IntentionName String getTextForElement(PsiElement element) {
return getTextForElement(element.getProject(), (String) ((PsiLiteralExpression) element).getValue());
}
@Override
protected void processIntention(@NotNull PsiElement element) {
System.out.println("proci2");
if(!(element instanceof PsiLiteralExpression)
|| !(((PsiLiteralExpression) element).getValue() instanceof String)) {
return;
}
extractTranslation(element.getProject(), (String) ((PsiLiteralExpression) element).getValue(), element);
}
@Override
protected @NotNull PsiElementPredicate getElementPredicate() {
System.out.println("predi2");
return element -> element instanceof PsiLiteralExpression;
}
}

View File

@ -0,0 +1,40 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.psi.PsiElement;
import com.siyeh.ipp.base.PsiElementPredicate;
import org.jetbrains.annotations.NotNull;
/**
* JavaScript specific translation key intention.
* @author marhali
*/
// TODO: current implementation does not support other languages than Java
public class JsExtractIntention extends AbstractExtractIntention {
@Override
public @NotNull String getFamilyName() {
return "JavaScript";
}
@Override
protected @IntentionName String getTextForElement(PsiElement element) {
return "HILFE";
//return getTextForElement(element.getProject(), ((JSLiteralExpression) element).getStringValue());
}
@Override
protected void processIntention(@NotNull PsiElement element) {
if(!(element instanceof JSLiteralExpression) || ((JSLiteralExpression) element).getStringValue() == null) {
return;
}
extractTranslation(element.getProject(), ((JSLiteralExpression) element).getStringValue(), element);
}
@Override
protected @NotNull PsiElementPredicate getElementPredicate() {
return element -> true;
}
}

View File

@ -0,0 +1,41 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.psi.PsiElement;
import com.siyeh.ipp.base.PsiElementPredicate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
import org.jetbrains.kotlin.psi.KtStringTemplateExpression;
/**
* Kotlin specific translation key intention.
* @author marhali
*/
// TODO: kotlin impl does not work - no action
public class KtExtractIntention extends AbstractExtractIntention {
@Override
protected @IntentionName String getTextForElement(PsiElement element) {
return "hallo";
//return getTextForElement(element.getProject(), element.getText());
}
@Override
protected void processIntention(@NotNull PsiElement element) {
System.out.println("Hallo");
if(!(element instanceof KtStringTemplateExpression)) {
System.out.println(element.getClass());
return;
}
System.out.println("hallo");
extractTranslation(element.getProject() , element.getText(), element);
}
@Override
protected @NotNull PsiElementPredicate getElementPredicate() {
System.out.println("predi");
return element -> element instanceof KtLiteralStringTemplateEntry;
}
}

View File

@ -0,0 +1,53 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceContributor;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Language specific translation key reference contributor.
* @author marhali
*/
abstract class AbstractKeyReferenceContributor extends PsiReferenceContributor implements OptionalAssistance {
/**
* Searches for relevant translation-key references
* @param project Opened project
* @param element Targeted element
* @param text Designated translation key
* @return Matched translation-key reference(s)
*/
protected @NotNull PsiReference[] getReferences(
@NotNull Project project, @NotNull PsiElement element, @Nullable String text) {
if(text == null || text.isEmpty() || !isAssistance(project)) {
return PsiReference.EMPTY_ARRAY;
}
ProjectSettings settings = ProjectSettingsService.get(project).getState();
KeyPathConverter converter = new KeyPathConverter(settings);
// TODO: We should provide multiple references if not a leaf node was provided (contextual / plurals support)
KeyPath path = converter.fromString(text);
TranslationValue values = InstanceManager.get(project).store().getData().getTranslation(path);
if(values == null) { // We only reference valid and existing translations
return PsiReference.EMPTY_ARRAY;
}
return new PsiReference[] {
new PsiKeyReference(converter, new Translation(path, values), element)
};
}
}

View File

@ -0,0 +1,40 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
/**
* Java specific key reference binding.
* @author marhali
*/
public class JavaKeyReferenceContributor extends AbstractKeyReferenceContributor {
// TODO: why not PsiLiteralExpression?
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(PsiLiteralValue.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Project project = element.getProject();
PsiLiteralValue literalValue = (PsiLiteralValue) element;
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
return getReferences(project, element, value);
}
};
}
}

View File

@ -0,0 +1,39 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.psi.PsiReferenceRegistrar;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
/**
* JavaScript specific translation-key reference binding.
* @author marhali
*/
public class JsKeyReferenceContributor extends AbstractKeyReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(JSLiteralExpression.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Project project = element.getProject();
JSLiteralExpression literalExpression = (JSLiteralExpression) element;
String value = literalExpression.getStringValue();
return getReferences(project, element, value);
}
};
}
}

View File

@ -0,0 +1,46 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.psi.PsiReferenceRegistrar;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
import org.jetbrains.kotlin.psi.KtStringTemplateExpression;
import java.util.Arrays;
import java.util.Optional;
/**
* Kotlin specific translation-key reference binding.
* @author marhali
*/
public class KtKeyReferenceContributor extends AbstractKeyReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement().inside(KtStringTemplateExpression.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Optional<PsiElement> targetElement = Arrays.stream(element.getChildren()).filter(child ->
child instanceof KtLiteralStringTemplateEntry).findAny();
if(targetElement.isEmpty()) {
return PsiReference.EMPTY_ARRAY;
}
return getReferences(element.getProject(), element, targetElement.get().getText());
}
};
}
}

View File

@ -0,0 +1,72 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.navigation.ItemPresentation;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReferenceBase;
import com.intellij.psi.SyntheticElement;
import com.intellij.psi.impl.FakePsiElement;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* References translation keys inside editor with corresponding {@link EditDialog} / {@link AddDialog}.
* @author marhali
*/
public class PsiKeyReference extends PsiReferenceBase<PsiElement> {
private final @NotNull Translation translation;
private final @NotNull KeyPathConverter converter;
protected PsiKeyReference(
@NotNull KeyPathConverter converter, @NotNull Translation translation, @NotNull PsiElement element) {
super(element, true);
this.translation = translation;
this.converter = converter;
}
public @NotNull String getKey() {
return converter.toString(translation.getKey());
}
@Override
public @Nullable PsiElement resolve() {
return new TranslationReference();
}
public class TranslationReference extends FakePsiElement implements SyntheticElement {
@Override
public PsiElement getParent() {
return myElement;
}
@Override
public void navigate(boolean requestFocus) {
new EditDialog(getProject(), translation).showAndHandle();
}
@Override
public String getPresentableText() {
return getKey();
}
@Override
public String getName() {
return getKey();
}
@Override
public @Nullable TextRange getTextRange() {
TextRange rangeInElement = getRangeInElement();
TextRange elementRange = myElement.getTextRange();
return elementRange != null ? rangeInElement.shiftRight(elementRange.getStartOffset()) : rangeInElement;
}
}
}

View File

@ -1,61 +0,0 @@
package de.marhali.easyi18n.editor;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull;
/**
* Superclass for managing key annotations.
* @author marhali
*/
public class KeyAnnotator {
/**
* Adds annotations for i18n keys with content preview for preferred locale.
* @param key I18n key extracted by psi element
* @param project Project instance
* @param holder Annotation holder
*/
protected void annotate(@NotNull String key, @NotNull Project project, @NotNull AnnotationHolder holder) {
// Do not annotate keys if service is disabled
if(!ProjectSettingsService.get(project).getState().isAssistance()) {
return;
}
ProjectSettings state = ProjectSettingsService.get(project).getState();
//String pathPrefix = state.getPathPrefix();
// TODO: Path prefix removal
String pathPrefix = "";
String previewLocale = state.getPreviewLocale();
KeyPathConverter converter = new KeyPathConverter(project);
String searchKey = key.length() >= pathPrefix.length()
? key.substring(pathPrefix.length())
: key;
if(searchKey.startsWith(KeyPath.DELIMITER)) {
searchKey = searchKey.substring(KeyPath.DELIMITER.length());
}
TranslationNode node = InstanceManager.get(project).store().getData().getNode(converter.split(searchKey));
if(node == null) { // Unknown translation. Just ignore it
return;
}
String tooltip = node.isLeaf() ? "I18n(" + previewLocale + ": " + node.getValue().get(previewLocale) + ")"
: "I18n ([])";
holder.newAnnotation(HighlightSeverity.INFORMATION, tooltip).create();
}
}

View File

@ -1,64 +0,0 @@
package de.marhali.easyi18n.editor;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.*;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.project.*;
import com.intellij.util.*;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.*;
import java.util.*;
/**
* I18n translation key completion provider.
* @author marhali
*/
public class KeyCompletionProvider extends CompletionProvider<CompletionParameters> {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
Project project = parameters.getOriginalFile().getProject();
// Do not annotate keys if service is disabled
if(!ProjectSettingsService.get(project).getState().isAssistance()) {
return;
}
DataStore store = InstanceManager.get(project).store();
String previewLocale = ProjectSettingsService.get(project).getState().getPreviewLocale();
//String pathPrefix = ProjectSettingsService.get(project).getState().getPathPrefix();
// TODO: Path prefix removal
String pathPrefix = "";
if(pathPrefix.length() > 0 && !pathPrefix.endsWith(KeyPath.DELIMITER)) {
pathPrefix += KeyPath.DELIMITER;
}
Set<KeyPath> fullKeys = store.getData().getFullKeys();
for(KeyPath currentKey : fullKeys) {
result.addElement(createElement(
pathPrefix,
currentKey,
previewLocale,
Objects.requireNonNull(store.getData().getTranslation(currentKey))
));
}
}
private LookupElement createElement(String prefix, KeyPath path, String locale, Translation translation) {
return LookupElementBuilder.create(prefix + path.toSimpleString())
.withIcon(AllIcons.Actions.PreserveCaseHover)
.appendTailText(" I18n(" + locale + ": " + translation.get(locale) + ")", true);
}
}

View File

@ -1,93 +0,0 @@
package de.marhali.easyi18n.editor;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Go to declaration reference for i18n keys.
* @author marhali
*/
public class KeyReference extends PsiReferenceBase<PsiElement> {
@Nullable private final String myKey;
public KeyReference(@NotNull final PsiElement element) {
this(element, (String)null);
}
public KeyReference(@NotNull final PsiElement element, @Nullable final String myKey) {
super(element, true);
this.myKey = myKey;
}
public KeyReference(@NotNull final PsiElement element, @NotNull TextRange textRange) {
this(element, textRange, null);
}
public KeyReference(@NotNull PsiElement element, TextRange textRange, @Nullable String myKey) {
super(element, textRange, true);
this.myKey = myKey;
}
@Override
public @Nullable PsiElement resolve() {
return new TranslationKey();
}
public String getKey() {
return myKey != null ? myKey : getValue();
}
class TranslationKey extends FakePsiElement implements SyntheticElement {
@Override
public PsiElement getParent() {
return myElement;
}
@Override
public void navigate(boolean requestFocus) {
KeyPathConverter converter = new KeyPathConverter(getProject());
KeyPath path = converter.split(getKey());
Translation translation = InstanceManager.get(getProject()).store().getData().getTranslation(path);
if(translation != null) {
new EditDialog(getProject(), new KeyedTranslation(path, translation)).showAndHandle();
} else {
new AddDialog(getProject(), path).showAndHandle();
}
}
@Override
public String getPresentableText() {
return getKey();
}
@Override
public String getName() {
return getKey();
}
@Override
public @Nullable TextRange getTextRange() {
final TextRange rangeInElement = getRangeInElement();
final TextRange elementRange = myElement.getTextRange();
return elementRange != null ? rangeInElement.shiftRight(elementRange.getStartOffset()) : rangeInElement;
}
}
public static boolean isReferencable(String value) {
return value.matches("^[^\\s:/]+$");
}
}

View File

@ -1,87 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralValue;
import com.intellij.psi.util.PsiTreeUtil;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Translation key folding with actual value based on i18n instance.
* @author marhali
*/
public class GenericFoldingBuilder extends FoldingBuilderEx {
@Override
public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
Collection<PsiLiteralValue> literalValues = PsiTreeUtil.findChildrenOfType(root, PsiLiteralValue.class);
List<FoldingDescriptor> descriptors = new ArrayList<>();
if(!ProjectSettingsService.get(root.getProject()).getState().isAssistance()) {
return FoldingDescriptor.EMPTY;
}
DataStore store = InstanceManager.get(root.getProject()).store();
KeyPathConverter converter = new KeyPathConverter(root.getProject());
for(final PsiLiteralValue literalValue : literalValues) {
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
// Undefined string literal or not a translation
if(value == null || store.getData().getTranslation(converter.split(value)) == null) {
continue;
}
descriptors.add(new FoldingDescriptor(literalValue.getNode(),
new TextRange(literalValue.getTextRange().getStartOffset() + 1,
literalValue.getTextRange().getEndOffset() - 1)));
}
return descriptors.toArray(new FoldingDescriptor[0]);
}
@Nullable
@Override
public String getPlaceholderText(@NotNull ASTNode node) {
PsiLiteralValue literalValue = node.getPsi(PsiLiteralValue.class);
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
if(value == null) {
return null;
}
DataStore store = InstanceManager.get(literalValue.getProject()).store();
KeyPathConverter converter = new KeyPathConverter(literalValue.getProject());
Translation translation = store.getData().getTranslation(converter.split(value));
if(translation == null) {
return null;
}
String previewLocale = ProjectSettingsService.get(literalValue.getProject()).getState().getPreviewLocale();
return translation.get(previewLocale);
}
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
return true;
}
}

View File

@ -1,33 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralValue;
import de.marhali.easyi18n.editor.KeyAnnotator;
import org.jetbrains.annotations.NotNull;
/**
* Translation key annotator for generic languages which support {@link PsiLiteralValue}.
* @author marhali
*/
public class GenericKeyAnnotator extends KeyAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if(!(element instanceof PsiLiteralValue)) {
return;
}
PsiLiteralValue literalValue = (PsiLiteralValue) element;
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
if(value == null) {
return;
}
annotate(value, element.getProject(), holder);
}
}

View File

@ -1,24 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.*;
import com.intellij.psi.*;
import com.intellij.psi.xml.*;
import de.marhali.easyi18n.editor.KeyCompletionProvider;
/**
* Translation key completion for generic languages which support {@link PsiLiteralValue}.
* @author marhali
*/
public class GenericKeyCompletionContributor extends CompletionContributor {
public GenericKeyCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement(PlainTextTokenTypes.PLAIN_TEXT),
new KeyCompletionProvider());
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(XmlElement.class),
new KeyCompletionProvider());
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(PsiLiteralValue.class),
new KeyCompletionProvider());
}
}

View File

@ -1,54 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull;
/**
* Generic translation key reference contributor.
* @author marhali
*/
public class GenericKeyReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralValue.class), getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
PsiLiteralValue literalValue = (PsiLiteralValue) element;
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
if(value == null) {
return PsiReference.EMPTY_ARRAY;
}
// Do not reference keys if service is disabled
if(!ProjectSettingsService.get(element.getProject()).getState().isAssistance()) {
return PsiReference.EMPTY_ARRAY;
}
KeyPathConverter converter = new KeyPathConverter(element.getProject());
if(InstanceManager.get(element.getProject()).store().getData().getTranslation(converter.split(value)) == null) {
if(!KeyReference.isReferencable(value)) { // Creation policy
return PsiReference.EMPTY_ARRAY;
}
}
return new PsiReference[] { new KeyReference(element, value) };
}
};
}
}

View File

@ -1,31 +0,0 @@
package de.marhali.easyi18n.editor.kotlin;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.psi.PsiElement;
import de.marhali.easyi18n.editor.KeyAnnotator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
/**
* Kotlin specific translation key annotator
* @author marhali
*/
public class KotlinKeyAnnotator extends KeyAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if(!(element instanceof KtLiteralStringTemplateEntry)) {
return;
}
String value = element.getText();
if(value == null) {
return;
}
annotate(value, element.getProject(), holder);
}
}

View File

@ -1,59 +0,0 @@
package de.marhali.easyi18n.editor.kotlin;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
import org.jetbrains.kotlin.psi.KtStringTemplateExpression;
/**
* Kotlin translation key reference contributor.
* @author marhali
*/
public class KotlinKeyReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(PlatformPatterns.psiElement().inside(KtStringTemplateExpression.class), getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
String value = null;
for (PsiElement child : element.getChildren()) {
if(child instanceof KtLiteralStringTemplateEntry) {
value = child.getText();
}
}
if(value == null) {
return PsiReference.EMPTY_ARRAY;
}
// Do not reference keys if service is disabled
if(!ProjectSettingsService.get(element.getProject()).getState().isAssistance()) {
return PsiReference.EMPTY_ARRAY;
}
KeyPathConverter converter = new KeyPathConverter(element.getProject());
if(InstanceManager.get(element.getProject()).store().getData().getNode(converter.split(value)) == null) {
return PsiReference.EMPTY_ARRAY;
}
return new PsiReference[] { new KeyReference(element, value) };
}
};
}
}

View File

@ -1,5 +1,27 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="JAVA" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<intentionAction>
<className>de.marhali.easyi18n.assistance.intention.JavaExtractIntention</className>
</intentionAction>
<psi.referenceContributor
language="JAVA"
implementation="de.marhali.easyi18n.assistance.reference.JavaKeyReferenceContributor"
/>
<lang.foldingBuilder
language="JAVA"
implementationClass="de.marhali.easyi18n.assistance.folding.JavaFoldingBuilder"
/>
<lang.documentationProvider
language="JAVA"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<completion.contributor
language="JAVA"
implementationClass="de.marhali.easyi18n.assistance.completion.JavaCompletionContributor"
/>
</extensions>
</idea-plugin>

View File

@ -1,6 +1,17 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="JavaScript" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<lang.foldingBuilder language="TypeScript" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<psi.referenceContributor
language="JavaScript"
implementation="de.marhali.easyi18n.assistance.reference.JsKeyReferenceContributor"
/>
<lang.documentationProvider
language="JavaScript"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<intentionAction>
<className>de.marhali.easyi18n.assistance.intention.JsExtractIntention</className>
</intentionAction>
</extensions>
</idea-plugin>

View File

@ -1,11 +1,27 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<annotator language="kotlin" implementationClass="de.marhali.easyi18n.editor.kotlin.KotlinKeyAnnotator" />
<intentionAction>
<className>de.marhali.easyi18n.assistance.intention.KtExtractIntention</className>
</intentionAction>
<completion.contributor language="kotlin"
implementationClass="de.marhali.easyi18n.editor.kotlin.KotlinKeyCompletionContributor" />
<psi.referenceContributor
language="kotlin"
implementation="de.marhali.easyi18n.assistance.reference.KtKeyReferenceContributor"
/>
<psi.referenceContributor language="kotlin"
implementation="de.marhali.easyi18n.editor.kotlin.KotlinKeyReferenceContributor" />
<lang.documentationProvider
language="kotlin"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<completion.contributor
language="kotlin"
implementationClass="de.marhali.easyi18n.assistance.completion.KtCompletionContributor"
/>
<lang.foldingBuilder
language="kotlin"
implementationClass="de.marhali.easyi18n.assistance.folding.KtFoldingBuilder"
/>
</extensions>
</idea-plugin>

View File

@ -34,15 +34,6 @@
id="de.marhali.easyi18n.service.AppSettingsConfigurable"
displayName="Easy I18n" nonDefaultProject="true"/>
<completion.contributor language="any"
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyCompletionContributor"/>
<annotator language=""
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyAnnotator"/>
<psi.referenceContributor
implementation="de.marhali.easyi18n.editor.generic.GenericKeyReferenceContributor"/>
<notificationGroup displayType="BALLOON" id="Easy I18n Notification Group"/>
<errorHandler implementation="de.marhali.easyi18n.service.ErrorReportHandler"/>