diff --git a/README.md b/README.md index 31d1abd..47c9da6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 6502 Assembly Plugin for IntelliJ -This IntelliJ plugin provides basic support for 6502 assembly language. It is suitable for projects which use the `ca65` assembler to target the WDC 65c02, 65c816, and related microprocessors. +This is plugin for JetBrains IDE's, which provides basic support for 6502 assembly language. It is suitable for projects which use the `ca65` assembler to target the WDC 6502, 65C816, and related microprocessors. ![6502 Example in IntelliJ](screenshot/6502_intellij_example.png) @@ -12,6 +12,8 @@ This IntelliJ plugin provides basic support for 6502 assembly language. It is su - Refactor/rename a label and its usages - Comment/uncomment blocks of code - Code folding for scopes, procedures and macro definitions +- Completion suggestions for mnemonics and labels +- Warnings for undefined and unused symbols ## Installation @@ -30,3 +32,4 @@ I'm aware of these other plugins, which are for different assemblers. - [4ch1m/kick-assembler-acbg](https://github.com/4ch1m/kick-assembler-acbg) - Kick Assembler - [67726e/IntelliJ-6502](https://github.com/67726e/IntelliJ-6502) - NESASM - [matozoid/Intellij6502](https://github.com/matozoid/Intellij6502) - 64tass + diff --git a/build.gradle b/build.gradle index 46c415e..561f9d7 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ apply plugin: 'org.jetbrains.grammarkit' import org.jetbrains.grammarkit.tasks.* group 'org.ca65' -version '1.5' +version '1.6' sourceCompatibility = 11 repositories { @@ -38,11 +38,9 @@ runPluginVerifier { } patchPluginXml { - changeNotes = """This change adds intention actions for converting numeric literals between hexadecimal, decimal and binary representations. + changeNotes = """This change adds warnings for unresolved references within the same assembly file, and also highlights unused declarations. The result is not correct for projects which include symbols from other files (as opposed to importing them), so this feature may be disabled per-project via an intention action. -It also adds a completion helper for assembly language mnemonics, an inspection to indicate when unsupported mnemonics are used, and a quick-fix for switching the CPU target to one which includes the unsupported mnemonic. - -The 'Generic 6502 Project' template has also been improved.""" + A new weak warning has been added to suggest padding hex and binary numbers to a whole byte, since eg. 7 digit binary numbers often indicate a problem.""" sinceBuild = '211.6693' } diff --git a/src/main/java/org/ca65/action/ConvertNumberToBinaryIntentionAction.java b/src/main/java/org/ca65/action/ConvertNumberToBinaryIntentionAction.java index 12f9476..8e8128d 100644 --- a/src/main/java/org/ca65/action/ConvertNumberToBinaryIntentionAction.java +++ b/src/main/java/org/ca65/action/ConvertNumberToBinaryIntentionAction.java @@ -35,7 +35,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file if (!canConvertToBinary(text)) { return false; } - setText(Asm6502Bundle.message("INTN.convert.to.bin", literal.getText())); + setText(Asm6502Bundle.message("INTN.convert.to.bin", literal.getText(), doConvertToBinary(literal.getText()))); return true; } diff --git a/src/main/java/org/ca65/action/ConvertNumberToDecimalIntentionAction.java b/src/main/java/org/ca65/action/ConvertNumberToDecimalIntentionAction.java index a977a60..562b947 100644 --- a/src/main/java/org/ca65/action/ConvertNumberToDecimalIntentionAction.java +++ b/src/main/java/org/ca65/action/ConvertNumberToDecimalIntentionAction.java @@ -34,7 +34,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file if (!canConvertToDecimal(text)) { return false; } - setText(Asm6502Bundle.message("INTN.convert.to.dec", literal.getText())); + setText(Asm6502Bundle.message("INTN.convert.to.dec", literal.getText(), doConvertToDecimal(literal.getText()))); return true; } diff --git a/src/main/java/org/ca65/action/ConvertNumberToHexadecimalIntentionAction.java b/src/main/java/org/ca65/action/ConvertNumberToHexadecimalIntentionAction.java index 9835626..584e7d2 100644 --- a/src/main/java/org/ca65/action/ConvertNumberToHexadecimalIntentionAction.java +++ b/src/main/java/org/ca65/action/ConvertNumberToHexadecimalIntentionAction.java @@ -34,7 +34,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file if (!canConvertToHex(text)) { return false; } - setText(Asm6502Bundle.message("INTN.convert.to.hex", literal.getText())); + setText(Asm6502Bundle.message("INTN.convert.to.hex", literal.getText(), doConvertToHex(literal.getText()))); return true; } diff --git a/src/main/java/org/ca65/action/DisableProjectReferenceCheckingIntentionAction.java b/src/main/java/org/ca65/action/DisableProjectReferenceCheckingIntentionAction.java new file mode 100644 index 0000000..622f090 --- /dev/null +++ b/src/main/java/org/ca65/action/DisableProjectReferenceCheckingIntentionAction.java @@ -0,0 +1,46 @@ +package org.ca65.action; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; +import org.ca65.Asm6502Bundle; +import org.ca65.config.AsmConfiguration; +import org.ca65.helpers.Cpu; +import org.jetbrains.annotations.NotNull; + +public class DisableProjectReferenceCheckingIntentionAction implements IntentionAction { + + public DisableProjectReferenceCheckingIntentionAction() { + } + + @Override + public @IntentionName @NotNull String getText() { + return Asm6502Bundle.message("INTN.NAME.disable.reference.checking"); + } + + @Override + public @NotNull @IntentionFamilyName String getFamilyName() { + return Asm6502Bundle.message("INTN.NAME.disable.reference.checking"); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + return true; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + AsmConfiguration.getInstance(project).setReferenceCheckingEnabled(false); + DaemonCodeAnalyzer.getInstance(project).restart(); + } + + @Override + public boolean startInWriteAction() { + return false; + } +} diff --git a/src/main/java/org/ca65/action/PadNumberIntentionAction.java b/src/main/java/org/ca65/action/PadNumberIntentionAction.java new file mode 100644 index 0000000..8f8bda4 --- /dev/null +++ b/src/main/java/org/ca65/action/PadNumberIntentionAction.java @@ -0,0 +1,49 @@ +package org.ca65.action; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +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.util.IncorrectOperationException; +import org.ca65.Asm6502Bundle; +import org.ca65.psi.AsmElementFactory; +import org.jetbrains.annotations.NotNull; + +public class PadNumberIntentionAction implements IntentionAction { + private final PsiElement existingElement; + private final String suggestedReplacement; + + public PadNumberIntentionAction(PsiElement existingElement, String suggestedReplacement) { + this.existingElement = existingElement; + this.suggestedReplacement = suggestedReplacement; + } + + @Override + public @IntentionName @NotNull String getText() { + return Asm6502Bundle.message("INTN.pad.number", this.suggestedReplacement); + } + + @Override + public @NotNull @IntentionFamilyName String getFamilyName() { + return Asm6502Bundle.message("INTN.NAME.pad.number"); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + return true; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + PsiElement newLiteral = AsmElementFactory.createNumericLiteral(project, suggestedReplacement); + existingElement.replace(newLiteral); + } + + @Override + public boolean startInWriteAction() { + return true; + } +} diff --git a/src/main/java/org/ca65/annotator/AsmNumericLiteralAnnotator.java b/src/main/java/org/ca65/annotator/AsmNumericLiteralAnnotator.java new file mode 100644 index 0000000..7c9c250 --- /dev/null +++ b/src/main/java/org/ca65/annotator/AsmNumericLiteralAnnotator.java @@ -0,0 +1,60 @@ +package org.ca65.annotator; + +import com.intellij.codeInspection.LocalQuickFix; +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.psi.PsiElement; +import org.apache.commons.lang.StringUtils; +import org.ca65.Asm6502Bundle; +import org.ca65.action.IntentionActionUtil; +import org.ca65.action.PadNumberIntentionAction; +import org.ca65.psi.AsmNumericLiteral; +import org.jetbrains.annotations.NotNull; + +/** + * Provide weak warnings for hex and binary literals which are not whole bytes, eg. 7-digit binary numbers. + */ +public class AsmNumericLiteralAnnotator implements Annotator { + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if(!(element instanceof AsmNumericLiteral)) { + return; + } + // Separate checks for hex vs binary literals + String elementText = element.getText(); + if(elementText.startsWith("$") &&elementText.length() > 1) { + annotateHex(element, holder, elementText.substring(1)); + } + if(elementText.startsWith("%") &&elementText.length() > 1) { + annotateBinary(element, holder, elementText.substring(1)); + } + } + + private void annotateBinary(PsiElement element, AnnotationHolder holder, String binString) { + int len = binString.length(); + int remainder = binString.length() % 8; + if(remainder == 0) { + return; + } + String suggestedReplacement = "%" + StringUtils.leftPad(binString, (len + 8) - remainder, "0"); + holder.newAnnotation(HighlightSeverity.WEAK_WARNING, Asm6502Bundle.message("INSPECT.binary.literal.length", element.getText())) + .range(element.getTextRange()) + .highlightType(ProblemHighlightType.WEAK_WARNING) + .withFix(new PadNumberIntentionAction(element, suggestedReplacement)) + .create(); + } + + private void annotateHex(PsiElement element, AnnotationHolder holder, String hexString) { + if(hexString.length() % 2 == 0) { + return; + } + String suggestedReplacement = "$0" + hexString; + holder.newAnnotation(HighlightSeverity.WEAK_WARNING, Asm6502Bundle.message("INSPECT.hex.literal.length", element.getText())) + .range(element.getTextRange()) + .highlightType(ProblemHighlightType.WEAK_WARNING) + .withFix(new PadNumberIntentionAction(element, suggestedReplacement)) + .create(); + } +} diff --git a/src/main/java/org/ca65/annotator/AsmUnresolvedReferenceAnnotator.java b/src/main/java/org/ca65/annotator/AsmUnresolvedReferenceAnnotator.java new file mode 100644 index 0000000..bf72862 --- /dev/null +++ b/src/main/java/org/ca65/annotator/AsmUnresolvedReferenceAnnotator.java @@ -0,0 +1,74 @@ +package org.ca65.annotator; + +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.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import org.ca65.Asm6502Bundle; +import org.ca65.action.DisableProjectReferenceCheckingIntentionAction; +import org.ca65.config.AsmConfiguration; +import org.ca65.psi.AsmDotexpr; +import org.ca65.psi.impl.AsmIdentifierrImpl; +import org.jetbrains.annotations.NotNull; + +/** + * Highlight references to symbols which are not defined in current file. Does not work with includes. + **/ +public class AsmUnresolvedReferenceAnnotator implements Annotator { + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (!AsmConfiguration.getInstance(element.getProject()).isReferenceCheckingEnabled()) { + return; // Reference checking is disabled + } + if (!(element instanceof AsmIdentifierrImpl)) { + return; + } + if (isInMacroDef(element)) { + // Identifiers used in macros are not correct + return; + } + PsiReference reference = element.getReference(); + if (reference != null && reference.resolve() != null) { + return; // definition exists + } + String elementName = ((AsmIdentifierrImpl) element).getName(); + holder.newAnnotation(HighlightSeverity.ERROR, Asm6502Bundle.message("INSPECT.unresolved.reference", elementName)) + .range(element.getTextRange()) + .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) + .withFix(new DisableProjectReferenceCheckingIntentionAction()) + .create(); + } + + private boolean isInMacroDef(PsiElement element) { + // Go up until element parent is file + while (element != null && !(element.getParent() instanceof PsiFile)) { + element = element.getParent(); + } + if (element == null) { + return false; + } + // Make a guess for performance: most macros are quite short. Assume we are *not* in a macro if we don't + // find a '.macro' statement after some reasonable number of elements. + int iterMax = 250; + int i = 0; + // Not a nested structure (really should be..), so we scan backwards from here. + // if we hit a .endmacro or start of file, we are not in a macro. + // if we hit a .macro, we are. + while (element != null && i < iterMax) { + if (element instanceof AsmDotexpr) { + String elementText = element.getFirstChild() == null ? element.getText() : element.getFirstChild().getText(); + if (".macro".equalsIgnoreCase(elementText) || ".mac".equalsIgnoreCase(elementText)) { + return true; + } else if (".endmacro".equalsIgnoreCase(elementText) || ".endmac".equalsIgnoreCase(elementText)) { + return false; + } + } + element = element.getPrevSibling(); + i++; + } + return false; + } +} diff --git a/src/main/java/org/ca65/annotator/AsmUnusedReferenceAnnotator.java b/src/main/java/org/ca65/annotator/AsmUnusedReferenceAnnotator.java new file mode 100644 index 0000000..a06ceda --- /dev/null +++ b/src/main/java/org/ca65/annotator/AsmUnusedReferenceAnnotator.java @@ -0,0 +1,62 @@ +package org.ca65.annotator; + +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.psi.PsiElement; +import com.intellij.psi.PsiNameIdentifierOwner; +import com.intellij.psi.PsiReference; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.util.Query; +import org.ca65.Asm6502Bundle; +import org.ca65.action.DisableProjectReferenceCheckingIntentionAction; +import org.ca65.config.AsmConfiguration; +import org.ca65.psi.AsmLabelDefinition; +import org.ca65.psi.impl.AsmIdentifierDefinitionImpl; +import org.ca65.psi.impl.AsmLabelDefinitionImpl; +import org.jetbrains.annotations.NotNull; + +/** + * Highlight unused definitions in file. Does not catch symbols defined in includes, and is not efficient at all. + **/ +public class AsmUnusedReferenceAnnotator implements Annotator { + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (!AsmConfiguration.getInstance(element.getProject()).isReferenceCheckingEnabled()) { + return; // Reference checking is disabled + } + if (!(element instanceof AsmLabelDefinitionImpl || element instanceof AsmIdentifierDefinitionImpl)) { + return; + } + if (isReferenced((PsiNameIdentifierOwner) element)) { + return; + } + if(element instanceof AsmLabelDefinitionImpl && ((AsmLabelDefinition) element).getNameIdentifier() == null) { + return; // References to anonymous labels don't work + } + String elementName = ((PsiNameIdentifierOwner) element).getName(); + holder.newAnnotation(HighlightSeverity.WEAK_WARNING, Asm6502Bundle.message("INSPECT.unused.reference", elementName)) + .range(element.getTextRange()) + .highlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL) + .withFix(new DisableProjectReferenceCheckingIntentionAction()) + .create(); + } + + private boolean isReferenced(PsiNameIdentifierOwner element) { + final Query refs = ReferencesSearch.search(element, GlobalSearchScope.fileScope(element.getContainingFile()), false); + if (element instanceof AsmLabelDefinitionImpl) { + // Simple case + PsiReference firstReference = refs.findFirst(); + return firstReference != null; + } + // These turn up references to themselves, using text range to skip. + for (PsiReference ref : refs) { + if (!ref.getAbsoluteRange().equals(element.getTextRange())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/org/ca65/annotator/UnsupportedMnemonicAnnotator.java b/src/main/java/org/ca65/annotator/UnsupportedMnemonicAnnotator.java index d41fb85..eb1d45c 100644 --- a/src/main/java/org/ca65/annotator/UnsupportedMnemonicAnnotator.java +++ b/src/main/java/org/ca65/annotator/UnsupportedMnemonicAnnotator.java @@ -16,7 +16,6 @@ public class UnsupportedMnemonicAnnotator implements Annotator { @Override public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) { - // Ensure the Psi Element is an expression if (!(element instanceof AsmInstructionMnemonic)) { return; } diff --git a/src/main/java/org/ca65/config/AsmConfiguration.java b/src/main/java/org/ca65/config/AsmConfiguration.java index 2b56d76..ad2a771 100644 --- a/src/main/java/org/ca65/config/AsmConfiguration.java +++ b/src/main/java/org/ca65/config/AsmConfiguration.java @@ -14,6 +14,9 @@ public class AsmConfiguration implements PersistentStateComponent + + + diff --git a/src/main/resources/messages/Asm6502Bundle.properties b/src/main/resources/messages/Asm6502Bundle.properties index 34d216c..32dcb3c 100644 --- a/src/main/resources/messages/Asm6502Bundle.properties +++ b/src/main/resources/messages/Asm6502Bundle.properties @@ -1,13 +1,23 @@ INTN.category.asm6502=6502 assembly INTN.NAME.convert.to.hex=Convert to hexadecimal -INTN.convert.to.hex=Convert ''{0}'' to hexadecimal +INTN.convert.to.hex=Convert ''{0}'' to hexadecimal ''{1}'' INTN.NAME.convert.to.dec=Convert to decimal -INTN.convert.to.dec=Convert ''{0}'' to decimal +INTN.convert.to.dec=Convert ''{0}'' to decimal ''{1}'' INTN.NAME.convert.to.bin=Convert to binary -INTN.convert.to.bin=Convert ''{0}'' to binary +INTN.convert.to.bin=Convert ''{0}'' to binary ''{1}'' INTN.NAME.change.cpu=Change project CPU target INTN.change.cpu=Target the {0} CPU for this project + +INSPECT.unresolved.reference=The symbol ''{0}'' is not defined in this file. +INSPECT.unused.reference=The symbol ''{0}'' is not used in this file. +INTN.NAME.disable.reference.checking=Disable 6502 assembly reference checking for this project + +INSPECT.binary.literal.length=Size of binary number ''{0}'' is not a multiple of 8 bits +INSPECT.hex.literal.length=Size of hex number ''{0}'' is not a multiple of 8 bits + +INTN.pad.number=Replace with ''{0}'' +INTN.NAME.pad.number=Pad number with zeroes