diff --git a/indra/alscript/core/alscriptformatter.cpp b/indra/alscript/core/alscriptformatter.cpp index a5f4011979..634a3a53d9 100644 --- a/indra/alscript/core/alscriptformatter.cpp +++ b/indra/alscript/core/alscriptformatter.cpp @@ -140,10 +140,13 @@ namespace // The line's tokens spaced afresh, after its indentation: one space // around a binary operator, none around a unary one or inside - // brackets, one after a comma and none before; a trailing comment, - // and anything the rules do not name, spaced as written. - std::string spaced(const Line& line, bool lua) + // brackets, one after a comma and none before; inside Luau's braces + // as Options::braceSpaces asks, whatever stands next to them; a + // trailing comment, and anything the rules do not name, spaced as + // written. + std::string spaced(const Line& line, const ALScriptFormatter::Options& options) { + const bool lua = options.lua; std::string out; const Token* prev = nullptr; // The last token before prev that is neither blank nor a comment: @@ -157,6 +160,9 @@ namespace // statement, spaced from it; what follows a cast's is its operand. std::vector brackets; bool conditionClosed = false; + // Whether the token before is a < that opened an LSL vector, not + // a comparison: a sign after it is the first part's. + bool vectorOpened = false; for (const Token& t : line.tokens) { if (t.kind == Kind::Space) @@ -166,6 +172,8 @@ namespace } const bool closedCondition = conditionClosed; conditionClosed = false; + const bool openedVector = vectorOpened; + vectorOpened = false; if (t.kind == Kind::Punct && t.text == "(") { brackets.push_back(prev && prev->kind == Kind::Ident && (prev->text == "if" || prev->text == "while" || prev->text == "for")); @@ -182,6 +190,7 @@ namespace if (!lua && t.kind == Kind::Punct && t.text == "<") { ++vector; + vectorOpened = true; } out += t.text; prev = &t; @@ -205,7 +214,13 @@ namespace else if (t.kind == Kind::Punct) { const std::string& p = t.text; - if (p == "," || p == ";") + if (lua && a.kind == Kind::Punct && a.text == "{" && p != "}") + { + // A table's first part, or a table type's, whatever + // begins it: { { 1 } }, { [k] = v }, { -1 }. + say(options.braceSpaces); + } + else if (p == "," || p == ";") { say(false); } @@ -219,13 +234,14 @@ namespace { say(isKeyword(lua, a) && !(lua && a.text == "function")); } - else if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == ")" || a.text == "]" || a.text == "." || a.text == ":")) + else if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == ")" || a.text == "]" || a.text == ".")) { say(false); } else if (a.kind == Kind::Punct && isOperator(lua, a.text)) { - say(true); + // After a sign, nothing: -(a), #(t), !(b). + say(!(isUnary(lua, a.text) && (!earlier || !operand(lua, *earlier)))); } } else if (p == "[") @@ -242,6 +258,11 @@ namespace { say(false); } + else if (a.kind == Kind::Punct && isUnary(lua, a.text) && (!earlier || !operand(lua, *earlier))) + { + // A length of a table written out: #{1, 2}. + say(false); + } else { say(true); @@ -249,7 +270,7 @@ namespace } else if (p == "}") { - say(!(a.kind == Kind::Punct && a.text == "{")); + say(!(a.kind == Kind::Punct && a.text == "{") && (!lua || options.braceSpaces)); } else if (p == "." || p == "++" || p == "--") { @@ -262,6 +283,7 @@ namespace if (!aOperand) { ++vector; + vectorOpened = true; say(!(a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == "<" || a.text == "{"))); } else @@ -281,7 +303,9 @@ namespace // A sign or a not: spaced before as its neighbour // asks, and nothing after, which the next token's // turn will see to. - if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || a.text == "<" || a.text == "{" || a.text == "!" || a.text == "~" || a.text == "#")) + // Not after a < that compares: LSL's where an operand + // stood before it, and Luau's everywhere. + if (a.kind == Kind::Punct && (a.text == "(" || a.text == "[" || (a.text == "<" && openedVector) || a.text == "{" || a.text == "!" || a.text == "~" || a.text == "#")) { say(false); } @@ -289,8 +313,10 @@ namespace { say(true); } - else if (a.kind == Kind::Punct && isUnary(lua, a.text)) + else if (a.kind == Kind::Punct && isUnary(lua, a.text) && (!earlier || !operand(lua, *earlier))) { + // A sign after a sign: -#t. Not after a minus + // that subtracts, x - #t. say(false); } else @@ -317,7 +343,7 @@ namespace } else if (p == "{") { - say(true); + say(!lua || options.braceSpaces); } else if (p == "," || p == ";") { @@ -793,7 +819,7 @@ namespace { Line line; line.tokens = tokens; - return indentText(options, indent) + spaced(line, options.lua); + return indentText(options, indent) + spaced(line, options); } // The tokens as pieces no wider than the width where a bracket's commas @@ -928,6 +954,320 @@ namespace return out; } + // How far in the author wrote a line, tabs to their stops. + S32 writtenIndent(const Line& line, S32 tab) + { + S32 column = 0; + if (!line.tokens.empty() && line.tokens.front().kind == Kind::Space) + { + for (const char c : line.tokens.front().text) + { + column = c == '\t' ? (column / tab + 1) * tab : column + 1; + } + } + return column; + } + + // Whether a line holds a comment and nothing else. + bool commentOnly(const Line& line) + { + bool comment = false; + for (const Token& t : line.tokens) + { + if (t.kind == Kind::Comment) + { + comment = true; + } + else if (t.kind != Kind::Space) + { + return false; + } + } + return comment && !line.directive && line.first == line.last; + } + + // A comment over a line that goes back out -- `elseif`, `else`, + // `until`, `end`, `}` -- is about that line where the author wrote it + // out of the block: no further in than that line, and less far than + // the code over it in the block, or, in a block with none, in text + // the author indented. It goes in as far as the line. One written as + // far in as the block, or in text with no indentation to tell, is the + // end of the block, and stays in it. + void overWhatFollows(std::vector& lines, const ALScriptFormatter::Options& options) + { + const S32 tab = options.indent > 0 ? options.indent : 4; + const auto code = [&](const Line& line) { return !line.blank && !line.directive && !commentOnly(line); }; + const bool indented = std::any_of(lines.begin(), lines.end(), + [&](const Line& line) { return code(line) && line.indent > 0 && writtenIndent(line, tab) > 0; }); + for (size_t i = 1; i < lines.size(); ++i) + { + const Line& next = lines[i]; + if (!code(next)) + { + continue; + } + size_t top = i; + while (top > 0 && commentOnly(lines[top - 1])) + { + --top; + } + if (top == i) + { + continue; + } + size_t over = top; + while (over > 0 && !code(lines[over - 1])) + { + --over; + } + if (over == 0) + { + continue; + } + // The code over them, in the block or the line that opens it. + const Line& above = lines[over - 1]; + const S32 written = writtenIndent(next, tab); + const auto out = [&](const Line& comment) { + const S32 at = writtenIndent(comment, tab); + return at <= written && (above.indent < comment.indent ? indented : at < writtenIndent(above, tab)); + }; + for (size_t j = i; j > top && lines[j - 1].indent > next.indent && out(lines[j - 1]); --j) + { + lines[j - 1].indent = next.indent; + } + } + } + + // Where the author put a line's `=` -- its first outside brackets -- + // or its trailing comment: the token, and its column from where the + // line's code starts, tabs to their stops; and whether the blank + // before it is wider than one space, which is what says it was put + // there. + struct Mark + { + size_t token = std::string::npos; + S32 column = 0; + bool padded = false; + }; + + struct Marks + { + Mark equals; + Mark comment; + // A comment and nothing else, which a run goes on past. + bool through = false; + }; + + Marks marksOf(const Line& line, S32 tab) + { + Marks marks; + marks.through = commentOnly(line); + if (line.blank || line.directive || line.first != line.last || marks.through) + { + return marks; + } + S32 column = 0; + S32 start = -1; + S32 depth = 0; + bool code = false; + for (size_t i = 0; i < line.tokens.size(); ++i) + { + const Token& t = line.tokens[i]; + if (t.kind != Kind::Space && start < 0) + { + start = column; + } + const bool padded = i > 0 && line.tokens[i - 1].kind == Kind::Space && line.tokens[i - 1].text != " "; + if (t.kind == Kind::Punct) + { + if (t.text == "(" || t.text == "[" || t.text == "{") + { + ++depth; + } + else if (t.text == ")" || t.text == "]" || t.text == "}") + { + // Closing what an earlier line opened: no statement's =. + depth = depth > 0 ? depth - 1 : -1000; + } + else if (t.text == "=" && depth == 0 && code && marks.equals.token == std::string::npos) + { + marks.equals = { i, column - start, padded }; + } + } + if (t.kind == Kind::Comment && code) + { + marks.comment = { i, column - start, padded }; + } + else if (t.kind != Kind::Space) + { + // Anything after a comment is no trailing one. + marks.comment = Mark(); + code = true; + } + for (const char c : t.text) + { + column = c == '\t' ? (column / tab + 1) * tab : column + ((static_cast(c) & 0xC0) != 0x80 ? 1 : 0); + } + } + if (marks.comment.token != std::string::npos && marks.equals.token > marks.comment.token) + { + marks.equals = Mark(); + } + return marks; + } + + // What the author lined up, lined up again once each line is spaced + // afresh: a run of lines one after another, as deep as each other, + // whose `=`, or whose trailing comments, the author put at one column + // and spaced wider than one somewhere to get there. Each goes to that + // column again, or just past the longest line where it no longer + // fits. A run of comments none of whose lines' code changed is left + // as it was, its blanks as written. `code` holds each line written + // out where it is the formatter's to write. + void lineUp(const std::vector& lines, const ALScriptFormatter::Options& options, std::vector>& code) + { + const S32 tab = options.indent > 0 ? options.indent : 4; + std::vector marks; + marks.reserve(lines.size()); + for (const Line& line : lines) + { + marks.push_back(marksOf(line, tab)); + } + // Each run of the marks `of` picks, from `from` to `to`, one past, + // a comment as far in on a line of its own going on past; each + // line of it given `each` has its mark. + const auto runs = [&](auto of, auto&& each) { + for (size_t from = 0; from < lines.size();) + { + const Mark first = of(marks[from]); + if (first.token == std::string::npos) + { + ++from; + continue; + } + size_t to = from; + size_t marked = 0; + bool padded = false; + for (size_t at = from; at < lines.size() && lines[at].indent == lines[from].indent; ++at) + { + if (marks[at].through) + { + continue; + } + if (of(marks[at]).token == std::string::npos || of(marks[at]).column != first.column) + { + break; + } + padded = padded || of(marks[at]).padded; + ++marked; + to = at + 1; + } + if (marked >= 2 && padded) + { + each(from, to, first.column); + } + from = to; + } + }; + const auto lead = [&](size_t i) { return indentText(options, lines[i].indent).size(); }; + runs([](const Marks& m) { return m.equals; }, + [&](size_t from, size_t to, S32 column) { + // Where each = is now: past its code as spaced, which is + // what the line written out begins with, and one space. + std::vector> at(to - from, { std::string::npos, 0 }); + S32 most = column; + for (size_t i = from; i < to; ++i) + { + if (!code[i] || marks[i].through) + { + continue; + } + Line before; + before.tokens.assign(lines[i].tokens.begin(), lines[i].tokens.begin() + static_cast(marks[i].equals.token)); + std::string prefix = spaced(before, options); + while (!prefix.empty() && (prefix.back() == ' ' || prefix.back() == '\t')) + { + prefix.pop_back(); + } + const size_t equals = lead(i) + prefix.size() + 1; + if (equals < code[i]->size() && (*code[i])[equals] == '=' && (*code[i])[equals - 1] == ' ') + { + at[i - from] = { equals, columns(prefix, 1) + 1 }; + most = std::max(most, at[i - from].second); + } + } + for (size_t i = from; i < to; ++i) + { + if (at[i - from].first != std::string::npos) + { + code[i]->insert(at[i - from].first, static_cast(most - at[i - from].second), ' '); + } + } + }); + runs([](const Marks& m) { return m.comment; }, + [&](size_t from, size_t to, S32 column) { + // Each line's code, as written out now, and its comment. + struct Split + { + size_t end = std::string::npos; + S32 columns = 0; + }; + std::vector at(to - from); + S32 most = column; + bool changed = false; + for (size_t i = from; i < to; ++i) + { + if (!code[i] || marks[i].through) + { + continue; + } + std::string comment = lines[i].tokens[marks[i].comment.token].text; + while (!comment.empty() && (comment.back() == ' ' || comment.back() == '\t' || comment.back() == '\r')) + { + comment.pop_back(); + } + const std::string& text = *code[i]; + if (comment.empty() || text.size() < comment.size() + lead(i) || + text.compare(text.size() - comment.size(), comment.size(), comment) != 0) + { + continue; + } + size_t end = text.size() - comment.size(); + while (end > lead(i) && (text[end - 1] == ' ' || text[end - 1] == '\t')) + { + --end; + } + const std::string_view now = std::string_view(text).substr(lead(i), end - lead(i)); + std::string was; + for (size_t t = 0; t < marks[i].comment.token; ++t) + { + was += was.empty() && lines[i].tokens[t].kind == Kind::Space ? std::string() : lines[i].tokens[t].text; + } + while (!was.empty() && (was.back() == ' ' || was.back() == '\t')) + { + was.pop_back(); + } + changed = changed || now != was; + at[i - from] = { end, columns(now, 1) }; + most = std::max(most, at[i - from].columns + 1); + } + if (!changed) + { + return; + } + for (size_t i = from; i < to; ++i) + { + if (at[i - from].end != std::string::npos) + { + std::string& text = *code[i]; + const size_t comment = text.find_first_not_of(" \t", at[i - from].end); + text = text.substr(0, at[i - from].end) + + std::string(static_cast(most - at[i - from].columns), ' ') + text.substr(comment); + } + } + }); + } + // Each line as written out, or none for a blank line past a run's // length where the whole text is asked for; past the width broken // where `wrap`. @@ -938,15 +1278,46 @@ namespace S32 blanks = 0; size_t kept = 0; const bool whole = first < 0; + const auto ours = [&](const Line& line) { return whole || (line.first >= first && line.last <= last); }; + // Each line of code the formatter writes, spaced, and lined up + // where the author lined them up, before any is broken. + std::vector> code(lines.size()); + for (size_t i = 0; i < lines.size(); ++i) + { + const Line& line = lines[i]; + if (!ours(line) || line.directive || line.blank) + { + continue; + } + std::string text = indentText(options, line.indent); + if (options.spacing) + { + text += spaced(line, options); + } + else + { + std::string body = asWritten(line); + size_t at = body.find_first_not_of(" \t"); + text += at == std::string::npos ? std::string() : body.substr(at); + } + while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) + { + text.pop_back(); + } + code[i] = std::move(text); + } + if (options.spacing) + { + lineUp(lines, options, code); + } for (size_t i = 0; i < lines.size(); ++i) { const Line& line = lines[i]; - const bool ours = whole || (line.first >= first && line.last <= last); std::string text; - if (!ours || line.directive) + if (!ours(line) || line.directive) { text = asWritten(line); - if (ours) + if (ours(line)) { while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) { @@ -972,21 +1343,7 @@ namespace } else { - text = indentText(options, line.indent); - if (options.spacing) - { - text += spaced(line, options.lua); - } - else - { - std::string body = asWritten(line); - size_t at = body.find_first_not_of(" \t"); - text += at == std::string::npos ? std::string() : body.substr(at); - } - while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) - { - text.pop_back(); - } + text = std::move(*code[i]); if (wrap) { text = wrapped(line, options, text); @@ -1043,6 +1400,7 @@ std::string ALScriptFormatter::format(std::string_view text, const Options& opti bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); + overWhatFollows(lines, options); return emit(lines, options, endsWithNewline, -1, -1, true); } @@ -1053,6 +1411,7 @@ std::string ALScriptFormatter::formatLines(std::string_view text, const Options& bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); + overWhatFollows(lines, options); return emit(lines, options, endsWithNewline, first, last, false); } @@ -1063,6 +1422,7 @@ std::vector ALScriptFormatter::formatEach(std::string_view text, co bool endsWithNewline = false; std::vector lines = linesOf(text, options.lua, endsWithNewline); decide(lines, options.lua); + overWhatFollows(lines, options); // Every line kept, blank or not: from the first where `first` is // below it, which would otherwise ask for the whole text's runs. const std::vector> each = writtenLines(lines, options, llmax(first, 0), last, true); diff --git a/indra/alscript/core/alscriptformatter.h b/indra/alscript/core/alscriptformatter.h index a7db72496f..a3a70d1c6b 100644 --- a/indra/alscript/core/alscriptformatter.h +++ b/indra/alscript/core/alscriptformatter.h @@ -39,7 +39,7 @@ // or `else` without braces -- and within a line the spaces are put // where the language reads best: one around a binary operator, none // around a unary one or inside brackets, one after a comma, none -// before it. What the author spaced deliberately is left alone: the +// before it, and inside Luau's braces as asked. What the author spaced deliberately is left alone: the // gap before a trailing comment, a preprocessor line, anything inside // a string or a comment. Runs of blank lines are shortened and trailing // whitespace dropped, unless only some lines are asked for, in which @@ -59,6 +59,10 @@ class ALScriptFormatter // Whether the spaces within a line are touched at all, or only // the indentation. bool spacing = true; + // Luau: a space inside a table's braces, and a table type's, as + // StyLua writes them -- { 1, 2 }, { any } -- or none, {1, 2}. + // Never inside {}, nor an interpolated string's. + bool braceSpaces = true; // How many columns a line may take before it is broken, a tab as // wide as a level: at the commas of the widest bracket on it that // has any, or of the table or list a call ends with, which stays diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 1a999d7f1c..d90b8fc889 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -182,14 +182,28 @@ namespace }; // An expression as written: its text, how tightly it binds, and whether - // it is a Luau boolean where LSL has 1 or 0. + // it is a Luau boolean where LSL has 1 or 0. Where it is a find's + // place counted as LSL's, `(x or 0) - 1`: x, SLua's place, counted + // from 1 or nil. Where it is a call that answers more than one value, + // kept to its first by brackets, `(math.modf(x))`: the call bare. struct Expr { std::string text; int prec = PRIMARY; bool boolean = false; + std::string place; + std::string bare; }; + // As one value, where only its first is read anyway -- an operator's + // operand, the one value assigned -- a call without the brackets that + // keep it to one. Where its values would spread -- a call's last + // argument, a table's last item, a return -- the brackets stay. + Expr oneValue(const Expr& e) + { + return e.bare.empty() ? e : Expr{ e.bare, PRIMARY, e.boolean }; + } + // Pairs of words, looked for by views of them without a copy. struct PairLess { @@ -452,6 +466,12 @@ namespace void findComments(); void place(size_t k, const std::vector& children, LSLASTNode* holder); bool placeInside(size_t k, LSLASTNode* node); + // A comment of one line after a bare statement, on the line it + // ends: given to the statement that ends that line (trailedBare), + // after it, or, where that is an empty body, after the line that + // opens it. Not where that is an if or a loop over lines, whose + // own line is its first. Whether it was. + bool trailBare(size_t k, LSLASTNode* statement, bool body); void placeIn(size_t k, LSLASTNode* compound); // Those given to a node, written over it; those at a holder's end, // with any of what it held that nothing wrote, at its end. Those @@ -582,6 +602,9 @@ namespace // Where each of the LSL's lines starts, from the first. const std::vector& lineStarts(); std::vector mLineStarts; + // Whether a line of the LSL lies blank between two others, counted + // from one, neither of them itself. + bool blankBetween(S32 after, S32 before); Expr lvalue(LSLLValueExpression* e); Expr call(LSLFunctionExpression* e); // The call as written, and the name SLua calls it by where it is @@ -659,12 +682,21 @@ namespace std::string llArgs(LSLFunctionExpression* e, U16 indexes); // `: type` for an LSL type, where types are written. std::string typed(LSLIType type) const; + // A table's braces round what it holds, or a table type's: spaced + // inside or not as Options::braceSpaces asks; {} round nothing. + std::string braced(const std::string& inside) const; // --- statements ------------------------------------------------------------- void statement(LSLASTNode* s, bool last); void statementBody(LSLASTNode* s, bool last); void block(LSLASTNode* s); + // A blank line over a statement or a global, where the LSL had one + // or more between it and the one written before it, which grouped + // them: one, and not where nothing has been written since `opened` + // -- a block's start -- nor where a comment over it keeps the + // blank line over itself (Comment::apart). + void keepApart(LSLASTNode* before, LSLASTNode* node, size_t opened); // An if written from `at` in the text, `lines` lines down, its // stretches from `spans`, put on one line where it is one statement // and nothing said over it: if c then s end. Not called where a @@ -904,6 +936,9 @@ namespace // What each part of the script may change, for LSL's order. ALLSLEffects mEffects; std::string mText; + // Where the block being written began in mText: no blank line kept + // over what is written first in it. + size_t mBlockStart = std::string::npos; // The LSL's stretches beside the SLua's made of them, the SLua's // counted in mText until the head goes over it, and its last lines // worked out once all is written (endSpans); the LSL lines of the @@ -1521,6 +1556,69 @@ namespace default: return true; } } + + // The statement that ends a bare one's last line: down an if's last + // branch, a while's or a for's body. Not into an if with no else on + // one line, which is written on one (Writer::onOneLine), but for an + // else's, which is an elseif. + LSLASTNode* trailedBare(LSLASTNode* node) + { + bool elseif = false; + while (!isNull(node) && node->getNodeType() == NODE_STATEMENT) + { + switch (node->getNodeSubType()) + { + case NODE_IF_STATEMENT: + { + LSLASTNode* no = node->getChild(2); + if (isNull(no) && !elseif && node->getLoc()->first_line == node->getLoc()->last_line) + { + return node; + } + elseif = !isNull(no) && no->getNodeSubType() == NODE_IF_STATEMENT; + node = isNull(no) ? node->getChild(1) : no; + continue; + } + case NODE_WHILE_STATEMENT: + elseif = false; + node = node->getChild(1); + continue; + case NODE_FOR_STATEMENT: + elseif = false; + node = node->getChild(3); + continue; + default: return node; + } + } + return node; + } + } + + bool Writer::trailBare(size_t k, LSLASTNode* statement, bool body) + { + const Pos at = mComments[k].at; + if (isNull(statement) || !endsBare(statement) || mComments[k].endLine != at.line || at.line != statement->getLoc()->last_line) + { + return false; + } + LSLASTNode* trailed = trailedBare(statement); + const LSLNodeSubType type = trailed->getNodeSubType(); + const bool control = type == NODE_IF_STATEMENT || type == NODE_WHILE_STATEMENT || type == NODE_FOR_STATEMENT || type == NODE_DO_STATEMENT; + if (control && trailed->getLoc()->first_line != trailed->getLoc()->last_line) + { + return false; + } + // An empty body writes no line of its own: the comment trails the + // one that opens it, `if c then -- why`. + if (type == NODE_NOP_STATEMENT && (body || trailed != statement)) + { + mCommentsOpening[trailed].push_back(k); + } + else + { + mCommentsAfter[trailed].push_back(k); + } + return true; } void Writer::place(size_t k, const std::vector& children, LSLASTNode* holder) @@ -1549,19 +1647,11 @@ namespace break; } // On the line a bare statement ends, after it: about that statement, - // and after it too, where it is one line of the comment's and the - // statement one line, or no if or loop, whose first line is not - // where the comment was. + // and after the one that ends the line (trailBare), or over it where + // it does not trail one. if (prev && endsBare(prev) && at.line == prev->getLoc()->last_line) { - const LSLNodeSubType type = prev->getNodeSubType(); - const bool control = type == NODE_IF_STATEMENT || type == NODE_WHILE_STATEMENT || type == NODE_FOR_STATEMENT || - type == NODE_DO_STATEMENT; - if (mComments[k].endLine == at.line && (!control || prev->getLoc()->first_line == prev->getLoc()->last_line)) - { - mCommentsAfter[prev].push_back(k); - } - else + if (!trailBare(k, prev, false)) { mCommentsBefore[prev].push_back(k); } @@ -1648,6 +1738,14 @@ namespace { return true; } + // On the line a bare branch ends, after it: after + // what ends that line, not over the else. + if (LSLASTNode* yes = node->getChild(1); !isNull(yes) && + !(at < Pos{ yes->getLoc()->last_line, yes->getLoc()->last_column }) && + trailBare(k, yes, true)) + { + return true; + } // Between the branches: over what the else runs -- an // elseif's line, or the else's first statement. LSLASTNode* no = node->getChild(2); @@ -1691,7 +1789,7 @@ namespace putTrailing(mText.size() - 1, indent() + c.text + "\n"); return; } - if (c.apart && !mText.empty() && !blank_over) + if (c.apart && !mText.empty() && !blank_over && mText.size() != mBlockStart) { mText += "\n"; } @@ -1994,7 +2092,7 @@ namespace { out += (out.empty() ? "" : ", ") + constant(item).text; } - return { "{" + out + "}" }; + return { braced(out) }; } default: return { "nil" }; @@ -2017,6 +2115,21 @@ namespace return mLineStarts; } + bool Writer::blankBetween(S32 after, S32 before) + { + const std::vector& starts = lineStarts(); + for (S32 line = std::max(after + 1, 1); line < before && static_cast(line) <= starts.size(); ++line) + { + const size_t from = starts[static_cast(line) - 1]; + const size_t to = std::min(mSource.find('\n', from), mSource.size()); + if (mSource.substr(from, to - from).find_first_not_of(" \t\r") == std::string_view::npos) + { + return true; + } + } + return false; + } + std::vector> Writer::numbersNear(LSLConstant* c) { std::vector> found; @@ -2550,6 +2663,47 @@ namespace return false; } + // A find's place counted as LSL's, compared with a whole number + // written out, on either side: compared in SLua's count instead, the + // number moved on by one -- `x == 1` -- or against nil where all it + // asks is whether anything was found. The same test whatever x is. + // Not an == or ~= with a number below -1, which no place is. + std::optional placeCompared(std::string_view word, const Expr& a, const Expr& b, LSLExpression* lhs, LSLExpression* rhs) + { + int v = 0; + const bool left = !a.place.empty() && wholeNumber(rhs, v); + const bool right = !left && !b.place.empty() && wholeNumber(lhs, v); + if (!left && !right) + { + return std::nullopt; + } + // With the place on the left. + std::string_view as = word; + if (right) + { + as = word == "<" ? ">" : word == ">" ? "<" : word == "<=" ? ">=" : word == ">=" ? "<=" : word; + } + const std::string& x = left ? a.place : b.place; + if ((as == "==" && v == -1) || (as == "<" && v == 0) || (as == "<=" && v == -1)) + { + return Expr{ x + " == nil", COMPARE, true }; + } + if ((as == "~=" && v == -1) || (as == ">=" && v == 0) || (as == ">" && v == -1)) + { + return Expr{ x + " ~= nil", COMPARE, true }; + } + const std::string n = std::to_string(static_cast(v) + 1); + if ((as == "==" || as == "~=") && v >= 0) + { + return Expr{ x + " " + std::string(as) + " " + n, COMPARE, true }; + } + if (as == "<" || as == ">" || as == "<=" || as == ">=") + { + return Expr{ "(" + x + " or 0) " + std::string(as) + " " + n, COMPARE, true }; + } + return std::nullopt; + } + // A number written out, an integer or a float, and whether it is // nought; or one with a minus before it. bool numberWritten(LSLExpression* e, bool& nought) @@ -2603,6 +2757,11 @@ namespace return static_cast(arg); } + std::string Writer::braced(const std::string& inside) const + { + return inside.empty() ? "{}" : mOptions.braceSpaces ? "{ " + inside + " }" : "{" + inside + "}"; + } + std::string Writer::typed(LSLIType type) const { if (!mOptions.types) @@ -2617,7 +2776,7 @@ namespace case LST_KEY: return ": uuid"; case LST_VECTOR: return ": vector"; case LST_QUATERNION: return ": quaternion"; - case LST_LIST: return ": { any }"; + case LST_LIST: return ": " + braced("any"); default: return std::string(); } } @@ -3095,11 +3254,12 @@ namespace // rule for a loop's range keeps alive, and not every // compiler has that rule. const std::optional> pieces = whitelistPieces(*textWritten(v)); + std::string each; for (std::string_view piece : *pieces) { - text += (text.empty() ? "{ " : ", ") + luaString(piece); + each += (each.empty() ? "" : ", ") + luaString(piece); } - text += " }"; + text = braced(each); break; } case Takes::Headers: @@ -3124,7 +3284,7 @@ namespace } for (const Gathered& g : gathered) { - fields[g.at] += "{ " + g.all + " }"; + fields[g.at] += braced(g.all); } // The serializer sends headers sorted by name: said where that is // not the order they were written in. @@ -3136,12 +3296,12 @@ namespace } // A key to a line where the LSL's list had more than one. const bool lines = list->getLoc()->first_line != list->getLoc()->last_line; - std::string table = lines ? "{\n" : "{ "; + std::string table; for (size_t i = 0; i < fields.size(); ++i) { table += lines ? listIndent(1) + fields[i] + ",\n" : (i ? ", " : "") + fields[i]; } - table += lines ? listIndent(0) + "}" : " }"; + table = lines ? "{\n" + table + listIndent(0) + "}" : braced(table); called = "ll." + lsl.substr(2); LSLParamList* params = e->getIdentifier()->getSymbol()->getFunctionDecl(); return Expr{ called + "(" + args(e->getArguments(), params, 0, call->at, table, true) + ")" }; @@ -3168,7 +3328,8 @@ namespace if (LSLASTNode* sought = soleItem(argumentAt(e, 1))) { called = "table.find"; - return Expr{ "(table.find(" + anyItems(argumentAt(e, 0), arg(0)) + ", " + itemText(sought) + ") or 0) - 1", ADD }; + const std::string found = "table.find(" + anyItems(argumentAt(e, 0), arg(0)) + ", " + itemText(sought) + ")"; + return Expr{ "(" + found + " or 0) - 1", ADD, false, found }; } } // A part of a list between two places written out, from its start @@ -3483,7 +3644,7 @@ namespace { const std::string args_text = (slua & ALLSLTraits::SluaIndexArgs) ? llArgs(e, indexes) : args(e->getArguments(), params, 0, -1, {}, true); called = "ll." + bare; - return { "(" + called + "(" + args_text + ") or 0) - 1", ADD }; + return { "(" + called + "(" + args_text + ") or 0) - 1", ADD, false, called + "(" + args_text + ")" }; } if (slua & ALLSLTraits::SluaRemoved) { @@ -3578,8 +3739,15 @@ namespace const LSLIType lt = slType(lhs); const LSLIType rt = slType(rhs); const auto infix = [&](const char* word, int prec, bool boolean = false, bool right_assoc = false) -> Expr { - const Expr a = value(lhs); - const Expr b = value(rhs); + const Expr a = oneValue(value(lhs)); + const Expr b = oneValue(value(rhs)); + if (prec == COMPARE && boolean) + { + if (std::optional compared = placeCompared(word, a, b, lhs, rhs)) + { + return *compared; + } + } return { bracketed(a, right_assoc ? prec + 1 : prec) + " " + word + " " + bracketed(b, right_assoc ? prec : prec + 1), prec, boolean }; }; // A find's answer asked only whether it found: against nil, as ll @@ -3683,13 +3851,13 @@ namespace std::string first = madeHere(lhs) ? anyItems(lhs, a) : "table.clone(" + a.text + ")"; if (lt != LST_LIST) { - first = "{" + a.text + "} :: { any }"; + first = braced(a.text) + " :: " + braced("any"); } else if (left->getNodeSubType() == NODE_BINARY_EXPRESSION && left->getOperation() == OP_PLUS) { first = a.text; } - return { "table.extend(" + first + ", " + (rt == LST_LIST ? b.text : "{" + b.text + "}") + ")" }; + return { "table.extend(" + first + ", " + (rt == LST_LIST ? b.text : braced(b.text)) + ")" }; } if (lt == LST_STRING || lt == LST_KEY || rt == LST_STRING || rt == LST_KEY) { @@ -3824,7 +3992,7 @@ namespace { // Bracketed where it begins with a minus of its own, which // after this one would be a comment. - const Expr v = value(child); + const Expr v = oneValue(value(child)); return { "-" + (!v.text.empty() && v.text.front() == '-' ? "(" + v.text + ")" : bracketed(v, UNARY)), UNARY }; } case OP_BOOLEAN_NOT: @@ -3893,8 +4061,9 @@ namespace case LST_INTEGER: if (from == LST_FLOATINGPOINT) { - // Toward zero, as LSL cuts a float. - return { "(math.modf(" + v.text + "))" }; + // Toward zero, as LSL cuts a float: math.modf's first + // value, its whole part. + return { "(math.modf(" + v.text + "))", PRIMARY, false, {}, "math.modf(" + v.text + ")" }; } if (from == LST_STRING) { @@ -3929,7 +4098,7 @@ namespace noteOnce(e, "SluaVectorText", "ll.DumpList2String writes a vector's or a rotation's parts as LSL did, with five places; SLua's tostring " "writes as few as each needs, <1, 2, 3>, which tovector reads back the same."); - return { "ll.DumpList2String({" + v.text + "}, \"\")" }; + return { "ll.DumpList2String(" + braced(v.text) + ", \"\")" }; default: return { "tostring(" + v.text + ")" }; } case LST_KEY: @@ -3949,7 +4118,7 @@ namespace // only where an operator around it binds more tightly than or. case LST_VECTOR: return { "tovector(" + v.text + ") or ZERO_VECTOR", OR }; case LST_QUATERNION: return { "toquaternion(" + v.text + ") or ZERO_ROTATION", OR }; - case LST_LIST: return { "{" + v.text + "}" }; + case LST_LIST: return { braced(v.text) }; default: return v; } } @@ -4048,7 +4217,7 @@ namespace auto* each = static_cast(item); out += (out.empty() ? "" : ", ") + coerced(each, each->getIType()).text; } - return { "{" + out + "}" }; + return { braced(out) }; } default: return { "nil" }; @@ -4125,12 +4294,12 @@ namespace { out += ","; } - to(begins(item), listIndent(0), i > 0); + to(begins(item), listIndent(0), i > 0 || mOptions.braceSpaces); // As LSL typed it: NULL_KEY in a list is LSL's string. out += coerced(item, item->getIType()).text; line = item->getLoc()->last_line; } - to(close, listIndent(-1), false); + to(close, listIndent(-1), mOptions.braceSpaces); out += "}"; --mListDepth; return out; @@ -4146,11 +4315,12 @@ namespace LSLASTNode* member = target->getMember(); // What the new value is, from the old one where the operator makes // it so. - const auto made = [&](const std::string& old, LSLIType t) -> std::string { + // `whole` where it is all of the value assigned, not a part of one. + const auto made = [&](const std::string& old, LSLIType t, bool whole = false) -> std::string { const Expr v = rhs ? value(rhs) : Expr{ "1" }; switch (op) { - case OP_ASSIGN: return rhs ? coerced(rhs, t).text : v.text; + case OP_ASSIGN: return rhs ? (whole ? oneValue(coerced(rhs, t)) : coerced(rhs, t)).text : v.text; case OP_ADD_ASSIGN: case OP_PRE_INCR: case OP_POST_INCR: @@ -4162,7 +4332,7 @@ namespace if (t == LST_LIST) { // Something else may hold the list: a new one. - return "table.extend(table.clone(" + old + "), " + (rhs && rhs->getIType() == LST_LIST ? v.text : "{" + v.text + "}") + ")"; + return "table.extend(table.clone(" + old + "), " + (rhs && rhs->getIType() == LST_LIST ? v.text : braced(v.text)) + ")"; } return old + " + " + bracketed(v, ADD + 1); case OP_SUB_ASSIGN: @@ -4280,7 +4450,9 @@ namespace line(name + " ..= " + joinedPiece(rhs).text); return; } - const Expr v = rhs ? value(rhs) : Expr{ "1" }; + // One value, but where it is math.fmod's last argument. + const Expr whole = rhs ? value(rhs) : Expr{ "1" }; + const Expr v = oneValue(whole); switch (op) { case OP_ADD_ASSIGN: @@ -4306,7 +4478,7 @@ namespace case OP_MOD_ASSIGN: if (type == LST_INTEGER) { - line(name + " = math.fmod(" + name + ", " + v.text + ")"); + line(name + " = math.fmod(" + name + ", " + whole.text + ")"); return; } break; @@ -4318,7 +4490,7 @@ namespace line(name + " = " + truthOf(rhs)); return; } - line(name + " = " + made(name, type)); + line(name + " = " + made(name, type, true)); } void Writer::effect(LSLExpression* e) @@ -4439,19 +4611,49 @@ namespace if (s->getNodeSubType() == NODE_COMPOUND_STATEMENT) { // What was after its brace that no line opening it took. + mBlockStart = mText.size(); commentsOpening(s); + const size_t opened = mText.size(); + LSLASTNode* before = nullptr; for (LSLASTNode* child = s->getChild(0); child; child = child->getNext()) { + keepApart(before, child, opened); statement(child, !child->getNext()); + before = isNull(child) || !child->getLoc() ? before : child; } lineUp(); commentsAtEnd(s); return; } + mBlockStart = mText.size(); statement(s, true); lineUp(); } + void Writer::keepApart(LSLASTNode* before, LSLASTNode* node, size_t opened) + { + if (!before || isNull(node) || !node->getLoc() || mText.size() == opened || + (mText.size() >= 2 && mText.compare(mText.size() - 2, 2, "\n\n") == 0)) + { + return; + } + const Pos begins = { node->getLoc()->first_line, node->getLoc()->first_column }; + if (const auto found = mCommentsBefore.find(node); found != mCommentsBefore.end()) + { + for (size_t k : found->second) + { + if (mComments[k].at < begins) + { + return; + } + } + } + if (blankBetween(before->getLoc()->last_line, begins.line)) + { + mText += "\n"; + } + } + void Writer::statement(LSLASTNode* s, bool last) { if (isNull(s)) @@ -4725,7 +4927,7 @@ namespace } else { - line("local " + name + typed(type) + " = " + (isNull(init) ? defaultOf(type) : coerced(init, type).text)); + line("local " + name + typed(type) + " = " + (isNull(init) ? defaultOf(type) : oneValue(coerced(init, type)).text)); } writeSteps(steps.after); return; @@ -5910,7 +6112,7 @@ namespace std::string Writer::anyItems(LSLExpression* e, const Expr& written) const { - return madeHere(e) ? bracketed(written, PRIMARY) + " :: { any }" : written.text; + return madeHere(e) ? bracketed(written, PRIMARY) + " :: " + braced("any") : written.text; } LSLASTNode* Writer::soleItem(LSLExpression* e) const @@ -6306,7 +6508,7 @@ namespace } else if (values.size() > 32) { - line("table.extend(" + name + ", {" + all + "})"); + line("table.extend(" + name + ", " + braced(all) + ")"); } else if (!values.empty()) { @@ -6548,7 +6750,7 @@ namespace { const std::string name = mNames.contains(var) ? mNames[var] : nameOf(var->getName()); const std::string parts = freshName(name + "Parts"); - line("local " + parts + (mOptions.types ? ": { string }" : "") + " = {}"); + line("local " + parts + (mOptions.types ? ": " + braced("string") : "") + " = {}"); mBuilding[var] = parts; joins.push_back({ name, parts, mDeclaredAtJoin.contains({ loop, var }) }); } @@ -7065,7 +7267,9 @@ namespace void Writer::globals() { - bool any = false; + bool any = false; + const size_t opened = mText.size(); + LSLASTNode* before = nullptr; for (LSLASTNode* g = mScript->getGlobals()->getChild(0); g; g = g->getNext()) { if (g->getNodeType() != NODE_GLOBAL_VARIABLE) @@ -7075,6 +7279,10 @@ namespace auto* global = static_cast(g); LSLIdentifier* id = global->getIdentifier(); LSLExpression* init = global->getInitializer(); + // Apart from the one before it as the LSL had them, or a + // function the LSL had between them, which is written later. + keepApart(before, global, opened); + before = global; commentsBefore(global); const size_t from = mText.size(); if (boolean(id->getSymbol())) @@ -7084,7 +7292,7 @@ namespace else { const LSLIType type = varType(id->getSymbol(), id->getIType()); - line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? defaultOf(type) : coerced(init, type).text)); + line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? defaultOf(type) : oneValue(coerced(init, type)).text)); } commentsAfter(global, from); lined(global, from); @@ -7189,7 +7397,7 @@ namespace { lead.push_back("local " + nameOf(static_cast(first)) + typed(LST_INTEGER) + " = #detected"); } - return mOptions.types ? "detected: { DetectedEvent }" : "detected"; + return mOptions.types ? "detected: " + braced("DetectedEvent") : "detected"; } int at = 0; for (LSLASTNode* p = first; p; p = p->getNext(), ++at) @@ -7249,14 +7457,15 @@ namespace std::vector lead; const std::string params = handlerParams(handler, lead); // The timer's handler, which LLTimers calls, where the script's - // timer is on LLTimers. + // timer is on LLTimers: the local timersPreamble declared, which + // a function statement of its name assigns. const bool timer = event == "timer" && mTimers; if (event == "timer" && !mTimers) { noteOnce(handler, "SluaTimer", "the timer event, set going by llcompat.SetTimerEvent; LLTimers:every is SLua's own."); } const bool field = mOptions.handlers == ALLSLToSLua::Options::Handlers::Field; - line(timer ? "timerHandler = function()" + line(timer ? "function timerHandler()" : field ? "function LLEvents." + event + "(" + params + ")" : "LLEvents:on(" + luaString(event) + ", function(" + params + ")"); trail(handler->getStatements()); @@ -7291,7 +7500,7 @@ namespace note(nullptr, "SluaStates", "LSL let go of a state's listens, sensor repeats and targets as it left the state; setState does not. Remove them " "yourself where the script relied on it."); - line("local states: { [string]: { [string]: (...any) -> () } } = {}"); + line("local states: " + braced("[string]: " + braced("[string]: (...any) -> ()")) + " = {}"); line("local currentState: string? = nil"); line(""); line("local function setState(name: string)"); @@ -7304,9 +7513,17 @@ namespace // a field assigned nil. const std::string off = "LLEvents:off(event :: any, handler)"; const std::string on = field ? "(LLEvents :: any)[event] = handler" : "LLEvents:on(event :: any, handler)"; + // The state entered looked up before the one left is, so that a + // name with none -- which LSL's compiler refused, but an edit + // afterwards may write -- stops the script saying so, at the call + // that named it, and leaves it in the state it was in. mText += " if name == currentState then\n" " return\n" " end\n" + " local entering = states[name]\n" + " if not entering then\n" + " error(\"unknown state: \" .. name, 2)\n" + " end\n" " local leaving = currentState and states[currentState]\n" " if leaving then\n" " if leaving.state_exit then\n" @@ -7319,7 +7536,6 @@ namespace " end\n" " end\n" " currentState = name\n" - " local entering = states[name]\n" " for event, handler in entering do\n" " if " + own + " then\n" " " + on + "\n" @@ -7508,7 +7724,7 @@ end out += R"LUA(-- LSL's (integer) of a string: the whole number it starts with, in -- decimal or 0x hexadecimal, after any spaces; 0 where it starts with none. local function lslInteger(s: string): number - return llcompat.List2Integer({ s }, 0) + return llcompat.List2Integer()LUA" + braced("s") + R"LUA(, 0) end )LUA"; @@ -7518,7 +7734,7 @@ end out += R"LUA(-- LSL's (float) of a string: the number it starts with; 0 where it -- starts with none. local function lslFloat(s: string): number - return llcompat.List2Float({ s }, 0) + return llcompat.List2Float()LUA" + braced("s") + R"LUA(, 0) end )LUA"; @@ -7600,6 +7816,11 @@ end { writeComment(c); } + // Ending with one line break, as format on save leaves it. + while (mText.size() >= 2 && mText.compare(mText.size() - 2, 2, "\n\n") == 0) + { + mText.pop_back(); + } // Each of its lines a comment, whatever language the studio says // it in. const std::string head = mOptions.comments diff --git a/indra/alscript/lsl/allsltoslua.h b/indra/alscript/lsl/allsltoslua.h index d902dfe113..af32f50d77 100644 --- a/indra/alscript/lsl/allsltoslua.h +++ b/indra/alscript/lsl/allsltoslua.h @@ -87,6 +87,10 @@ class ALLSLToSLua // Each place noted as a "-- LSL:" comment over its line, as well as // in the notes. bool comments = true; + // A space inside a table's braces, and a table type's, as StyLua + // and the studio's formatter write them by default -- { "a", 1 }, + // { any } -- or none: {"a", 1}. Nothing inside, {} either way. + bool braceSpaces = true; // The script's own comments, each over what it stood over, after it // where it was after it on its line, or at the end of what held it: // // as --, and /* */ as --[[ ]]; a rule of stars or slashes as one of diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 1a57c6da97..066e7955b7 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -25,6 +25,7 @@ #include "linden_common.h" #include "../lsl/allsltoslua.h" +#include "../core/alscriptformatter.h" #include "../lint/alscriptfixes.h" #include "../lsl/allslservice.h" #include "../lsl/allsltraits.h" @@ -264,12 +265,12 @@ namespace tut ensure("a float as LSL writes it: " + r.text, has(r, "string.format(\"%.6f\", f)")); ensure("a string as an integer, as a list's item converts it: " + r.text, has(r, "lslInteger(\"12abc\")") && has(r, "return llcompat.List2Integer({ s }, 0)")); - ensure("lists compared by length: " + r.text, has(r, "#l == #{1}") && noted(r, "SluaListCompare")); + ensure("lists compared by length: " + r.text, has(r, "#l == #{ 1 }") && noted(r, "SluaListCompare")); // bit32 answers 0 to 4294967295: made LSL's signed integer, but where // bit32 takes it again. ensure("bits: " + r.text, has(r, "bit32.s32(bit32.bor(bit32.band(a, b), 4))") && !has(r, "local function int32")); ensure("a vector's part set: " + r.text, has(r, "v = vector(4, v.y, v.z)") && has(r, "v = vector(v.x, v.y, v.z + 1)")); - ensure("a vector as LSL writes it: " + r.text, has(r, "ll.DumpList2String({v}, \"\")")); + ensure("a vector as LSL writes it: " + r.text, has(r, "ll.DumpList2String({ v }, \"\")")); checksClean(r); } @@ -324,7 +325,14 @@ namespace tut ensure("calling the state's handler: " + r.text, has(r, "states[currentState].timer") && has(r, "event ~= \"timer\"")); ensure("state_exit a handler of its state: " + r.text, has(r, "state_exit = function()")); ensure("what is let go of noted: " + r.text, noted(r, "SluaStates")); + // Tapple's: a name with no state, which only an edit could write, + // said so at the call, before the state it is in is left. + const size_t looked = r.text.find(" local entering = states[name]\n if not entering then\n error(\"unknown state: \" .. name, 2)\n end\n"); + ensure("a state not there said: " + r.text, looked != std::string::npos && looked < r.text.find("local leaving")); checksClean(r); + ALLSLToSLua::Options typed; + typed.types = true; + checksClean(ALLSLToSLua::convert("default { touch_start(integer n) { state two; } }\nstate two { state_entry() { state default; } }\n", typed)); } template<> template<> @@ -400,7 +408,7 @@ namespace tut ensure("one thing found in a list: " + r.text, has(r, "if table.find(l, \"a\") ~= nil then")); ensure("~ of a find: " + r.text, has(r, "if string.find(s, \"c\", 1, true) ~= nil then")); ensure("< 0 of one: " + r.text, has(r, "if string.find(s, \"z\", 1, true) == nil then")); - ensure("a find's index as a number, through llcompat: " + r.text, has(r, "llcompat.ListFindList(l, {\"a\", \"b\"})")); + ensure("a find's index as a number, through llcompat: " + r.text, has(r, "llcompat.ListFindList(l, { \"a\", \"b\" })")); ensure("^, math and vector: " + r.text, has(r, "2.0 ^ 3.0") && has(r, "math.abs(-1.0)") && has(r, "vector.magnitude(vector(1, 2, 3) - ZERO_VECTOR)")); ensure("half up, as LSL rounds, not math.round: " + r.text, has(r, "math.floor(2.5 + 0.5)")); ensure("os.time and print: " + r.text, has(r, "os.time()") && has(r, "print(")); @@ -528,7 +536,7 @@ namespace tut ensure("a global toggled: " + r.text, has(r, "local gOn = false") && has(r, "gOn = not gOn") && has(r, "if gOn then")); ensure("compared with FALSE: " + r.text, has(r, "local gSeen = false") && has(r, "if not gSeen and isOwner(") && has(r, "gSeen = true")); ensure("a function that answers one: " + r.text, has(r, "return k == ll.GetOwner()")); - ensure("a length, as a truth: " + r.text, has(r, "return #l ~= 0") && has(r, "local busy = anyOf({1})") && has(r, "while not busy do")); + ensure("a length, as a truth: " + r.text, has(r, "return #l ~= 0") && has(r, "local busy = anyOf({ 1 })") && has(r, "while not busy do")); ensure("stepped: " + r.text, has(r, "local count = 0") && has(r, "count += 1")); ensure("printed: " + r.text, has(r, "local shown = 1")); ensure("more than TRUE, compared with it: " + r.text, has(r, "local loose = 5") && has(r, "if loose == 1 then")); @@ -622,19 +630,19 @@ namespace tut " show(llListInsertList(gGrown, [addOne()], 0));\n" " show(kept() + b + a + gAll + gStored);\n" "} }\n"); - ensure("a local returned, grown in place: " + r.text, has(r, "local out = ll.ParseString2List(s, {\",\"}, {})") && has(r, "table.insert(out, \"end\")")); + ensure("a local returned, grown in place: " + r.text, has(r, "local out = ll.ParseString2List(s, { \",\" }, {})") && has(r, "table.insert(out, \"end\")")); ensure("a call's list, put on the end: " + r.text, has(r, "gAll = parts(\"a,b\")") && has(r, "table.extend(gAll, parts(\"c\"))")); ensure("prepended: " + r.text, has(r, "table.insert(gAll, 1, \"first\")")); ensure("a list by its name: " + r.text, has(r, "table.extend(gAll, more)")); ensure("several that do not run apart, each had before any is added: " + r.text, has(r, "table.append(gAll, ll.GetTime(), ll.Frand(1.0))")); ensure("passed to a function that only reads it: " + r.text, has(r, "table.insert(gSeen, n)")); ensure("passed to one that keeps it: " + r.text, - has(r, "gPassed = table.extend(table.clone(gPassed), {n})") && + has(r, "gPassed = table.extend(table.clone(gPassed), { n })") && has(r, "-- LSL: LSL's lists were values, and gPassed is passed to a function of the script's that keeps it")); - ensure("returned: " + r.text, has(r, "gKept = table.extend(table.clone(gKept), {2})") && has(r, "gKept is returned by a function")); - ensure("given to another: " + r.text, has(r, "a = table.extend(table.clone(a), {2})") && has(r, "a is given to another variable")); + ensure("returned: " + r.text, has(r, "gKept = table.extend(table.clone(gKept), { 2 })") && has(r, "gKept is returned by a function")); + ensure("given to another: " + r.text, has(r, "a = table.extend(table.clone(a), { 2 })") && has(r, "a is given to another variable")); ensure("read where a call in the statement grows it: " + r.text, - has(r, "gGrown = table.extend(table.clone(gGrown), {1})") && + has(r, "gGrown = table.extend(table.clone(gGrown), { 1 })") && has(r, "gGrown is read where a call of the script's in the same statement changes it")); ensure("said once for each: " + r.text, count(r, "and gPassed is") == 1); checksClean(r); @@ -705,7 +713,7 @@ namespace tut ensure("not where the left of the operator is more than x: " + r.text, has(r, "b = b - c - 1") && has(r, "c = c * 2 + b")); ensure("a string's pieces, joined: " + r.text, has(r, "s ..= `y{b}`")); ensure("a float's and a vector's as LSL writes them, in one interpolated string: " + r.text, - has(r, "print(`{a}{string.format(\"%.6f\", f)}{ll.DumpList2String({v}, \"\")}{s}{b + c}`)")); + has(r, "print(`{a}{string.format(\"%.6f\", f)}{ll.DumpList2String({ v }, \"\")}{s}{b + c}`)")); checksClean(r); } @@ -742,7 +750,7 @@ namespace tut "} }\n"); ensure("left out: " + r.text, !has(r, "PointAt(") && has(r, "-- llPointAt, left out") && noted(r, "SluaAbsent", "llPointAt")); ensure("its empty value where read: " + r.text, has(r, "local e = {}") && noted(r, "SluaAbsent", "llGetExperienceList")); - ensure("llcompat, said: " + r.text, has(r, "llcompat.GetPrimitiveParams({PRIM_FULLBRIGHT, 0})") && noted(r, "SluaBoolList", "GetPrimitiveParams")); + ensure("llcompat, said: " + r.text, has(r, "llcompat.GetPrimitiveParams({ PRIM_FULLBRIGHT, 0 })") && noted(r, "SluaBoolList", "GetPrimitiveParams")); ensure("not a truth: " + r.text, !has(r, "if llcompat.GetPrimitiveParams")); checksClean(r); } @@ -763,7 +771,7 @@ namespace tut "} }\n"); ensure("a key against a key: " + r.text, has(r, "if k ~= NULL_KEY then")); ensure("a string where LSL's string was: " + r.text, has(r, "tostring(NULL_KEY) ~= \"\"") && has(r, "ll.GetSubString(tostring(NULL_KEY), 1, 8)")); - ensure("in a list, as LSL had it: " + r.text, has(r, "{tostring(NULL_KEY), tostring(TEXTURE_BLANK)}")); + ensure("in a list, as LSL had it: " + r.text, has(r, "{ tostring(NULL_KEY), tostring(TEXTURE_BLANK) }")); ensure("a string's truth: " + r.text, has(r, "if tostring(NULL_KEY) ~= \"\" then print(")); ensure("given to a string and a key: " + r.text, has(r, "local gName = tostring(NULL_KEY)") && has(r, "local gKey = NULL_KEY")); ensure("returned as a string function's: " + r.text, has(r, "return tostring(NULL_KEY)")); @@ -1325,7 +1333,7 @@ namespace tut ensure("over the function it stood over: " + r.text, at("--[[ old code:") > at("local NAMES") && at("--[[ old code:") < at("-- Turns the door.")); ensure("a brace's, after the line that opens it: " + r.text, has(r, "-- Turns the door.\nlocal function swing(by) -- by degrees\n")); ensure("over a statement: " + r.text, has(r, " -- the rotation it turns by\n local r = ")); - ensure("a trailing one, after its statement: " + r.text, has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, {PRIM_ROT_LOCAL, r * ll.GetLocalRot()}) -- turn\n")); + ensure("a trailing one, after its statement: " + r.text, has(r, " ll.SetLinkPrimitiveParamsFast(LINK_THIS, { PRIM_ROT_LOCAL, r * ll.GetLocalRot() }) -- turn\n")); ensure("between branches, over the elseif: " + r.text, has(r, " -- when it shuts\n elseif by < 0 then\n")); ensure("an empty branch's, in it: " + r.text, has(r, " if false then\n --[[ left off ]]\n elseif by == 0 then\n")); ensure("at a block's end: " + r.text, has(r, " -- done\nend\n")); @@ -1627,8 +1635,8 @@ namespace tut " llOwnerSay((string)ZERO_ROTATION);\n" "} }\n"); ensure("converted", r.converted); - ensure("LSL's text kept: " + r.text, has(r, "set(7, `{SEAT_NUM};{ll.DumpList2String({offset}, \"\")}`)")); - ensure("a rotation's too: " + r.text, has(r, "ll.DumpList2String({ZERO_ROTATION}, \"\")")); + ensure("LSL's text kept: " + r.text, has(r, "set(7, `{SEAT_NUM};{ll.DumpList2String({ offset }, \"\")}`)")); + ensure("a rotation's too: " + r.text, has(r, "ll.DumpList2String({ ZERO_ROTATION }, \"\")")); ensure("said why, once: " + r.text, noted(r, "SluaVectorText") && r.text.find("-- LSL: ll.DumpList2String writes") != std::string::npos && r.text.find("-- LSL: ll.DumpList2String writes") == r.text.rfind("-- LSL: ll.DumpList2String writes")); checksClean(r); @@ -2081,8 +2089,17 @@ namespace tut ensure("once: " + r.text, has(r, "timerHandle = LLTimers:once(seconds, function()") && !has(r, "LLTimers:every(seconds, function")); ensure("said so: " + r.text, has(r, "-- time it is set, as its handler turns it off before anything else.")); ensure("the handler's own off kept, and set again: " + r.text, - has(r, "timerHandler = function()\n setTimer(0.0)\n") && has(r, "setTimer(2)") && has(r, "setTimer(1)")); + has(r, "function timerHandler()\n setTimer(0.0)\n") && has(r, "setTimer(2)") && has(r, "setTimer(1)")); checksClean(r); + // Tapple's: the local the preamble declared, assigned by a function + // statement of its name, which types as it is declared. + ALLSLToSLua::Options typed; + typed.types = true; + const ALLSLToSLua::Result types = ALLSLToSLua::convert("default { state_entry() { llSetTimerEvent(5); } timer() { llOwnerSay(\"t\"); } }\n", typed); + ensure("a function statement, typed: " + types.text, + has(types, "local timerHandler: (() -> ())? = nil\n") && has(types, "\nfunction timerHandler()\n print(\"t\")\nend\n") && + !has(types, "timerHandler = function")); + checksClean(types); const ALLSLToSLua::Result states = convert("default {\n" " state_entry() { llSetTimerEvent(1); }\n" @@ -2417,15 +2434,15 @@ namespace tut ensure("a list by its name: " + r.text, has(r, "table.extend(all, more)")); ensure("itself: " + r.text, has(r, "table.extend(all, all)")); ensure("a call's, run once: " + r.text, has(r, "table.extend(all, llcompat.List2List(more, 0, n))") && !has(r, "for _, item in")); - ensure("many values: " + r.text, has(r, "table.extend(all, {0, 1, 2,") && has(r, "32, \"x\"})")); + ensure("many values: " + r.text, has(r, "table.extend(all, { 0, 1, 2,") && has(r, "32, \"x\" })")); ensure("one copy, of the variable a call answers: " + r.text, has(r, "show(table.extend(table.extend(table.clone(kept()), more), all))")); - ensure("a variable copied: " + r.text, has(r, "show(table.extend(table.clone(more), {n}))")); - ensure("a value: " + r.text, has(r, "show(table.extend({n} :: { any }, more))")); - ensure("written out: " + r.text, has(r, "show(table.extend({PRIM_NAME, \"x\"} :: { any }, more))") && - has(r, "table.extend({PRIM_COLOR, ALL_SIDES, vector(1, 1, 1), 1.0} :: { any }, more)")); + ensure("a variable copied: " + r.text, has(r, "show(table.extend(table.clone(more), { n }))")); + ensure("a value: " + r.text, has(r, "show(table.extend({ n } :: { any }, more))")); + ensure("written out: " + r.text, has(r, "show(table.extend({ PRIM_NAME, \"x\" } :: { any }, more))") && + has(r, "table.extend({ PRIM_COLOR, ALL_SIDES, vector(1, 1, 1), 1.0 } :: { any }, more)")); ensure("a library call's: " + r.text, - has(r, "show(table.extend(table.extend(ll.ParseString2List(\"c\", {}, {}) :: { any }, {1}), {vector(1, 2, 3)}))")); + has(r, "show(table.extend(table.extend(ll.ParseString2List(\"c\", {}, {}) :: { any }, { 1 }), { vector(1, 2, 3) }))")); ensure("no helper, nor a move: " + r.text, !has(r, "joinLists") && !has(r, "table.move(")); checksClean(r); } @@ -2471,22 +2488,22 @@ namespace tut "} }\n"); ensure("replaced in place, in Luau's numeric for: " + r.text, has(r, "for i = 0, #l - 1 do\n l[i + 1] = l[i + 1] * 2\n")); ensure("a list held elsewhere, replaced as LSL's: " + r.text, - has(r, "for j = 0, #held - 1 do\n") && has(r, " held = llcompat.ListReplaceList(held, {j}, j, j)\n")); + has(r, "for j = 0, #held - 1 do\n") && has(r, " held = llcompat.ListReplaceList(held, { j }, j, j)\n")); ensure("a global a call of the script's could change: " + r.text, has(r, "while k < #gL do") && - has(r, "gL = llcompat.ListReplaceList(gL, {bump()}, k, k)")); - ensure("the last of a list that could be empty: " + r.text, has(r, "l = llcompat.ListReplaceList(l, {7}, -1, -1)")); + has(r, "gL = llcompat.ListReplaceList(gL, { bump() }, k, k)")); + ensure("the last of a list that could be empty: " + r.text, has(r, "l = llcompat.ListReplaceList(l, { 7 }, -1, -1)")); ensure("on the end: " + r.text, has(r, "table.insert(l, 9)")); ensure("a part, and all of it: " + r.text, has(r, "local part = table.move(l, 2, 3, 1, {})") && has(r, "local all = table.clone(l)")); ensure("to the end, and the part left out, as LSL's: " + r.text, has(r, "local rest = llcompat.List2List(l, 2, -1)") && has(r, "local wrapped = llcompat.List2List(l, 3, 1)")); ensure("whole numbers joined: " + r.text, has(r, "print(table.concat(l, \",\"))") && has(r, "print(table.concat(l))")); - ensure("and text: " + r.text, has(r, "print(table.concat(table.extend(table.clone(gNames), {n}), \", \"))")); - ensure("a float as LSL writes one: " + r.text, has(r, "print(ll.DumpList2String(table.extend(table.clone(l), {1.5}), \",\"))")); + ensure("and text: " + r.text, has(r, "print(table.concat(table.extend(table.clone(gNames), { n }), \", \"))")); + ensure("a float as LSL writes one: " + r.text, has(r, "print(ll.DumpList2String(table.extend(table.clone(l), { 1.5 }), \",\"))")); ensure("found, its place: " + r.text, has(r, "local at = (table.find(gNames, \"b\") or 0) - 1")); ensure("NULL_KEY as LSL's string, as its place and found: " + r.text, has(r, "local none = (table.find(gNames, tostring(NULL_KEY)) or 0) - 1") && has(r, "if table.find(gNames, tostring(NULL_KEY)) ~= nil then")); - ensure("in a library call's list: " + r.text, has(r, "local parsed = (table.find(ll.ParseString2List(\"a,b\", {\",\"}, {}) :: { any }, \"b\") or 0) - 1")); + ensure("in a library call's list: " + r.text, has(r, "local parsed = (table.find(ll.ParseString2List(\"a,b\", { \",\" }, {}) :: { any }, \"b\") or 0) - 1")); ensure("no llcompat but those: " + r.text, count(r, "llcompat.") == 5); checksClean(r); } @@ -2586,10 +2603,10 @@ namespace tut // The join leaves a first piece of none out, escapes a \, and sends // no rule at all for no text. ensure("a whitelist the join would change, a list: " + r.text, - has(r, "ll.SetPrimMediaParams(1, {PRIM_MEDIA_WHITELIST, \",a.com\"})") && has(r, "ll.SetPrimMediaParams(2, {PRIM_MEDIA_WHITELIST, \"a") && - has(r, "ll.SetPrimMediaParams(3, {PRIM_MEDIA_WHITELIST, \"\"})")); + has(r, "ll.SetPrimMediaParams(1, { PRIM_MEDIA_WHITELIST, \",a.com\" })") && has(r, "ll.SetPrimMediaParams(2, { PRIM_MEDIA_WHITELIST, \"a") && + has(r, "ll.SetPrimMediaParams(3, { PRIM_MEDIA_WHITELIST, \"\" })")); ensure("a truth not written out as one, a list: " + r.text, - has(r, "ll.SetPrimMediaParams(4, {PRIM_MEDIA_AUTO_PLAY, on})") && has(r, "ll.SetPrimMediaParams(5, {PRIM_MEDIA_AUTO_ZOOM, 2})")); + has(r, "ll.SetPrimMediaParams(4, { PRIM_MEDIA_AUTO_PLAY, on })") && has(r, "ll.SetPrimMediaParams(5, { PRIM_MEDIA_AUTO_ZOOM, 2 })")); checksClean(r); } @@ -2634,9 +2651,9 @@ namespace tut " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" " llHTTPRequest(\"https://z\", [HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_CUSTOM_HEADER, \"B\", counted()], \"\");\n" "} }\n"); - ensure("a name twice, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", {HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\"}, \"\")")); - ensure("a name not written out, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", {HTTP_CUSTOM_HEADER, token, \"1\"}, \"\")")); - ensure("moved past another, a list: " + lists.text, has(lists, "{HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted()}")); + ensure("a name twice, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, \"A\", \"1\", HTTP_CUSTOM_HEADER, \"A\", \"2\" }, \"\")")); + ensure("a name not written out, a list: " + lists.text, has(lists, "ll.HTTPRequest(\"https://z\", { HTTP_CUSTOM_HEADER, token, \"1\" }, \"\")")); + ensure("moved past another, a list: " + lists.text, has(lists, "{ HTTP_CUSTOM_HEADER, \"A\", counted(), HTTP_METHOD, \"GET\", HTTP_CUSTOM_HEADER, \"B\", counted() }")); ensure("one after another, run in their order: " + lists.text, has(lists, "{ custom_header = { [\"A\"] = counted(), [\"B\"] = counted() } }")); checksClean(lists); } @@ -2661,18 +2678,18 @@ namespace tut "} }\n"; const ALLSLToSLua::Result r = convert(script); ensure("not written out: " + r.text, has(r, "ll.ParticleSystem(gRules)")); - ensure("a key twice: " + r.text, has(r, "ll.ParticleSystem({PSYS_PART_MAX_AGE, 2.0, PSYS_PART_MAX_AGE, 3.0})")); - ensure("another table's rule: " + r.text, has(r, "ll.ParticleSystem({PRIM_MEDIA_AUTO_PLAY, 1})")); - ensure("a number for a rule: " + r.text, has(r, "ll.ParticleSystem({7, 2.0})")); - ensure("a value of another shape: " + r.text, has(r, "ll.ParticleSystem({PSYS_PART_START_ALPHA, vector(1, 0, 0)})")); - ensure("no value: " + r.text, has(r, "ll.ParticleSystem({PSYS_PART_MAX_AGE})")); + ensure("a key twice: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE, 2.0, PSYS_PART_MAX_AGE, 3.0 })")); + ensure("another table's rule: " + r.text, has(r, "ll.ParticleSystem({ PRIM_MEDIA_AUTO_PLAY, 1 })")); + ensure("a number for a rule: " + r.text, has(r, "ll.ParticleSystem({ 7, 2.0 })")); + ensure("a value of another shape: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_START_ALPHA, vector(1, 0, 0) })")); + ensure("no value: " + r.text, has(r, "ll.ParticleSystem({ PSYS_PART_MAX_AGE })")); ensure("flags as they are: " + r.text, has(r, "ll.LinkParticleSystem(1, { flags = PSYS_PART_WIND_MASK + PSYS_PART_BOUNCE_MASK })") && has(r, "ll.LinkParticleSystem(2, { flags = bit32.bor(PSYS_PART_WIND_MASK, PSYS_PART_WIND_MASK) })")); ensure("masks bracketed, each true: " + r.text, has(r, "ll.LinkParticleSystem(3, { wind = true, bounce = true, ribbon = true })")); ALLSLToSLua::Options plain; plain.idioms = false; const ALLSLToSLua::Result lists = ALLSLToSLua::convert(script, plain); - ensure("no idioms, no tables: " + lists.text, has(lists, "ll.LinkParticleSystem(3, {PSYS_PART_FLAGS, ") && !has(lists, "wind = true")); + ensure("no idioms, no tables: " + lists.text, has(lists, "ll.LinkParticleSystem(3, { PSYS_PART_FLAGS, ") && !has(lists, "wind = true")); } template<> template<> @@ -2816,12 +2833,12 @@ namespace tut ensure("a global's, its comments where they were: " + r.text, has(r, "local gColours = {\n -- warm\n \"red\", \"orange\",\n --[[ cool ]] \"blue\" -- last\n}\n")); ensure("closed on the line it closed on, a comment after it trailing that: " + r.text, - has(r, "local gTwo = {1, 2,\n 3} -- three\n")); + has(r, "local gTwo = { 1, 2,\n 3 } -- three\n")); ensure("Tapple's, as the LSL had it: " + r.text, has(r, " local activeRequest = ll.List2Json(JSON_OBJECT, {\n" " \"fn\", \"SELECT\",\n" " \"radius\", 10,\n" - " \"ima\", ll.List2Json(JSON_ARRAY, {\"Ungulate\"}),\n" + " \"ima\", ll.List2Json(JSON_ARRAY, { \"Ungulate\" }),\n" " \"ownerOnly\", 1, -- only the owner\n" " \"toucher\", ll.GetOwner(),\n" " \"requestor\", ll.List2Json(JSON_OBJECT, {\n" @@ -2832,8 +2849,8 @@ namespace tut " \"responders\", \"[]\"\n" " }) -- sent\n")); ensure("each comment once: " + r.text, count(r, "only the owner") == 1 && count(r, "-- sent") == 1 && count(r, "-- first") == 1); - ensure("in a line made inside an expression, one line: " + r.text, has(r, "(function() m = {3, 4} return m end)()")); - ensure("in a condition: " + r.text, has(r, " if table.find({\"a\", -- first\n \"b\"}")); + ensure("in a line made inside an expression, one line: " + r.text, has(r, "(function() m = { 3, 4 } return m end)()")); + ensure("in a condition: " + r.text, has(r, " if table.find({ \"a\", -- first\n \"b\" }")); ensure("one written another way, its comment over it: " + r.text, has(r, " -- none\n ll.ParticleSystem({\n")); checksClean(r); } @@ -2881,10 +2898,10 @@ namespace tut " if (mine(llJsonGetValue(gJson, [\"owner\"]))) llOwnerSay(\"mine\");\n" "} }\n"; const ALLSLToSLua::Result r = convert(lsl); - ensure("Tapple's, as it is: " + r.text, has(r, "ll.Key2Name(ll.JsonGetValue(gJson, {\"selected\"}) :: any)")); + ensure("Tapple's, as it is: " + r.text, has(r, "ll.Key2Name(ll.JsonGetValue(gJson, { \"selected\" }) :: any)")); ensure("bracketed where it must be: " + r.text, has(r, "ll.Key2Name((\"a2e76fcd-9360-4f6d-a924-\" .. \"938f923df11d\") :: any)")); ensure("a UUID written out, a uuid: " + r.text, has(r, "ll.Key2Name(uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\"))")); - ensure("the script's own function's key, uuid(): " + r.text, has(r, "mine(uuid(ll.JsonGetValue(gJson, {\"owner\"})))") && + ensure("the script's own function's key, uuid(): " + r.text, has(r, "mine(uuid(ll.JsonGetValue(gJson, { \"owner\" })))") && noted(r, "SluaUuidText")); ensure("said once: " + r.text, count(r, "-- LSL: SLua's ll takes a key as text") == 1); checksClean(r); @@ -2907,4 +2924,260 @@ namespace tut ensure("llcompat's the same: " + close.text, close.converted && close.text.find(":: any)") != std::string::npos); checksClean(close); } + + template<> template<> + void allsltoslua_object::test<86>() + { + set_test_name("a find's place compared with a whole number written out is compared in SLua's count, from 1 or nil, on either side: " + "x == 1 where LSL said == 0; a place below -1 left as written, and one read as a number kept LSL's"); + const std::string lsl = "string P = \"cfg\";\n" + "list L;\n" + "check(string item) {\n" + " if (P != \"\" && llSubStringIndex(item, P) == 0) llOwnerSay(\"a\");\n" + " if (0 == llSubStringIndex(item, P)) llOwnerSay(\"b\");\n" + " if (llSubStringIndex(item, P) != 2) llOwnerSay(\"c\");\n" + " if (llSubStringIndex(item, P) > 0) llOwnerSay(\"d\");\n" + " if (3 < llSubStringIndex(item, P)) llOwnerSay(\"e\");\n" + " if (llSubStringIndex(item, P) == -2) llOwnerSay(\"f\");\n" + " if (!(llSubStringIndex(item, P) == 0)) llOwnerSay(\"g\");\n" + " if (llListFindList(L, [item]) == 0) llOwnerSay(\"h\");\n" + " integer at = llSubStringIndex(item, P);\n" + " llOwnerSay((string)at);\n" + "}\n" + "default { touch_start(integer n) { check(\"x\"); } }\n"; + const ALLSLToSLua::Result r = convert(lsl); + ensure("Tapple's: " + r.text, has(r, "if P ~= \"\" and ll.SubStringIndex(item, P) == 1 then")); + ensure("the number on the left: " + r.text, has(r, "if ll.SubStringIndex(item, P) == 1 then print(\"b\")")); + ensure("~=: " + r.text, has(r, "if ll.SubStringIndex(item, P) ~= 3 then")); + ensure("an order, nil read as LSL's -1: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) > 1 then")); + ensure("an order the other way round: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) > 4 then")); + ensure("below -1, as written: " + r.text, has(r, "if (ll.SubStringIndex(item, P) or 0) - 1 == -2 then")); + ensure("under a not: " + r.text, has(r, "if ll.SubStringIndex(item, P) ~= 1 then print(\"g\")")); + ensure("table.find's: " + r.text, has(r, "if table.find(L, item) == 1 then")); + ensure("a number, LSL's: " + r.text, has(r, "local at = (ll.SubStringIndex(item, P) or 0) - 1")); + checksClean(r); + + // table.find's alone, without SLua's ll: whether it found, against nil. + ALLSLToSLua::Options idioms = ALLSLToSLua::Options::closeToLSL(); + idioms.idioms = true; + const ALLSLToSLua::Result found = + ALLSLToSLua::convert("list L;\ndefault { touch_start(integer n) {\n" + " if (llListFindList(L, [n]) == -1) llOwnerSay(\"none\");\n" + " if (llListFindList(L, [n]) >= 0) llOwnerSay(\"some\");\n" + " if (llListFindList(L, [n]) == 2) llOwnerSay(\"third\");\n" + "} }\n", + idioms); + ensure("found, against nil: " + found.text, found.converted && has(found, "if table.find(L, n) == nil then") && + has(found, "if table.find(L, n) ~= nil then") && has(found, "if table.find(L, n) == 3 then")); + checksClean(found); + + const ALLSLToSLua::Result close = ALLSLToSLua::convert(lsl, ALLSLToSLua::Options::closeToLSL()); + ensure("llcompat's, LSL's count: " + close.text, close.converted && has(close, "llcompat.SubStringIndex(item, P) == 0")); + checksClean(close); + } + + template<> template<> + void allsltoslua_object::test<87>() + { + set_test_name("a blank line between two statements or globals is kept, one for a run of them; none at a block's start or end, " + "nor a second where a comment over the statement keeps its own"); + const ALLSLToSLua::Result r = convert("integer a = 1;\n" + "integer b = 2;\n" + "\n" + "\n" + "integer c = 3;\n" + "integer preCheck(key avatar) {\n" + "\n" + " // only allow my owner to request contents\n" + " if (avatar != llGetOwner()) return FALSE;\n" + "\n" + " return TRUE;\n" + "}\n" + "default {\n" + " touch_start(integer n) {\n" + " if (preCheck(llDetectedKey(0))) {\n" + " llOwnerSay((string)a);\n" + "\n" + " llOwnerSay((string)b);\n" + "\n" + " }\n" + "\n" + " // said apart\n" + " llOwnerSay((string)c);\n" + " }\n" + "}\n"); + ensure("the globals: " + r.text, has(r, "local a = 1\nlocal b = 2\n\nlocal c = 3\n")); + ensure("Tapple's: " + r.text, has(r, " -- only allow my owner to request contents\n if avatar ~= ll.GetOwner() then return false end\n\n return true\nend")); + ensure("none at a block's start: " + r.text, has(r, "(avatar)\n -- only allow")); + ensure("in a block, none at its end: " + r.text, has(r, " print(tostring(a))\n\n print(tostring(b))\n end\n")); + ensure("one over a comment kept apart: " + r.text, has(r, " end\n\n -- said apart\n print(tostring(c))\n")); + ensure("never two: " + r.text, !has(r, "\n\n\n")); + checksClean(r); + } + + template<> template<> + void allsltoslua_object::test<88>() + { + set_test_name("tables and table types spaced inside their braces as asked, as the formatter spaces them: StyLua's by default, " + "Tapple's line; none where asked; an interpolated string's braces and {} never"); + const std::string lsl = "list gTwo = [1, 2,\n 3];\n" + "default {\n" + " state_entry() { state two; }\n" + "}\n" + "state two {\n" + " listen(integer c, string name, key id, string msg) {\n" + " list params = llParseStringKeepNulls(msg, [\"\\n\"], []);\n" + " llSetPrimMediaParams(0, [PRIM_MEDIA_AUTO_PLAY, TRUE, PRIM_MEDIA_WHITELIST, \"a.com,b.com\"]);\n" + " llOwnerSay(\"n=\" + (string)llGetListLength(params) + \" \" + (string)<1, 2, 3>);\n" + " }\n" + "}\n"; + ALLSLToSLua::Options typed; + typed.types = true; + const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, typed); + ensure("Tapple's: " + r.text, r.converted && has(r, "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})")); + ensure("a rule table: " + r.text, has(r, "ll.SetPrimMediaParams(0, { auto_play = true, whitelist = { \"a.com\", \"b.com\" } })")); + ensure("the states' type: " + r.text, has(r, "local states: { [string]: { [string]: (...any) -> () } } = {}")); + ensure("over lines: " + r.text, has(r, "local gTwo: { any } = { 1, 2,\n 3 }\n")); + ensure("an interpolated string's: " + r.text, has(r, "`n={#params} {ll.DumpList2String({ vector(1, 2, 3) }, \"\")}`")); + checksClean(r); + + typed.braceSpaces = false; + const ALLSLToSLua::Result none = ALLSLToSLua::convert(lsl, typed); + ensure("none: " + none.text, none.converted && has(none, "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})") && + has(none, "ll.SetPrimMediaParams(0, {auto_play = true, whitelist = {\"a.com\", \"b.com\"}})") && + has(none, "local states: {[string]: {[string]: (...any) -> ()}} = {}") && + has(none, "local gTwo: {any} = {1, 2,\n 3}\n") && !has(none, "{ ")); + checksClean(none); + + // As the formatter would leave them, either way. + for (bool spaces : { true, false }) + { + ALScriptFormatter::Options formatter; + formatter.lua = true; + formatter.braceSpaces = spaces; + const ALLSLToSLua::Result same = spaces ? r : none; + ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(same.text, formatter), same.text); + } + } + + template<> template<> + void allsltoslua_object::test<89>() + { + set_test_name("converted SLua is as format on save leaves it: a comment after an unbraced body trails it, after the line that " + "opens it where the body is empty; what the LSL lined up, lined up; one line break at the end; either brace spacing"); + const std::string lsl = "integer CHANNEL = 1; // channel\n" + "integer OWNER_ONLY = 0; // owner\n" + "float TIMEOUT = 2.5; // seconds\n" + "integer asInt(string s) { return (integer)s; }\n" + "default {\n" + " touch_start(integer n) {\n" + " integer b;\n" + " if (n == 1)\n" + " llOwnerSay(\"a\"); // after a\n" + " else if (n == 2)\n" + " llOwnerSay(\"b\"); // after b\n" + " else\n" + " llOwnerSay(\"c\"); // after c\n" + " if ((b = 1) == 2); // an empty body\n" + " else llOwnerSay(\"f\"); // after f\n" + " while (n > 5)\n" + " n = n - 1; // after the step\n" + " if (n) {\n" + " llOwnerSay(\"d\");\n" + " }\n" + " // over the else\n" + " else {\n" + " llOwnerSay(\"e\");\n" + " }\n" + " do ; while ((b = b - 1) > 0);\n" + " llOwnerSay((string)asInt(\"3\") + (string)CHANNEL + (string)OWNER_ONLY + (string)TIMEOUT);\n" + " }\n" + "}\n"; + for (bool spaces : { true, false }) + { + ALLSLToSLua::Options options; + options.braceSpaces = spaces; + const ALLSLToSLua::Result r = ALLSLToSLua::convert(lsl, options); + ensure("converted", r.converted); + ensure("each after its own: " + r.text, has(r, " print(\"a\") -- after a\n elseif") && has(r, " print(\"b\") -- after b\n else") && + has(r, " print(\"c\") -- after c\n end")); + ensure("an empty body's, after the line that opens it: " + r.text, has(r, " == 2 then -- an empty body\n else\n print(\"f\") -- after f\n")); + ensure("a loop's body's: " + r.text, has(r, " n -= 1 -- after the step\n end")); + ensure("between a block and its else, over what the else runs: " + r.text, has(r, " else\n -- over the else\n print(\"e\")\n")); + ensure("lined up: " + r.text, has(r, "local CHANNEL = 1 -- channel\nlocal OWNER_ONLY = 0 -- owner\nlocal TIMEOUT = 2.5 -- seconds\n")); + ensure("one line break at the end: " + r.text, r.text.size() > 2 && r.text.compare(r.text.size() - 2, 2, "\n\n") != 0 && r.text.back() == '\n'); + ALScriptFormatter::Options formatter; + formatter.lua = true; + formatter.braceSpaces = spaces; + ensure_equals(spaces ? "formatted, spaced" : "formatted, none", ALScriptFormatter::format(r.text, formatter), r.text); + checksClean(r); + } + } + + template<> template<> + void allsltoslua_object::test<95>() + { + set_test_name("a comment after an unbraced body, on the line it ends, trails the statement that ends that line: a then's, an " + "elseif's, an else's, a loop's; an empty body's the line that opens it; a one-line if's, the whole if"); + const ALLSLToSLua::Result r = convert("default { touch_start(integer n) {\n" + " integer a; integer b;\n" + " if (n == 1)\n" + " llOwnerSay(\"a\"); // after a\n" + " else if (n == 2)\n" + " llOwnerSay(\"b\"); // after b\n" + " else\n" + " llOwnerSay(\"c\"); // after c\n" + " if ( (b = 1) == 2 ); // empty\n" + " else if ( b == 2 ); // empty too\n" + " else a = 2; // after a = 2\n" + " if (a)\n" + " if (b) llOwnerSay(\"x\"); // inner, one line\n" + " while (n > 5)\n" + " n = n - 1; // after the step\n" + " while (a) ; // an empty loop\n" + " if (n == 5) llOwnerSay(\"h\"); // after h\n" + "} }\n"); + ensure("a then's, an elseif's, an else's: " + r.text, + has(r, " if n == 1 then\n print(\"a\") -- after a\n elseif n == 2 then\n print(\"b\") -- after b\n" + " else\n print(\"c\") -- after c\n end\n")); + ensure("an empty body's, the line that opens it: " + r.text, + has(r, " == 2 then -- empty\n elseif b == 2 then -- empty too\n else\n a = true -- after a = 2\n end\n")); + ensure("an if on one line inside another: " + r.text, has(r, " if b ~= 0 then print(\"x\") end -- inner, one line\n")); + ensure("a loop's body's: " + r.text, has(r, " n -= 1 -- after the step\n end\n") && has(r, " while a do -- an empty loop\n end\n")); + ensure("a one-line if, as before: " + r.text, has(r, " if n == 5 then print(\"h\") end -- after h\n")); + checksClean(r); + } + + template<> template<> + void allsltoslua_object::test<96>() + { + set_test_name("a float cut to an integer is math.modf's first value: bare where only its first is read anyway -- an operand, all " + "of a value assigned -- and kept to it by brackets where its values would spread"); + const ALLSLToSLua::Result r = convert("integer menuChannel;\n" + "vector gV;\n" + "integer cut(float f) { return (integer)f; }\n" + "default { touch_start(integer n) {\n" + " float f = llFrand(10);\n" + " menuChannel = (integer)llFrand(1000) - 10000;\n" + " integer x = (integer)f;\n" + " x = (integer)f;\n" + " x += (integer)f;\n" + " x = -(integer)f;\n" + " if ((integer)f == 3) llOwnerSay((string)((integer)f));\n" + " llOwnerSay(llList2CSV([(integer)f]));\n" + " gV.z = (integer)f;\n" + " llOwnerSay((string)(cut(f) + x + menuChannel) + (string)gV);\n" + "} }\n"); + ensure("Tapple's: " + r.text, has(r, "menuChannel = math.modf(ll.Frand(1000)) - 10000\n")); + ensure("a local: " + r.text, has(r, "local x = math.modf(f)\n")); + ensure("assigned: " + r.text, has(r, " x = math.modf(f)\n")); + ensure("added: " + r.text, has(r, " x += math.modf(f)\n")); + ensure("its negative: " + r.text, has(r, " x = -math.modf(f)\n")); + ensure("compared: " + r.text, has(r, "if math.modf(f) == 3 then")); + ensure("an argument: " + r.text, has(r, "print(tostring((math.modf(f))))")); + ensure("a list's item: " + r.text, has(r, "{ (math.modf(f)) }")); + ensure("returned: " + r.text, has(r, "return (math.modf(f))")); + ensure("a vector's part, an argument: " + r.text, has(r, "gV = vector(gV.x, gV.y, (math.modf(f)))")); + checksClean(r); + } } diff --git a/indra/alscript/tests/alscriptformatter_test.cpp b/indra/alscript/tests/alscriptformatter_test.cpp index f79d411b57..f74d8ef06e 100644 --- a/indra/alscript/tests/alscriptformatter_test.cpp +++ b/indra/alscript/tests/alscriptformatter_test.cpp @@ -382,4 +382,102 @@ namespace tut tabs.width = 99; ensure("does not", ALScriptFormatter::format(tabbed, tabs).find("\t\tllSay(\n\t\t\t0,\n") != std::string::npos); } + + template<> template<> + void alscriptformatter_object::test<12>() + { + set_test_name("Luau's braces spaced inside as asked, whatever stands next to them, a table type's too; never {} nor an " + "interpolated string's; a type's colon before a bracket spaced as written; LSL's braces as they were"); + const std::string in = "local states: {[string]: {[string]:(...any) -> ()}} = {}\n" + "local t = {{1}, {2, 3}, [k] = v, -1, #t}\n" + "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})\n" + "print(`a {b} c {{d}}`, `{ {1} }`)\n" + "local f: (number) -> () = obj:method(1)\n"; + const std::string want = "local states: { [string]: { [string]:(...any) -> () } } = {}\n" + "local t = { { 1 }, { 2, 3 }, [k] = v, -1, #t }\n" + "local params: { any } = ll.ParseStringKeepNulls(msg, { \"\\n\" }, {})\n" + "print(`a {b} c {{d}}`, `{ {1} }`)\n" + "local f: (number) -> () = obj:method(1)\n"; + ensure_equals("StyLua's, by default", lua(in), want); + ALScriptFormatter::Options none; + none.lua = true; + none.braceSpaces = false; + ensure_equals("none", ALScriptFormatter::format(in, none), + std::string("local states: {[string]: {[string]:(...any) -> ()}} = {}\n" + "local t = {{1}, {2, 3}, [k] = v, -1, #t}\n" + "local params: {any} = ll.ParseStringKeepNulls(msg, {\"\\n\"}, {})\n" + "print(`a {b} c {{d}}`, `{ {1} }`)\n" + "local f: (number) -> () = obj:method(1)\n")); + ensure_equals("a type's colon, spaced", lua("local states: {[string]: (...any) -> ()} = {}\n"), + std::string("local states: { [string]: (...any) -> () } = {}\n")); + ALScriptFormatter::Options lsl_none; + lsl_none.braceSpaces = false; + ensure_equals("LSL's braces", ALScriptFormatter::format("default { state_entry() { llSay(0, \"a\"); } }\n", lsl_none), + std::string("default { state_entry() { llSay(0, \"a\"); } }\n")); + } + + template<> template<> + void alscriptformatter_object::test<13>() + { + set_test_name("a sign against the bracket or brace after it, and after a < that compares or a minus that subtracts, spaced " + "from it: -(a), #{ 1 }, i < -1, x - #t, in Luau and in LSL; LSL's vector's first sign still against its <"); + ensure_equals("Luau", lua("local x = - (a)\nlocal y = # (t)\nlocal z = # {1}\nif i <#t and i <-1 and i > - (2) then end\n" + "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{1} -#{2} - -#t + -#t\n"), + std::string("local x = -(a)\nlocal y = #(t)\nlocal z = #{ 1 }\nif i < #t and i < -1 and i > -(2) then end\n" + "local w = not (a)\nlocal q = a - (b)\nreturn -(x)\nlocal d = #{ 1 } - #{ 2 } - -#t + -#t\n")); + ensure_equals("LSL", lsl("default { state_entry() { integer x = - (a); vector v = < -1, 0, 0 >; if (i <-1) x = ! (b); } }\n"), + std::string("default { state_entry() { integer x = -(a); vector v = <-1, 0, 0>; if (i < -1) x = !(b); } }\n")); + ensure_equals("a list of vectors", lsl("list l = [<-1, 0, 0>, < -2, 0, 0 >];\n"), std::string("list l = [<-1, 0, 0>, <-2, 0, 0>];\n")); + } + + template<> template<> + void alscriptformatter_object::test<14>() + { + set_test_name("what the author lined up stays lined up: a run's =, at the author's column or past the longest, a comment on a line " + "of its own going on past; a run's trailing comments where its code changed; not equal columns by chance; tabs to " + "their stops"); + const std::string lined = "local CHANNEL = 1 -- channel\n" + "local OWNER_ONLY = true -- owner\n" + "-- a note over the next\n" + "local USE_OWNER_SAY = true -- say\n"; + ensure_equals("as it was", lua(lined), lined); + ensure_equals("spaced afresh, lined up again", lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n"), + std::string("local a = 1 -- one\nlocal bbb = { 2, 3 } -- two\n")); + ensure_equals("the author's column", lua("local a = 1\nlocal bb = 2\n"), std::string("local a = 1\nlocal bb = 2\n")); + ensure_equals("past the longest", lua("t[i+1] = 1\nt[ii] = 2\n"), std::string("t[i + 1] = 1\nt[ii] = 2\n")); + ensure_equals("not by chance", lua("ab=1\nc = 2\n"), std::string("ab = 1\nc = 2\n")); + ensure_equals("not across a blank line", lua("local a = 1\n\nlocal bb = 2\n"), std::string("local a = 1\n\nlocal bb = 2\n")); + const std::string fields = "local t = {\n alpha = 1,\n b = 2,\n}\n"; + ensure_equals("a table's fields", lua(fields), fields); + ensure_equals("LSL, by tabs", lsl("integer a\t= 1;\ninteger bb\t= 2;\n"), std::string("integer a = 1;\ninteger bb = 2;\n")); + // The formatter's own, again: the same. + ensure_equals("again", lua(lua("local a =1 -- one\nlocal bbb = {2,3} -- two\n")), + std::string("local a = 1 -- one\nlocal bbb = { 2, 3 } -- two\n")); + } + + template<> template<> + void alscriptformatter_object::test<15>() + { + set_test_name("a comment the author wrote out of the block over an elseif, else, until or end stays as far in as that line; one " + "written in the block stays in it; in an empty block, as written where the text is indented; flat text, in"); + const std::string chain = "if a then\n" + " x()\n" + "-- about b\n" + "elseif b then\n" + " y()\n" + " -- the end of y\n" + "else\n" + " z()\n" + "end\n"; + ensure_equals("as written", lua(chain), chain); + const std::string repeat = "local function f()\n" + " repeat\n" + " -- why\n" + " until x\n" + "end\n"; + ensure_equals("an empty block, indented", lua(repeat), repeat); + ensure_equals("flat", lua("if a then\nx()\n-- note\nend\n"), std::string("if a then\n x()\n -- note\nend\n")); + ensure_equals("LSL", lsl("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n"), + std::string("default\n{\n touch_start(integer n)\n {\n x();\n // about the end\n }\n}\n")); + } } diff --git a/indra/newview/alfloaterscriptpreferences.cpp b/indra/newview/alfloaterscriptpreferences.cpp index badf86605a..f8adbc6bc3 100644 --- a/indra/newview/alfloaterscriptpreferences.cpp +++ b/indra/newview/alfloaterscriptpreferences.cpp @@ -91,7 +91,7 @@ namespace "ALScriptStudioAutoClose", "ALScriptStudioCaretStyle", "ALScriptStudioCaretBlink", "ALScriptStudioHoverCards", "ALScriptStudioHoverDelay", "ALScriptStudioVimClipboard", - "ALScriptFormatBlankLines", "ALScriptFormatSpacing", "ALScriptFormatWidth", "ALScriptFormatOnSave", "ALScriptTrimOnSave", "ALScriptFixOnSave", + "ALScriptFormatBlankLines", "ALScriptFormatSpacing", "ALScriptFormatBraceSpaces", "ALScriptFormatWidth", "ALScriptFormatOnSave", "ALScriptTrimOnSave", "ALScriptFixOnSave", "ALScriptTemplateLSL", "ALScriptTemplateSLua", "ALScriptPreprocDefines", "ExternalEditor", "ALScriptConvertLLTimers", "ALScriptConvertDetectedTable", "ALScriptConvertSLuaCalls", "ALScriptConvertIdioms", "ALScriptConvertHandlerFields", "ALScriptConvertTypes", "ALScriptConvertComments", "ALScriptConvertKeepComments", diff --git a/indra/newview/alfloaterscriptstudio.cpp b/indra/newview/alfloaterscriptstudio.cpp index 8e51269a9f..e16fff6e7f 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -7625,6 +7625,7 @@ void ALFloaterScriptStudio::format(Doc& doc, bool selection_only) options.tabs = !doc.editor->getSoftTabs(); options.maxBlankLines = llclamp(gSavedSettings.getS32("ALScriptFormatBlankLines"), 0, 10); options.spacing = gSavedSettings.getBOOL("ALScriptFormatSpacing"); + options.braceSpaces = gSavedSettings.getBOOL("ALScriptFormatBraceSpaces"); options.width = llmax(0, gSavedSettings.getS32("ALScriptFormatWidth")); const std::string text = document.text(); // Line for line, since only some lines may be asked for and the @@ -8193,6 +8194,8 @@ void ALFloaterScriptStudio::convertToSLua(Doc& doc) options.types = gSavedSettings.getBOOL("ALScriptConvertTypes"); options.comments = gSavedSettings.getBOOL("ALScriptConvertComments"); options.keepComments = gSavedSettings.getBOOL("ALScriptConvertKeepComments"); + // Its tables' braces as the formatter writes them. + options.braceSpaces = gSavedSettings.getBOOL("ALScriptFormatBraceSpaces"); // Its notes, and its comments, in the skin's words. options.words = alScriptKeyedWords; const std::string source = doc.editor->wholeText(); diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 63cb616c64..df436de314 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -882,6 +882,17 @@ Value 1 + ALScriptFormatBraceSpaces + + Comment + Script Studio writes a space inside SLua's braces, { 1, 2 } and { any }, as StyLua does: its formatter, and Convert to SLua; off, {1, 2}. + Persist + 1 + Type + Boolean + Value + 1 + ALScriptFormatWidth Comment diff --git a/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml b/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml index 18651c94e5..cd8a3aefc5 100644 --- a/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml +++ b/indra/newview/skins/default/xui/en/floater_script_studio_prefs.xml @@ -575,6 +575,17 @@ end tool_tip="Formatting breaks a line with more columns than this at the commas of a bracket on it, each part on a line of its own; 0 never breaks a line" top_delta="-2" width="60" /> +