IntelliJ Platform Plugin SDK Help

19. Quick Fix

A quick fix for a custom language supports the IntelliJ Platform-based IDE feature Intention Actions. For the Simple Language, this tutorial adds a quick fix that helps to define an unresolved property from its usage.

Reference: Code Inspections and Intentions

Update the Element Factory

The SimpleElementFactory is updated to include two new methods to support the user choice of creating a new property for the Simple Language quick fix. The new createCRLF() method supports adding a newline to the end of the test.simple file before adding a new property. A new overload of createProperty() creates a new key-value pair for Simple Language.

// Copyright 2000-2022 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.sdk.language.psi; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFileFactory; import org.intellij.sdk.language.SimpleFileType; public class SimpleElementFactory { public static SimpleProperty createProperty(Project project, String name) { final SimpleFile file = createFile(project, name); return (SimpleProperty) file.getFirstChild(); } public static SimpleFile createFile(Project project, String text) { String name = "dummy.simple"; return (SimpleFile) PsiFileFactory.getInstance(project).createFileFromText(name, SimpleFileType.INSTANCE, text); } public static SimpleProperty createProperty(Project project, String name, String value) { final SimpleFile file = createFile(project, name + " = " + value); return (SimpleProperty) file.getFirstChild(); } public static PsiElement createCRLF(Project project) { final SimpleFile file = createFile(project, "\n"); return file.getFirstChild(); } }

Define an Intention Action

The SimpleCreatePropertyQuickFix creates a property in the file chosen by the user - in this case, a Java file containing a prefix:key - and navigate to this property after creation. Under the hood, SimpleCreatePropertyQuickFix is an Intention Action. For a more in-depth example of an Intention Action, see conditional_operator_intention.

// Copyright 2000-2022 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.sdk.language; import com.intellij.codeInsight.intention.impl.BaseIntentionAction; import com.intellij.lang.ASTNode; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileChooser.FileChooser; import com.intellij.openapi.fileChooser.FileChooserDescriptor; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.Navigatable; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import; import; import com.intellij.util.IncorrectOperationException; import org.intellij.sdk.language.psi.SimpleElementFactory; import org.intellij.sdk.language.psi.SimpleFile; import org.intellij.sdk.language.psi.SimpleProperty; import org.jetbrains.annotations.NotNull; import java.util.Collection; class SimpleCreatePropertyQuickFix extends BaseIntentionAction { private final String key; SimpleCreatePropertyQuickFix(String key) { this.key = key; } @NotNull @Override public String getText() { return "Create property '" + key + "'"; } @NotNull @Override public String getFamilyName() { return "Create property"; } @Override public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { return true; } @Override public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { ApplicationManager.getApplication().invokeLater(() -> { Collection<VirtualFile> virtualFiles = FileTypeIndex.getFiles(SimpleFileType.INSTANCE, GlobalSearchScope.allScope(project)); if (virtualFiles.size() == 1) { createProperty(project, virtualFiles.iterator().next()); } else { final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor(SimpleFileType.INSTANCE); descriptor.setRoots(ProjectUtil.guessProjectDir(project)); final VirtualFile file1 = FileChooser.chooseFile(descriptor, project, null); if (file1 != null) { createProperty(project, file1); } } }); } private void createProperty(final Project project, final VirtualFile file) { WriteCommandAction.writeCommandAction(project).run(() -> { SimpleFile simpleFile = (SimpleFile) PsiManager.getInstance(project).findFile(file); ASTNode lastChildNode = simpleFile.getNode().getLastChildNode(); // TODO: Add another check for CRLF if (lastChildNode != null/* && !lastChildNode.getElementType().equals(SimpleTypes.CRLF)*/) { simpleFile.getNode().addChild(SimpleElementFactory.createCRLF(project).getNode()); } // IMPORTANT: change spaces to escaped spaces or the new node will only have the first word for the key SimpleProperty property = SimpleElementFactory.createProperty(project, key.replaceAll(" ", "\\\\ "), ""); simpleFile.getNode().addChild(property.getNode()); ((Navigatable) property.getLastChild().getNavigationElement()).navigate(true); FileEditorManager.getInstance(project).getSelectedTextEditor().getCaretModel().moveCaretRelatively(2, 0, false, false, false); }); } }

Update the Annotator

When a badProperty annotation is created, the badProperty.registerFix() method is called. This method call registers the SimpleCreatePropertyQuickFix as the Intention Action for the Intellij Platform to use to correct the problem.

// Copyright 2000-2022 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.sdk.language; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiLiteralExpression; import org.intellij.sdk.language.psi.SimpleProperty; import org.jetbrains.annotations.NotNull; import java.util.List; public class SimpleAnnotator implements Annotator { // Define strings for the Simple language prefix - used for annotations, line markers, etc. public static final String SIMPLE_PREFIX_STR = "simple"; public static final String SIMPLE_SEPARATOR_STR = ":"; @Override public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) { // Ensure the Psi Element is an expression if (!(element instanceof PsiLiteralExpression)) { return; } // Ensure the Psi element contains a string that starts with the prefix and separator PsiLiteralExpression literalExpression = (PsiLiteralExpression) element; String value = literalExpression.getValue() instanceof String ? (String) literalExpression.getValue() : null; if ((value == null) || !value.startsWith(SIMPLE_PREFIX_STR + SIMPLE_SEPARATOR_STR)) { return; } // Define the text ranges (start is inclusive, end is exclusive) // "simple:key" // 01234567890 TextRange prefixRange = TextRange.from(element.getTextRange().getStartOffset(), SIMPLE_PREFIX_STR.length() + 1); TextRange separatorRange = TextRange.from(prefixRange.getEndOffset(), SIMPLE_SEPARATOR_STR.length()); TextRange keyRange = new TextRange(separatorRange.getEndOffset(), element.getTextRange().getEndOffset() - 1); // highlight "simple" prefix and ":" separator holder.newSilentAnnotation(HighlightSeverity.INFORMATION) .range(prefixRange).textAttributes(DefaultLanguageHighlighterColors.KEYWORD).create(); holder.newSilentAnnotation(HighlightSeverity.INFORMATION) .range(separatorRange).textAttributes(SimpleSyntaxHighlighter.SEPARATOR).create(); // Get the list of properties for given key String key = value.substring(SIMPLE_PREFIX_STR.length() + SIMPLE_SEPARATOR_STR.length()); List<SimpleProperty> properties = SimpleUtil.findProperties(element.getProject(), key); if (properties.isEmpty()) { holder.newAnnotation(HighlightSeverity.ERROR, "Unresolved property") .range(keyRange) .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) // ** Tutorial step 18.3 - Add a quick fix for the string containing possible properties .withFix(new SimpleCreatePropertyQuickFix(key)) .create(); } else { // Found at least one property, force the text attributes to Simple syntax value character holder.newSilentAnnotation(HighlightSeverity.INFORMATION) .range(keyRange).textAttributes(SimpleSyntaxHighlighter.VALUE).create(); } } }

Run the Project

Run the project by using the Gradle runIde task. Open the test Java file.

To test SimpleCreatePropertyQuickFix, change simple:website to simple:website.url. The key website.url is highlighted by SimpleAnnotator as an invalid key, as shown below. Choose "Create Property".

Quick Fix

The IDE opens the test.simple file and adds website.url as a new key. Add the new value for the new website.url key.

New Property

Now switch back to the Java file; the new key is highlighted as valid.

Last modified: 02 June 2022