mirror of
https://github.com/peterdell/wudsn-ide.git
synced 2024-06-10 21:29:31 +00:00
520 lines
18 KiB
Java
520 lines
18 KiB
Java
/**
|
|
* Copyright (C) 2009 - 2021 <a href="https://www.wudsn.com" target="_top">Peter Dell</a>
|
|
*
|
|
* This file is part of WUDSN IDE.
|
|
*
|
|
* WUDSN IDE is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* WUDSN IDE is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with WUDSN IDE. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package com.wudsn.ide.lng.editor;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
|
|
import org.eclipse.jface.text.TextAttribute;
|
|
import org.eclipse.jface.text.rules.ICharacterScanner;
|
|
import org.eclipse.jface.text.rules.IRule;
|
|
import org.eclipse.jface.text.rules.IToken;
|
|
import org.eclipse.jface.text.rules.RuleBasedScanner;
|
|
import org.eclipse.jface.text.rules.Token;
|
|
|
|
import com.wudsn.ide.lng.compiler.parser.CompilerSourceParser;
|
|
import com.wudsn.ide.lng.compiler.parser.CompilerSourceParserTreeObject;
|
|
import com.wudsn.ide.lng.compiler.parser.CompilerSourceParserTreeObjectType;
|
|
import com.wudsn.ide.lng.compiler.syntax.CompilerSyntax;
|
|
import com.wudsn.ide.lng.compiler.syntax.Directive;
|
|
import com.wudsn.ide.lng.compiler.syntax.Instruction;
|
|
import com.wudsn.ide.lng.compiler.syntax.InstructionSet;
|
|
import com.wudsn.ide.lng.compiler.syntax.InstructionType;
|
|
import com.wudsn.ide.lng.compiler.syntax.Opcode;
|
|
import com.wudsn.ide.lng.preferences.LanguageHardwareCompilerDefinitionPreferencesConstants;
|
|
import com.wudsn.ide.lng.preferences.LanguagePreferences;
|
|
import com.wudsn.ide.lng.preferences.LanguagePreferencesConstants;
|
|
import com.wudsn.ide.lng.preferences.LanguagePreferencesConstants.EditorConstants;
|
|
import com.wudsn.ide.lng.preferences.TextAttributeConverter;
|
|
|
|
/**
|
|
* A rule based scanner for instructions.
|
|
*
|
|
* @author Peter Dell
|
|
* @author Andy Reek
|
|
*/
|
|
final class LanguageSourceScanner extends RuleBasedScanner {
|
|
|
|
private final class LanguageWordRule implements IRule {
|
|
public final class State {
|
|
public CompilerSourceParser compilerSourceParser;
|
|
|
|
public CompilerSyntax compilerSyntax;
|
|
public InstructionSet instructionSet;
|
|
|
|
public boolean instructionsCaseSensitive;
|
|
public Map<String, IToken> instructionWordTokens;
|
|
|
|
public boolean identifiersCaseSensitive;
|
|
public Map<String, IToken> identifierWordTokens;
|
|
|
|
public State() {
|
|
instructionWordTokens = new TreeMap<String, IToken>();
|
|
identifierWordTokens = new TreeMap<String, IToken>();
|
|
}
|
|
|
|
public State createDeepCopy() {
|
|
State result = new State();
|
|
result.compilerSourceParser = compilerSourceParser;
|
|
result.compilerSyntax = compilerSyntax;
|
|
result.instructionSet = instructionSet;
|
|
result.instructionsCaseSensitive = instructionsCaseSensitive;
|
|
result.instructionWordTokens = instructionWordTokens;
|
|
result.identifiersCaseSensitive = identifiersCaseSensitive;
|
|
result.identifierWordTokens = identifierWordTokens;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// State of the LanguageWordRule instance.
|
|
State state;
|
|
|
|
/** Buffer used for pattern detection in evaluate() only. */
|
|
private StringBuilder instructionBuffer = new StringBuilder();
|
|
private StringBuilder identifierBuffer = new StringBuilder();
|
|
private StringBuilder numberBuffer = new StringBuilder();
|
|
|
|
public LanguageWordRule() {
|
|
|
|
}
|
|
|
|
public void setCompilerSourceParser(CompilerSourceParser compilerSourceParser) {
|
|
if (compilerSourceParser == null) {
|
|
throw new IllegalArgumentException("Parameter 'compilerSourceParser' must not be null.");
|
|
}
|
|
|
|
State state = new State();
|
|
state.compilerSourceParser = compilerSourceParser;
|
|
state.instructionSet = compilerSourceParser.getInstructionSet();
|
|
state.compilerSyntax = compilerSourceParser.getCompilerSyntax();
|
|
|
|
state.instructionsCaseSensitive = state.compilerSyntax.areInstructionsCaseSensitive();
|
|
state.identifiersCaseSensitive = state.compilerSyntax.areIdentifiersCaseSensitive();
|
|
|
|
synchronized (this) {
|
|
this.state = state;
|
|
}
|
|
}
|
|
|
|
public void setInstructions() {
|
|
synchronized (this) {
|
|
state.instructionWordTokens.clear();
|
|
|
|
state.instructionsCaseSensitive = state.compilerSyntax.areInstructionsCaseSensitive();
|
|
state.instructionSet = state.compilerSourceParser.getInstructionSet();
|
|
|
|
List<Instruction> instructions = state.instructionSet.getInstructions();
|
|
|
|
// Map with lower case name and corresponding token.
|
|
for (Instruction instruction : instructions) {
|
|
IToken token;
|
|
if (instruction instanceof Directive) {
|
|
token = directiveToken;
|
|
} else if (instruction instanceof Opcode) {
|
|
|
|
Opcode opcode = (Opcode) instruction;
|
|
|
|
switch (opcode.getType()) {
|
|
|
|
case InstructionType.LEGAL_OPCODE:
|
|
token = legalOpcodeToken;
|
|
break;
|
|
case InstructionType.ILLEGAL_OPCODE:
|
|
|
|
token = illegalOpcodeToken;
|
|
break;
|
|
case InstructionType.PSEUDO_OPCODE:
|
|
token = pseudoOpcodeToken;
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Unknown opcode type " + opcode.getType() + ".");
|
|
|
|
}
|
|
} else {
|
|
throw new IllegalStateException("Unknown instruction type " + instruction.toString() + ".");
|
|
|
|
}
|
|
// Case insensitive word rules expect upper case words.
|
|
if (state.instructionsCaseSensitive) {
|
|
state.instructionWordTokens.put(instruction.getName(), token);
|
|
} else {
|
|
state.instructionWordTokens.put(instruction.getUpperCaseName(), token);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Update the list of identifiers to be highlighted
|
|
*
|
|
* @param identifiers The list of identifiers, not <code>null</code>.
|
|
*/
|
|
public void setIdentifiers(List<CompilerSourceParserTreeObject> identifiers) {
|
|
if (identifiers == null) {
|
|
throw new IllegalArgumentException("Parameter 'identifiers' must not be null.");
|
|
}
|
|
synchronized (this) {
|
|
|
|
for (CompilerSourceParserTreeObject element : identifiers) {
|
|
IToken token;
|
|
switch (element.getType()) {
|
|
case CompilerSourceParserTreeObjectType.EQUATE_DEFINITION:
|
|
token = equateIdentifierToken;
|
|
break;
|
|
case CompilerSourceParserTreeObjectType.LABEL_DEFINITION:
|
|
token = labelIdentifierToken;
|
|
break;
|
|
case CompilerSourceParserTreeObjectType.ENUM_DEFINITION_SECTION:
|
|
token = enumDefinitionSectionIdentifierToken;
|
|
break;
|
|
case CompilerSourceParserTreeObjectType.STRUCTURE_DEFINITION_SECTION:
|
|
token = structureDefinitionSectionIdentifierToken;
|
|
break;
|
|
case CompilerSourceParserTreeObjectType.LOCAL_SECTION:
|
|
token = localSectionIdentifierToken;
|
|
break;
|
|
case CompilerSourceParserTreeObjectType.MACRO_DEFINITION_SECTION:
|
|
token = macroDefinitionSectionIdentifierToken;
|
|
break;
|
|
case CompilerSourceParserTreeObjectType.PROCEDURE_DEFINITION_SECTION:
|
|
token = procedureDefinitionSectionIdentifierToken;
|
|
break;
|
|
|
|
default:
|
|
throw new RuntimeException("Unexpected identifier element type " + element.getType() + " - "
|
|
+ element.getTreePath() + ".");
|
|
}
|
|
if (element.getDescription().startsWith("@style=(")) { // TODO: Test and document @style annotation
|
|
String value = element.getDescription().substring(8);
|
|
int index = value.indexOf(")");
|
|
if (index > 0) {
|
|
value = value.substring(0, index);
|
|
TextAttribute textAttribute = TextAttributeConverter.fromString(value);
|
|
token = new Token(textAttribute);
|
|
}
|
|
}
|
|
if (state.identifiersCaseSensitive) {
|
|
state.identifierWordTokens.put(element.getName(), token);
|
|
} else {
|
|
state.identifierWordTokens.put(element.getName().toUpperCase(), token);
|
|
|
|
}
|
|
}
|
|
}
|
|
System.out.println("" + this + ":" + state.identifierWordTokens.size() + " identifier word tokens set: "
|
|
+ state.identifierWordTokens); // TODO
|
|
}
|
|
|
|
/*
|
|
* @see IRule#evaluate(ICharacterScanner)
|
|
*/
|
|
@Override
|
|
public IToken evaluate(ICharacterScanner scanner) {
|
|
|
|
// Create a local copy to prevent synchronization issues.
|
|
State localState;
|
|
synchronized (this) {
|
|
localState = state.createDeepCopy();
|
|
}
|
|
|
|
int c = scanner.read();
|
|
boolean instructionStartCharacter = localState.instructionSet.isInstructionStartCharacter((char) c);
|
|
boolean identifierStartCharacter = localState.compilerSyntax.isIdentifierStartCharacter((char) c);
|
|
boolean numberStartCharacter = localState.compilerSyntax.isNumberStartCharacter((char) c);
|
|
if (c != ICharacterScanner.EOF
|
|
&& (instructionStartCharacter || identifierStartCharacter || numberStartCharacter)) {
|
|
|
|
instructionBuffer.setLength(0);
|
|
identifierBuffer.setLength(0);
|
|
numberBuffer.setLength(0);
|
|
int charactersRead = 0;
|
|
boolean instructionPartCharacter = instructionStartCharacter;
|
|
boolean identifierPartCharacter = identifierStartCharacter;
|
|
boolean numberPartCharacter = numberStartCharacter;
|
|
do {
|
|
charactersRead++;
|
|
if (instructionPartCharacter) {
|
|
instructionBuffer.append((char) c);
|
|
}
|
|
if (identifierPartCharacter) {
|
|
identifierBuffer.append((char) c);
|
|
}
|
|
if (numberPartCharacter) {
|
|
numberBuffer.append((char) c);
|
|
}
|
|
c = scanner.read();
|
|
instructionPartCharacter = instructionPartCharacter
|
|
&& localState.instructionSet.isInstructionPartCharacter((char) c);
|
|
identifierPartCharacter = identifierPartCharacter
|
|
&& (localState.compilerSyntax.isIdentifierPartCharacter((char) c));
|
|
numberPartCharacter = numberPartCharacter
|
|
&& localState.compilerSyntax.isNumberPartCharacter((char) c);
|
|
|
|
} while (c != ICharacterScanner.EOF
|
|
&& (instructionPartCharacter || identifierPartCharacter || numberPartCharacter));
|
|
scanner.unread();
|
|
|
|
String instructionString = instructionBuffer.toString();
|
|
String identifierString = identifierBuffer.toString();
|
|
String numberString = numberBuffer.toString();
|
|
// System.out.println(instructionString + "/" + identifierString
|
|
// + "/" + numberString);
|
|
|
|
// If case-insensitive, convert to upper case before
|
|
// accessing the map
|
|
if (!localState.instructionsCaseSensitive) {
|
|
instructionString = instructionString.toUpperCase();
|
|
}
|
|
|
|
IToken instructionToken = localState.instructionWordTokens.get(instructionString);
|
|
|
|
// Anything found at all?
|
|
if (instructionToken == null && identifierString.length() == 0 && numberString.length() == 0) {
|
|
unreadBuffer(scanner, charactersRead);
|
|
return Token.UNDEFINED;
|
|
}
|
|
|
|
// If the identifier string is longer, use it.
|
|
IToken token;
|
|
if (instructionToken == null || identifierString.length() > instructionString.length()) {
|
|
if (identifierString.length() >= numberString.length()) {
|
|
if (identifierString.length() == 0) {
|
|
return Token.UNDEFINED;
|
|
}
|
|
if (!localState.identifiersCaseSensitive) {
|
|
identifierString = identifierString.toUpperCase();
|
|
}
|
|
token = localState.identifierWordTokens.get(identifierString);
|
|
|
|
// Consume the next separator if there is one.
|
|
if (localState.compilerSyntax.isIdentifierSeparatorCharacter((char) c)) {
|
|
charactersRead--;
|
|
}
|
|
unreadBuffer(scanner, charactersRead - identifierString.length());
|
|
if (token == null) {
|
|
token = Token.UNDEFINED;
|
|
}
|
|
return token;
|
|
}
|
|
unreadBuffer(scanner, charactersRead - numberString.length());
|
|
return numberToken;
|
|
|
|
}
|
|
if (instructionString.length() >= numberString.length()) {
|
|
unreadBuffer(scanner, charactersRead - instructionString.length());
|
|
return instructionToken;
|
|
} else if (numberString.length() > 0) {
|
|
return numberToken;
|
|
}
|
|
|
|
return Token.UNDEFINED;
|
|
}
|
|
|
|
scanner.unread();
|
|
return Token.UNDEFINED;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the specified number of characters to the scanner.
|
|
*
|
|
* @param scanner The scanner to be used, not <code>null</code>.
|
|
* @param count The count. If the count is 0 or negative, no characters will
|
|
* be returned.
|
|
*/
|
|
private void unreadBuffer(ICharacterScanner scanner, int count) {
|
|
for (int i = 0; i < count; i++) {
|
|
scanner.unread();
|
|
}
|
|
}
|
|
}
|
|
|
|
private LanguageEditor editor;
|
|
private Map<String, Token> tokens;
|
|
|
|
// Numbers
|
|
IToken numberToken;
|
|
|
|
// Instructions.
|
|
IToken directiveToken;
|
|
IToken legalOpcodeToken;
|
|
IToken illegalOpcodeToken;
|
|
IToken pseudoOpcodeToken;
|
|
|
|
// Identifiers.
|
|
IToken equateIdentifierToken;
|
|
IToken labelIdentifierToken;
|
|
IToken enumDefinitionSectionIdentifierToken;
|
|
IToken structureDefinitionSectionIdentifierToken;
|
|
IToken localSectionIdentifierToken;
|
|
IToken macroDefinitionSectionIdentifierToken;
|
|
IToken procedureDefinitionSectionIdentifierToken;
|
|
|
|
// Word rule
|
|
private LanguageWordRule wordRule;
|
|
|
|
/**
|
|
* Creates a new instance. Called by the
|
|
* {@link LanguageSourceViewerConfiguration}.
|
|
*
|
|
* @param editor The underlying LanguageEditor for the code scanner, not
|
|
* <code>null</code>.
|
|
*/
|
|
LanguageSourceScanner(LanguageEditor editor) {
|
|
if (editor == null) {
|
|
throw new IllegalArgumentException("Parameter 'editor' must not be null.");
|
|
}
|
|
this.editor = editor;
|
|
this.tokens = new TreeMap<String, Token>();
|
|
|
|
createTokens();
|
|
createRules();
|
|
}
|
|
|
|
/**
|
|
* The token are stable over the life time of the editor, whereas the rules may
|
|
* change if the preferences are changed.
|
|
*/
|
|
private void createTokens() {
|
|
|
|
// Numbers
|
|
numberToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_NUMBER);
|
|
|
|
// Instructions
|
|
directiveToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_DIRECTVE);
|
|
legalOpcodeToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_OPCODE_LEGAL);
|
|
illegalOpcodeToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_OPCODE_ILLEGAL);
|
|
pseudoOpcodeToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_OPCODE_PSEUDO);
|
|
|
|
// Identifiers
|
|
equateIdentifierToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_EQUATE);
|
|
labelIdentifierToken = createLanguageToken(EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_LABEL);
|
|
enumDefinitionSectionIdentifierToken = createLanguageToken(
|
|
EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_ENUM_DEFINITION_SECTION);
|
|
structureDefinitionSectionIdentifierToken = createLanguageToken(
|
|
EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_STRUCTURE_DEFINITION_SECTION);
|
|
localSectionIdentifierToken = createLanguageToken(
|
|
EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_LOCAL_SECTION);
|
|
macroDefinitionSectionIdentifierToken = createLanguageToken(
|
|
EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_MACRO_DEFINITION_SECTION);
|
|
procedureDefinitionSectionIdentifierToken = createLanguageToken(
|
|
EditorConstants.EDITOR_TEXT_ATTRIBUTE_IDENTIFIER_PROCEDURE_DEFINITION_SECTION);
|
|
|
|
}
|
|
|
|
/**
|
|
* Creates a token bound to a text attribute name.
|
|
*
|
|
* @param textAttributeName The text attribute name, not empty and not
|
|
* <code>null</code>.
|
|
* @return The new token, not <code>null</code>.
|
|
*/
|
|
private IToken createLanguageToken(String textAttributeName) {
|
|
if (textAttributeName == null) {
|
|
throw new IllegalArgumentException("Parameter 'textAttributeName' must not be null.");
|
|
}
|
|
var preferences = editor.getLanguagePreferences();
|
|
var preferencesKey = EditorConstants.getEditorAttributeKey(preferences.getLanguage(), textAttributeName);
|
|
var token = new Token(preferences.getTextAttribute(preferencesKey));
|
|
tokens.put(preferencesKey, token);
|
|
return token;
|
|
}
|
|
|
|
/**
|
|
* Creates and sets the rules based on the the compiler syntax.
|
|
*/
|
|
private void createRules() {
|
|
|
|
// Instructions, identifiers and numbers.
|
|
wordRule = new LanguageWordRule();
|
|
List<IRule> rules = new ArrayList<IRule>(4);
|
|
rules.add(wordRule);
|
|
setRules(rules.toArray(new IRule[rules.size()]));
|
|
|
|
CompilerSourceParser compilerSourceParser = editor.createCompilerSourceParser();
|
|
wordRule.setCompilerSourceParser(compilerSourceParser);
|
|
wordRule.setInstructions();
|
|
}
|
|
|
|
/**
|
|
* Update the list of identifiers to be highlighted
|
|
*
|
|
* @param identifiers The list of identifiers, not <code>null</code>.
|
|
*/
|
|
final void setIdentifiers(List<CompilerSourceParserTreeObject> identifiers) {
|
|
if (identifiers == null) {
|
|
throw new IllegalArgumentException("Parameter 'identifiers' must not be null.");
|
|
}
|
|
wordRule.setIdentifiers(identifiers);
|
|
}
|
|
|
|
/**
|
|
* Dispose UI resources.
|
|
*/
|
|
final void dispose() {
|
|
for (Token token : tokens.values()) {
|
|
TextAttributeConverter.dispose((TextAttribute) token.getData());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the token based on the preferences. Called by
|
|
* {@link LanguageSourceViewerConfiguration}.
|
|
*
|
|
* @param preferences The preferences, not <code>null</code>.
|
|
* @param changedPreferencesKeys The set of changed property names, not
|
|
* <code>null</code>.
|
|
*
|
|
* @return <code>true</code> If the editor has to be refreshed.
|
|
*/
|
|
final boolean preferencesChanged(LanguagePreferences preferences, Set<String> changedPreferencesKeys) {
|
|
if (preferences == null) {
|
|
throw new IllegalArgumentException("Parameter 'preferences' must not be null.");
|
|
}
|
|
if (changedPreferencesKeys == null) {
|
|
throw new IllegalArgumentException("Parameter 'changedPropertyNames' must not be null.");
|
|
}
|
|
boolean refresh = false;
|
|
var compilerTargetPreferencesKey = LanguageHardwareCompilerDefinitionPreferencesConstants
|
|
.getCompilerTargetName(editor.getLanguage(), editor.getHardware(), editor.getCompilerDefinition());
|
|
for (String preferencesKey : changedPreferencesKeys) {
|
|
Token token = tokens.get(preferencesKey);
|
|
if (token != null) {
|
|
TextAttributeConverter.dispose((TextAttribute) token.getData());
|
|
token.setData(preferences.getTextAttribute(preferencesKey));
|
|
refresh = true;
|
|
|
|
} else if (compilerTargetPreferencesKey.equals(preferencesKey)) {
|
|
CompilerSourceParser compilerSourceParser = editor.createCompilerSourceParser();
|
|
wordRule.setCompilerSourceParser(compilerSourceParser);
|
|
wordRule.setInstructions();
|
|
refresh = true;
|
|
}
|
|
}
|
|
return refresh;
|
|
|
|
}
|
|
|
|
}
|