Commit b051d010 authored by twanvl's avatar twanvl

improved error reporting for the keyword editor

parent eb29499a
...@@ -60,7 +60,7 @@ menu: ...@@ -60,7 +60,7 @@ menu:
set info tab: &Set Information F6 set info tab: &Set Information F6
style tab: St&yle F7 style tab: St&yle F7
keywords tab: &Keywords F8 keywords tab: &Keywords F8
stats tab: S&tatistics F8 stats tab: S&tatistics F9
help: &Help help: &Help
index: &Index... F1 index: &Index... F1
...@@ -331,6 +331,9 @@ label: ...@@ -331,6 +331,9 @@ label:
mode: Mode mode: Mode
uses: Uses uses: Uses
reminder: Reminder text 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 # Open dialogs
all files All files all files All files
......
...@@ -84,27 +84,30 @@ void KeywordReminderTextValue::store() { ...@@ -84,27 +84,30 @@ void KeywordReminderTextValue::store() {
retrieve(); retrieve();
return; return;
} }
// Re-highlight // new value
String new_value = untag(value); String new_value = untag(value);
highlight(new_value);
// Try to parse the script // Try to parse the script
try { vector<ScriptParseError> parse_errors;
ScriptP new_script = parse(new_value, true); ScriptP new_script = parse(new_value, true, parse_errors);
if (parse_errors.empty()) {
// parsed okay, assign // parsed okay, assign
errors.clear(); errors.clear();
keyword.reminder.getScriptP() = new_script; keyword.reminder.getScriptP() = new_script;
keyword.reminder.getUnparsed() = new_value; keyword.reminder.getUnparsed() = new_value;
} catch (const Error& e) { } else {
// parse errors, report // 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() { 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 // Add tags to indicate code / syntax highlight
// i.e. bla {if code "x" } bla // i.e. bla {if code "x" } bla
// becomes: // becomes:
...@@ -112,7 +115,24 @@ void KeywordReminderTextValue::highlight(const String& code) { ...@@ -112,7 +115,24 @@ void KeywordReminderTextValue::highlight(const String& code) {
String new_value; String new_value;
int in_brace = 0; int in_brace = 0;
bool in_string = true; bool in_string = true;
vector<ScriptParseError>::const_iterator error = errors.begin();
for (size_t pos = 0 ; pos < code.size() ; ) { 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); Char c = code.GetChar(pos);
if (c == _('<')) { if (c == _('<')) {
new_value += _('\1'); // escape new_value += _('\1'); // escape
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include <util/prec.hpp> #include <util/prec.hpp>
#include <util/action_stack.hpp> #include <util/action_stack.hpp>
#include <util/error.hpp>
#include <data/field/text.hpp> #include <data/field/text.hpp>
class Set; class Set;
...@@ -93,7 +94,7 @@ class KeywordReminderTextValue : public KeywordTextValue { ...@@ -93,7 +94,7 @@ class KeywordReminderTextValue : public KeywordTextValue {
virtual void retrieve(); virtual void retrieve();
/// Syntax highlight, and store in value /// Syntax highlight, and store in value
void highlight(const String& code); void highlight(const String& code, const vector<ScriptParseError>& errors);
}; };
// ----------------------------------------------------------------------------- : EOF // ----------------------------------------------------------------------------- : EOF
......
...@@ -33,3 +33,23 @@ void AtomTextElement::draw(RotatedDC& dc, double scale, const RealRect& rect, co ...@@ -33,3 +33,23 @@ void AtomTextElement::draw(RotatedDC& dc, double scale, const RealRect& rect, co
} }
CompoundTextElement::draw(dc, scale, rect, xs, what, start, end); 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);
}
\ No newline at end of file
...@@ -73,18 +73,19 @@ struct TextElementsFromString { ...@@ -73,18 +73,19 @@ struct TextElementsFromString {
// What formatting is enabled? // What formatting is enabled?
int bold, italic, symbol; int bold, italic, symbol;
int soft, kwpph, param, line; 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; int param_id;
bool bracket; bool bracket;
TextElementsFromString() TextElementsFromString()
: bold(0), italic(0), symbol(0), soft(0), kwpph(0), param(0), line(0) : 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) {} , param_id(0), bracket(false) {}
// read TextElements from a string // read TextElements from a string
void fromString(TextElements& te, const String& text, size_t start, size_t end, const TextStyle& style, Context& ctx) { void fromString(TextElements& te, const String& text, size_t start, size_t end, const TextStyle& style, Context& ctx) {
te.elements.clear(); te.elements.clear();
end = min(end, text.size());
// for each character... // for each character...
for (size_t pos = start ; pos < end ; ) { for (size_t pos = start ; pos < end ; ) {
Char c = text.GetChar(pos); Char c = text.GetChar(pos);
...@@ -126,11 +127,18 @@ struct TextElementsFromString { ...@@ -126,11 +127,18 @@ struct TextElementsFromString {
else if (is_substr(text, tag_start, _("</line"))) line -= 1; else if (is_substr(text, tag_start, _("</line"))) line -= 1;
else if (is_substr(text, tag_start, _("<atom"))) { else if (is_substr(text, tag_start, _("<atom"))) {
// 'atomic' indicator // 'atomic' indicator
size_t end = match_close_tag(text, tag_start); size_t end_tag = min(end, match_close_tag(text, tag_start));
shared_ptr<AtomTextElement> e(new AtomTextElement(text, pos, end)); shared_ptr<AtomTextElement> e(new AtomTextElement(text, pos, end_tag));
fromString(e->elements, text, pos, end, style, ctx); fromString(e->elements, text, pos, end_tag, style, ctx);
te.elements.push_back(e); 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 { } else {
// ignore other tags // ignore other tags
} }
......
...@@ -164,6 +164,13 @@ class AtomTextElement : public CompoundTextElement { ...@@ -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; 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 // ----------------------------------------------------------------------------- : Other text elements
/* /*
......
...@@ -51,10 +51,11 @@ enum OpenBrace ...@@ -51,10 +51,11 @@ enum OpenBrace
, BRACE_PAREN // (, [, { , 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 { class TokenIterator {
public: 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 /// Peek at the next token, doesn't move to the one after that
/** Can peek further forward by using higher values of offset. /** Can peek further forward by using higher values of offset.
...@@ -87,6 +88,14 @@ class TokenIterator { ...@@ -87,6 +88,14 @@ class TokenIterator {
void readToken(); void readToken();
/// Read the next token which is a string (after the opening ") /// Read the next token which is a string (after the opening ")
void readStringToken(); 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 // ----------------------------------------------------------------------------- : Characters
...@@ -102,10 +111,11 @@ bool isLongOper(const String& s) { return s==_(":=") || s==_("==") || s==_("!=") ...@@ -102,10 +111,11 @@ bool isLongOper(const String& s) { return s==_(":=") || s==_("==") || s==_("!=")
// ----------------------------------------------------------------------------- : Tokenizing // ----------------------------------------------------------------------------- : Tokenizing
TokenIterator::TokenIterator(const String& str, bool string_mode) TokenIterator::TokenIterator(const String& str, bool string_mode, vector<ScriptParseError>& errors)
: input(str) : input(str)
, pos(0) , pos(0)
, newline(false) , newline(false)
, errors(errors)
{ {
if (string_mode) { if (string_mode) {
open_braces.push(BRACE_STRING_MODE); open_braces.push(BRACE_STRING_MODE);
...@@ -221,7 +231,8 @@ void TokenIterator::readToken() { ...@@ -221,7 +231,8 @@ void TokenIterator::readToken() {
// comment untill end of line // comment untill end of line
while (pos < input.size() && input[pos] != _('\n')) ++pos; while (pos < input.size() && input[pos] != _('\n')) ++pos;
} else { } 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() { ...@@ -234,7 +245,10 @@ void TokenIterator::readStringToken() {
addToken(TOK_STRING, str); addToken(TOK_STRING, str);
return; return;
} else { } 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++); Char c = input.GetChar(pos++);
...@@ -246,7 +260,12 @@ void TokenIterator::readStringToken() { ...@@ -246,7 +260,12 @@ void TokenIterator::readStringToken() {
return; return;
} else if (c == _('\\')) { } else if (c == _('\\')) {
// escape // 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++); c = input.GetChar(pos++);
if (c == _('n')) str += _('\n'); if (c == _('n')) str += _('\n');
else if (c == _('<')) str += _('\1'); // escape for < else if (c == _('<')) str += _('\1'); // escape for <
...@@ -264,8 +283,20 @@ void TokenIterator::readStringToken() { ...@@ -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 // ----------------------------------------------------------------------------- : Parsing
/// Precedence levels for parsing, higher = tighter /// Precedence levels for parsing, higher = tighter
enum Precedence enum Precedence
{ PREC_ALL { PREC_ALL
...@@ -300,22 +331,43 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec); ...@@ -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); 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); ScriptP script(new Script);
parseOper(input, *script, PREC_ALL, I_RET); parseOper(input, *script, PREC_ALL, I_RET);
if (input.peek() != TOK_EOF) { Token eof = input.read();
throw ScriptParseError(_("end of input"), input.peek().value); if (eof != TOK_EOF) {
} else { input.expected(_("end of input"));
}
// were there errors?
if (errors_out.empty()) {
return script; return script;
} else {
return ScriptP();
}
}
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, throws if it is not found
void expectToken(TokenIterator& input, const Char* expect) { // 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(); Token token = input.read();
if (token != expect) { if (token == expect) {
throw ScriptParseError(expect, token.value); 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) { ...@@ -367,7 +419,9 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec) {
// for each AAA in BBB do CCC // for each AAA in BBB do CCC
input.read(); // each input.read(); // each
Token name = input.read(); // AAA 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 expectToken(input, _("in")); // in
parseOper(input, script, PREC_SET); // BBB parseOper(input, script, PREC_SET); // BBB
script.addInstruction(I_UNARY, I_ITERATOR_C); // iterator_collection script.addInstruction(I_UNARY, I_ITERATOR_C); // iterator_collection
...@@ -428,7 +482,8 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec) { ...@@ -428,7 +482,8 @@ void parseExpr(TokenIterator& input, Script& script, Precedence minPrec) {
} else if (token == TOK_STRING) { } else if (token == TOK_STRING) {
script.addInstruction(I_PUSH_CONST, to_script(token.value)); script.addInstruction(I_PUSH_CONST, to_script(token.value));
} else { } else {
throw ScriptParseError(_("Unexpected token '") + token.value + _("'")); input.expected(_("expression"));
return;
} }
break; break;
} }
...@@ -459,11 +514,10 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc ...@@ -459,11 +514,10 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc
// not an expression. Remove that instruction. // not an expression. Remove that instruction.
Instruction instr = script.getInstructions().back(); Instruction instr = script.getInstructions().back();
if (instr.instr != I_GET_VAR) { if (instr.instr != I_GET_VAR) {
throw ScriptParseError(_("Can only assign to variables")); input.add_error(_("Can only assign to variables"));
} else {
script.getInstructions().pop_back();
parseOper(input, script, PREC_SET, I_SET_VAR, instr.data);
} }
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==_("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); 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 ...@@ -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) { if (token == TOK_NAME || token == TOK_INT || token == TOK_DOUBLE || token == TOK_STRING) {
script.addInstruction(I_MEMBER_C, token.value); script.addInstruction(I_MEMBER_C, token.value);
} else { } else {
throw ScriptParseError(_("name"), input.peek().value); input.expected(_("name"));
} }
} else if (minPrec <= PREC_FUN && token==_("[")) { // get member by expr } else if (minPrec <= PREC_FUN && token==_("[")) { // get member by expr
parseOper(input, script, PREC_ALL, I_BINARY, I_MEMBER); parseOper(input, script, PREC_ALL, I_BINARY, I_MEMBER);
...@@ -528,14 +582,15 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc ...@@ -528,14 +582,15 @@ void parseOper(TokenIterator& input, Script& script, Precedence minPrec, Instruc
} else { } else {
parseOper(input, script, PREC_ALL, I_BINARY, I_ADD); // e parseOper(input, script, PREC_ALL, I_BINARY, I_ADD); // e
} }
expectToken(input, _("}\"")); if (expectToken(input, _("}\""), _("}"))) {
parseOper(input, script, PREC_NONE); // y parseOper(input, script, PREC_NONE); // y
// optimize: e + "" -> e // optimize: e + "" -> e
i = script.getInstructions().back(); i = script.getInstructions().back();
if (i.instr == I_PUSH_CONST && script.getConstants()[i.data]->toString().empty()) { if (i.instr == I_PUSH_CONST && script.getConstants()[i.data]->toString().empty()) {
script.getInstructions().pop_back(); script.getInstructions().pop_back();
} else { } else {
script.addInstruction(I_BINARY, I_ADD); script.addInstruction(I_BINARY, I_ADD);
}
} }
} else if (minPrec <= PREC_NEWLINE && token.newline) { } else if (minPrec <= PREC_NEWLINE && token.newline) {
// newline functions as ; // newline functions as ;
......
...@@ -10,13 +10,25 @@ ...@@ -10,13 +10,25 @@
// ----------------------------------------------------------------------------- : Includes // ----------------------------------------------------------------------------- : Includes
#include <util/prec.hpp> #include <util/prec.hpp>
#include <util/error.hpp>
#include <script/script.hpp> #include <script/script.hpp>
// ----------------------------------------------------------------------------- : Parser // ----------------------------------------------------------------------------- : Parser
/// Parse a String to a Script /// Parse a String to a Script
/** If string_mode then s is interpreted as a string, /** 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); ScriptP parse(const String& s, bool string_mode = false);
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
#include <util/error.hpp> #include <util/error.hpp>
DECLARE_TYPEOF_COLLECTION(ScriptParseError);
// ----------------------------------------------------------------------------- : Error types // ----------------------------------------------------------------------------- : Error types
Error::Error(const String& message) Error::Error(const String& message)
...@@ -20,6 +22,32 @@ String Error::what() const { ...@@ -20,6 +22,32 @@ String Error::what() const {
return message; 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 // ----------------------------------------------------------------------------- : Error handling
// Errors for which a message box was already shown // Errors for which a message box was already shown
......
...@@ -75,9 +75,18 @@ class FileParseError : public ParseError { ...@@ -75,9 +75,18 @@ class FileParseError : public ParseError {
/// Parse error in a script /// Parse error in a script
class ScriptParseError : public ParseError { class ScriptParseError : public ParseError {
public: public:
inline ScriptParseError(const String& str) : ParseError(str) {} ScriptParseError(size_t pos, const String& str);
inline ScriptParseError(const String& exp, const String& found) ScriptParseError(size_t pos, const String& expected, const String& found);
: ParseError(_("Expected '") + exp + _("' instead of '") + 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 // ----------------------------------------------------------------------------- : Script errors
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment