mirror of
https://github.com/amyinspace/MagicSetEditor2.git
synced 2026-06-10 04:57:00 -04:00
improved error reporting for the keyword editor
git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@260 0fc631ac-6414-0410-93d0-97cfa31319b6
This commit is contained in:
@@ -60,7 +60,7 @@ menu:
|
||||
set info tab: &Set Information F6
|
||||
style tab: St&yle F7
|
||||
keywords tab: &Keywords F8
|
||||
stats tab: S&tatistics F8
|
||||
stats tab: S&tatistics F9
|
||||
|
||||
help: &Help
|
||||
index: &Index... F1
|
||||
@@ -331,6 +331,9 @@ label:
|
||||
mode: Mode
|
||||
uses: Uses
|
||||
reminder: Reminder text
|
||||
standard keyword:
|
||||
This is a standard %s keyword, you can not edit it.
|
||||
If you make a copy of the keyword your copy will take precedent.
|
||||
|
||||
# Open dialogs
|
||||
all files All files
|
||||
|
||||
@@ -84,27 +84,30 @@ void KeywordReminderTextValue::store() {
|
||||
retrieve();
|
||||
return;
|
||||
}
|
||||
// Re-highlight
|
||||
// new value
|
||||
String new_value = untag(value);
|
||||
highlight(new_value);
|
||||
// Try to parse the script
|
||||
try {
|
||||
ScriptP new_script = parse(new_value, true);
|
||||
vector<ScriptParseError> parse_errors;
|
||||
ScriptP new_script = parse(new_value, true, parse_errors);
|
||||
if (parse_errors.empty()) {
|
||||
// parsed okay, assign
|
||||
errors.clear();
|
||||
keyword.reminder.getScriptP() = new_script;
|
||||
keyword.reminder.getUnparsed() = new_value;
|
||||
} catch (const Error& e) {
|
||||
} else {
|
||||
// parse errors, report
|
||||
errors = e.what(); // TODO
|
||||
errors = ScriptParseErrors(parse_errors).what();
|
||||
}
|
||||
// re-highlight input, show errors
|
||||
highlight(new_value, parse_errors);
|
||||
}
|
||||
|
||||
void KeywordReminderTextValue::retrieve() {
|
||||
highlight(*underlying);
|
||||
vector<ScriptParseError> no_errors;
|
||||
highlight(*underlying, no_errors);
|
||||
}
|
||||
|
||||
void KeywordReminderTextValue::highlight(const String& code) {
|
||||
void KeywordReminderTextValue::highlight(const String& code, const vector<ScriptParseError>& errors) {
|
||||
// Add tags to indicate code / syntax highlight
|
||||
// i.e. bla {if code "x" } bla
|
||||
// becomes:
|
||||
@@ -112,7 +115,24 @@ void KeywordReminderTextValue::highlight(const String& code) {
|
||||
String new_value;
|
||||
int in_brace = 0;
|
||||
bool in_string = true;
|
||||
vector<ScriptParseError>::const_iterator error = errors.begin();
|
||||
for (size_t pos = 0 ; pos < code.size() ; ) {
|
||||
// error underlining
|
||||
while (error != errors.end() && error->start == error->end) ++error;
|
||||
if (error != errors.end()) {
|
||||
if (error->start == pos) {
|
||||
new_value += _("<error>");
|
||||
}
|
||||
if (error->end == pos) {
|
||||
++error;
|
||||
if (error == errors.end() || error->start > pos) {
|
||||
new_value += _("</error>");
|
||||
} else {
|
||||
// immediatly open again
|
||||
}
|
||||
}
|
||||
}
|
||||
// process a character
|
||||
Char c = code.GetChar(pos);
|
||||
if (c == _('<')) {
|
||||
new_value += _('\1'); // escape
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <util/prec.hpp>
|
||||
#include <util/action_stack.hpp>
|
||||
#include <util/error.hpp>
|
||||
#include <data/field/text.hpp>
|
||||
|
||||
class Set;
|
||||
@@ -93,7 +94,7 @@ class KeywordReminderTextValue : public KeywordTextValue {
|
||||
virtual void retrieve();
|
||||
|
||||
/// Syntax highlight, and store in value
|
||||
void highlight(const String& code);
|
||||
void highlight(const String& code, const vector<ScriptParseError>& errors);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------- : EOF
|
||||
|
||||
@@ -33,3 +33,23 @@ void AtomTextElement::draw(RotatedDC& dc, double scale, const RealRect& rect, co
|
||||
}
|
||||
CompoundTextElement::draw(dc, scale, rect, xs, what, start, end);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------- : ErrorTextElement
|
||||
|
||||
void ErrorTextElement::draw(RotatedDC& dc, double scale, const RealRect& rect, const double* xs, DrawWhat what, size_t start, size_t end) const {
|
||||
// Draw wavy underline
|
||||
dc.SetPen(*wxRED_PEN);
|
||||
RealPoint pos = rect.bottomLeft() - dc.trInvS(RealSize(0,2));
|
||||
RealSize dx(dc.trInvS(2), 0), dy(0, dc.trInvS(1));
|
||||
while (pos.x + 1 < rect.right()) {
|
||||
dc.DrawLine(pos - dy, pos + dx + dy);
|
||||
pos += dx;
|
||||
dy = -dy;
|
||||
}
|
||||
if (pos.x < rect.right()) {
|
||||
// final piece
|
||||
dc.DrawLine(pos - dy, pos + dx / 2);
|
||||
}
|
||||
// Draw the contents
|
||||
CompoundTextElement::draw(dc, scale, rect, xs, what, start, end);
|
||||
}
|
||||
@@ -73,18 +73,19 @@ struct TextElementsFromString {
|
||||
// What formatting is enabled?
|
||||
int bold, italic, symbol;
|
||||
int soft, kwpph, param, line;
|
||||
int code, code_kw, code_string, param_ref;
|
||||
int code, code_kw, code_string, param_ref, error;
|
||||
int param_id;
|
||||
bool bracket;
|
||||
|
||||
TextElementsFromString()
|
||||
: bold(0), italic(0), symbol(0), soft(0), kwpph(0), param(0), line(0)
|
||||
, code(0), code_kw(0), code_string(0), param_ref(0)
|
||||
, code(0), code_kw(0), code_string(0), param_ref(0), error(0)
|
||||
, param_id(0), bracket(false) {}
|
||||
|
||||
// read TextElements from a string
|
||||
void fromString(TextElements& te, const String& text, size_t start, size_t end, const TextStyle& style, Context& ctx) {
|
||||
te.elements.clear();
|
||||
end = min(end, text.size());
|
||||
// for each character...
|
||||
for (size_t pos = start ; pos < end ; ) {
|
||||
Char c = text.GetChar(pos);
|
||||
@@ -126,11 +127,18 @@ struct TextElementsFromString {
|
||||
else if (is_substr(text, tag_start, _("</line"))) line -= 1;
|
||||
else if (is_substr(text, tag_start, _("<atom"))) {
|
||||
// 'atomic' indicator
|
||||
size_t end = match_close_tag(text, tag_start);
|
||||
shared_ptr<AtomTextElement> e(new AtomTextElement(text, pos, end));
|
||||
fromString(e->elements, text, pos, end, style, ctx);
|
||||
size_t end_tag = min(end, match_close_tag(text, tag_start));
|
||||
shared_ptr<AtomTextElement> e(new AtomTextElement(text, pos, end_tag));
|
||||
fromString(e->elements, text, pos, end_tag, style, ctx);
|
||||
te.elements.push_back(e);
|
||||
pos = skip_tag(text, end);
|
||||
pos = skip_tag(text, end_tag);
|
||||
} else if (is_substr(text, tag_start, _( "<error"))) {
|
||||
// error indicator
|
||||
size_t end_tag = min(end, match_close_tag(text, tag_start));
|
||||
shared_ptr<ErrorTextElement> e(new ErrorTextElement(text, pos, end_tag));
|
||||
fromString(e->elements, text, pos, end_tag, style, ctx);
|
||||
te.elements.push_back(e);
|
||||
pos = skip_tag(text, end_tag);
|
||||
} else {
|
||||
// ignore other tags
|
||||
}
|
||||
|
||||
@@ -164,6 +164,13 @@ class AtomTextElement : public CompoundTextElement {
|
||||
virtual void draw(RotatedDC& dc, double scale, const RealRect& rect, const double* xs, DrawWhat what, size_t start, size_t end) const;
|
||||
};
|
||||
|
||||
/// A TextElement drawn using a red wavy underline
|
||||
class ErrorTextElement : public CompoundTextElement {
|
||||
public:
|
||||
ErrorTextElement(const String& text, size_t start ,size_t end) : CompoundTextElement(text, start, end) {}
|
||||
|
||||
virtual void draw(RotatedDC& dc, double scale, const RealRect& rect, const double* xs, DrawWhat what, size_t start, size_t end) const;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------- : Other text elements
|
||||
/*
|
||||
|
||||
+85
-30
@@ -51,10 +51,11 @@ enum OpenBrace
|
||||
, BRACE_PAREN // (, [, {
|
||||
};
|
||||
|
||||
/// Iterator over a string, one token at a time
|
||||
/// Iterator over a string, one token at a time.
|
||||
/** Also stores errors found when tokenizing or parsing */
|
||||
class TokenIterator {
|
||||
public:
|
||||
TokenIterator(const String& str, bool string_mode);
|
||||
TokenIterator(const String& str, bool string_mode, vector<ScriptParseError>& errors);
|
||||
|
||||
/// Peek at the next token, doesn't move to the one after that
|
||||
/** Can peek further forward by using higher values of offset.
|
||||
@@ -87,6 +88,14 @@ class TokenIterator {
|
||||
void readToken();
|
||||
/// Read the next token which is a string (after the opening ")
|
||||
void readStringToken();
|
||||
|
||||
public:
|
||||
/// All errors found
|
||||
vector<ScriptParseError>& errors;
|
||||
/// Add an error message
|
||||
void add_error(const String& message);
|
||||
/// Expected some token instead of what was found
|
||||
void expected(const String& exp);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------- : Characters
|
||||
@@ -102,10 +111,11 @@ bool isLongOper(const String& s) { return s==_(":=") || s==_("==") || s==_("!=")
|
||||
|
||||
// ----------------------------------------------------------------------------- : Tokenizing
|
||||
|
||||
TokenIterator::TokenIterator(const String& str, bool string_mode)
|
||||
TokenIterator::TokenIterator(const String& str, bool string_mode, vector<ScriptParseError>& errors)
|
||||
: input(str)
|
||||
, pos(0)
|
||||
, newline(false)
|
||||
, errors(errors)
|
||||
{
|
||||
if (string_mode) {
|
||||
open_braces.push(BRACE_STRING_MODE);
|
||||
@@ -221,7 +231,8 @@ void TokenIterator::readToken() {
|
||||
// comment untill end of line
|
||||
while (pos < input.size() && input[pos] != _('\n')) ++pos;
|
||||
} else {
|
||||
throw ScriptParseError(_("Unknown character in script: '") + String(1,c) + _("'"));
|
||||
add_error(_("Unknown character in script: '") + String(1,c) + _("'"));
|
||||
// just skip the character
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +245,10 @@ void TokenIterator::readStringToken() {
|
||||
addToken(TOK_STRING, str);
|
||||
return;
|
||||
} else {
|
||||
throw ScriptParseError(_("Unexpected end of input in string constant"));
|
||||
add_error(_("Unexpected end of input in string constant"));
|
||||
// fix up
|
||||
addToken(TOK_STRING, str);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Char c = input.GetChar(pos++);
|
||||
@@ -246,7 +260,12 @@ void TokenIterator::readStringToken() {
|
||||
return;
|
||||
} else if (c == _('\\')) {
|
||||
// escape
|
||||
if (pos >= input.size()) throw ScriptParseError(_("Unexpected end of input in string constant"));
|
||||
if (pos >= input.size()) {
|
||||
add_error(_("Unexpected end of input in string constant"));
|
||||
// fix up
|
||||
addToken(TOK_STRING, str);
|
||||
return;
|
||||
}
|
||||
c = input.GetChar(pos++);
|
||||
if (c == _('n')) str += _('\n');
|
||||
else if (c == _('<')) str += _('\1'); // escape for <
|
||||
@@ -264,8 +283,20 @@ void TokenIterator::readStringToken() {
|
||||
}
|
||||
|
||||
|
||||
void TokenIterator::add_error(const String& message) {
|
||||
if (!errors.empty() && errors.back().start == pos) return; // already an error here
|
||||
errors.push_back(ScriptParseError(pos, message));
|
||||
}
|
||||
void TokenIterator::expected(const String& expected) {
|
||||
size_t error_pos = pos - peek(0).value.size();
|
||||
if (!errors.empty() && errors.back().start == pos) return; // already an error here
|
||||
errors.push_back(ScriptParseError(error_pos, expected, peek(0).value));
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------- : Parsing
|
||||
|
||||
|
||||
/// Precedence levels for parsing, higher = tighter
|
||||
enum Precedence
|
||||
{ PREC_ALL
|
||||
@@ -300,22 +331,43 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec);
|
||||
*/
|
||||
void parseOper(TokenIterator& input, Script& script, Precedence minPrec, InstructionType closeWith = I_NOP, int closeWithData = 0);
|
||||
|
||||
ScriptP parse(const String& s, bool string_mode) {
|
||||
TokenIterator input(s, string_mode);
|
||||
|
||||
ScriptP parse(const String& s, bool string_mode, vector<ScriptParseError>& errors_out) {
|
||||
errors_out.clear();
|
||||
// parse
|
||||
TokenIterator input(s, string_mode, errors_out);
|
||||
ScriptP script(new Script);
|
||||
parseOper(input, *script, PREC_ALL, I_RET);
|
||||
if (input.peek() != TOK_EOF) {
|
||||
throw ScriptParseError(_("end of input"), input.peek().value);
|
||||
} else {
|
||||
Token eof = input.read();
|
||||
if (eof != TOK_EOF) {
|
||||
input.expected(_("end of input"));
|
||||
}
|
||||
// were there errors?
|
||||
if (errors_out.empty()) {
|
||||
return script;
|
||||
} else {
|
||||
return ScriptP();
|
||||
}
|
||||
}
|
||||
|
||||
// Expect a token, throws if it is not found
|
||||
void expectToken(TokenIterator& input, const Char* expect) {
|
||||
ScriptP parse(const String& s, bool string_mode) {
|
||||
vector<ScriptParseError> errors;
|
||||
ScriptP script = parse(s, string_mode, errors);
|
||||
if (!errors.empty()) {
|
||||
throw ScriptParseErrors(errors);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
|
||||
// Expect a token, adds an error if it is not found
|
||||
bool expectToken(TokenIterator& input, const Char* expect, const Char* name_in_error = nullptr) {
|
||||
Token token = input.read();
|
||||
if (token != expect) {
|
||||
throw ScriptParseError(expect, token.value);
|
||||
if (token == expect) {
|
||||
return true;
|
||||
} else {
|
||||
input.expected(name_in_error ? name_in_error : expect);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +419,9 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec) {
|
||||
// for each AAA in BBB do CCC
|
||||
input.read(); // each
|
||||
Token name = input.read(); // AAA
|
||||
if (name != TOK_NAME) throw ScriptParseError(_("name"), name.value);
|
||||
if (name != TOK_NAME) {
|
||||
input.expected(_("name"));
|
||||
}
|
||||
expectToken(input, _("in")); // in
|
||||
parseOper(input, script, PREC_SET); // BBB
|
||||
script.addInstruction(I_UNARY, I_ITERATOR_C); // iterator_collection
|
||||
@@ -428,7 +482,8 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec) {
|
||||
} else if (token == TOK_STRING) {
|
||||
script.addInstruction(I_PUSH_CONST, to_script(token.value));
|
||||
} else {
|
||||
throw ScriptParseError(_("Unexpected token '") + token.value + _("'"));
|
||||
input.expected(_("expression"));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -459,11 +514,10 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc
|
||||
// not an expression. Remove that instruction.
|
||||
Instruction instr = script.getInstructions().back();
|
||||
if (instr.instr != I_GET_VAR) {
|
||||
throw ScriptParseError(_("Can only assign to variables"));
|
||||
} else {
|
||||
script.getInstructions().pop_back();
|
||||
parseOper(input, script, PREC_SET, I_SET_VAR, instr.data);
|
||||
input.add_error(_("Can only assign to variables"));
|
||||
}
|
||||
script.getInstructions().pop_back();
|
||||
parseOper(input, script, PREC_SET, I_SET_VAR, instr.data);
|
||||
}
|
||||
else if (minPrec <= PREC_AND && token==_("and")) parseOper(input, script, PREC_CMP, I_BINARY, I_AND);
|
||||
else if (minPrec <= PREC_AND && token==_("or" )) parseOper(input, script, PREC_CMP, I_BINARY, I_OR);
|
||||
@@ -484,7 +538,7 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc
|
||||
if (token == TOK_NAME || token == TOK_INT || token == TOK_DOUBLE || token == TOK_STRING) {
|
||||
script.addInstruction(I_MEMBER_C, token.value);
|
||||
} else {
|
||||
throw ScriptParseError(_("name"), input.peek().value);
|
||||
input.expected(_("name"));
|
||||
}
|
||||
} else if (minPrec <= PREC_FUN && token==_("[")) { // get member by expr
|
||||
parseOper(input, script, PREC_ALL, I_BINARY, I_MEMBER);
|
||||
@@ -528,14 +582,15 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc
|
||||
} else {
|
||||
parseOper(input, script, PREC_ALL, I_BINARY, I_ADD); // e
|
||||
}
|
||||
expectToken(input, _("}\""));
|
||||
parseOper(input, script, PREC_NONE); // y
|
||||
// optimize: e + "" -> e
|
||||
i = script.getInstructions().back();
|
||||
if (i.instr == I_PUSH_CONST && script.getConstants()[i.data]->toString().empty()) {
|
||||
script.getInstructions().pop_back();
|
||||
} else {
|
||||
script.addInstruction(I_BINARY, I_ADD);
|
||||
if (expectToken(input, _("}\""), _("}"))) {
|
||||
parseOper(input, script, PREC_NONE); // y
|
||||
// optimize: e + "" -> e
|
||||
i = script.getInstructions().back();
|
||||
if (i.instr == I_PUSH_CONST && script.getConstants()[i.data]->toString().empty()) {
|
||||
script.getInstructions().pop_back();
|
||||
} else {
|
||||
script.addInstruction(I_BINARY, I_ADD);
|
||||
}
|
||||
}
|
||||
} else if (minPrec <= PREC_NEWLINE && token.newline) {
|
||||
// newline functions as ;
|
||||
|
||||
+13
-1
@@ -10,13 +10,25 @@
|
||||
// ----------------------------------------------------------------------------- : Includes
|
||||
|
||||
#include <util/prec.hpp>
|
||||
#include <util/error.hpp>
|
||||
#include <script/script.hpp>
|
||||
|
||||
// ----------------------------------------------------------------------------- : Parser
|
||||
|
||||
/// Parse a String to a Script
|
||||
/** If string_mode then s is interpreted as a string,
|
||||
* escaping to script mode can be done with {}
|
||||
* escaping to script mode can be done with {}.
|
||||
*
|
||||
* Errors are stored in the output vector.
|
||||
* If there are errors, the result is a null pointer
|
||||
*/
|
||||
ScriptP parse(const String& s, bool string_mode, vector<ScriptParseError>& errors_out);
|
||||
|
||||
/// Parse a String to a Script
|
||||
/** If string_mode then s is interpreted as a string,
|
||||
* escaping to script mode can be done with {}.
|
||||
*
|
||||
* If an error is encountered, an exception is thrown.
|
||||
*/
|
||||
ScriptP parse(const String& s, bool string_mode = false);
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <util/error.hpp>
|
||||
|
||||
DECLARE_TYPEOF_COLLECTION(ScriptParseError);
|
||||
|
||||
// ----------------------------------------------------------------------------- : Error types
|
||||
|
||||
Error::Error(const String& message)
|
||||
@@ -20,6 +22,32 @@ String Error::what() const {
|
||||
return message;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------- : Parse errors
|
||||
|
||||
ScriptParseError::ScriptParseError(size_t pos, const String& error)
|
||||
: start(pos), end(pos)
|
||||
, ParseError(error)
|
||||
{}
|
||||
ScriptParseError::ScriptParseError(size_t pos, const String& exp, const String& found)
|
||||
: start(pos), end(pos + found.size())
|
||||
, ParseError(_("Expected '") + exp + _("' instead of '") + found + _("'"))
|
||||
{}
|
||||
String ScriptParseError::what() const {
|
||||
return String(_("(")) << (int)start << _("): ") << Error::what();
|
||||
}
|
||||
|
||||
String concat(const vector<ScriptParseError>& errors) {
|
||||
String total;
|
||||
FOR_EACH_CONST(e, errors) {
|
||||
if (!total.empty()) total += _("\n");
|
||||
total += e.what();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
ScriptParseErrors::ScriptParseErrors(const vector<ScriptParseError>& errors)
|
||||
: ParseError(concat(errors))
|
||||
{}
|
||||
|
||||
// ----------------------------------------------------------------------------- : Error handling
|
||||
|
||||
// Errors for which a message box was already shown
|
||||
|
||||
+12
-3
@@ -75,9 +75,18 @@ class FileParseError : public ParseError {
|
||||
/// Parse error in a script
|
||||
class ScriptParseError : public ParseError {
|
||||
public:
|
||||
inline ScriptParseError(const String& str) : ParseError(str) {}
|
||||
inline ScriptParseError(const String& exp, const String& found)
|
||||
: ParseError(_("Expected '") + exp + _("' instead of '") + found + _("'")) {}
|
||||
ScriptParseError(size_t pos, const String& str);
|
||||
ScriptParseError(size_t pos, const String& expected, const String& found);
|
||||
/// Position of the error
|
||||
size_t start, end;
|
||||
/// Return the error message
|
||||
virtual String what() const;
|
||||
};
|
||||
|
||||
/// Multiple parse errors in a script
|
||||
class ScriptParseErrors : public ParseError {
|
||||
public:
|
||||
ScriptParseErrors(const vector<ScriptParseError>& errors);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------- : Script errors
|
||||
|
||||
Reference in New Issue
Block a user