diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 81a0bae247..d8c347eb14 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -506,6 +506,10 @@ namespace // key of it than LSL's held: no text, which is NULL_KEY, and a key // with capitals, which it writes small. Whether it said either. bool keyText(LSLExpression* at, std::string_view text); + // What a variable declared with nothing starts as: its type's empty + // value, and for a key held as a uuid NULL_KEY, which is said, as + // LSL's held "", which no uuid holds. + std::string declaredWithNothing(LSLIdentifier* id, LSLIType type); // Why a list is shared, as SluaListCopy says it. std::string sharedWords(Shared why) const; @@ -992,12 +996,13 @@ namespace // Whether listItem writes a call as an index into its list: its // items already what is asked for. bool listItemFits(LSLFunctionExpression* e); - // llList2Key where keys are held as text: its item read as text, as - // LSL gave it -- a string's, a key's -- where SLua's List2Key - // answers NULL_KEY for text that is no UUID. + // llList2Key where keys are held as text, or where what it gives is + // made text (Writer::coerced): its item read as text, as LSL gave + // it -- a string's, a key's -- where SLua's List2Key answers + // NULL_KEY for text that is no UUID. bool textKeyItem(LSLFunctionExpression* e) const { - return !mOptions.uuidKeys && std::string_view(e->getIdentifier()->getName()) == "llList2Key"; + return (!mOptions.uuidKeys || mTextItems.contains(e)) && std::string_view(e->getIdentifier()->getName()) == "llList2Key"; } // --- keys that hold text ----------------------------------------------------- @@ -1186,6 +1191,8 @@ namespace // Comparisons written the other way round, a not before them. boost::unordered_flat_set mFlipped; boost::unordered_flat_set mTextKeys; + // llList2Key's calls whose key is made text (textKeyItem). + boost::unordered_flat_set mTextItems; }; std::string Writer::said(const char* key, const char* english, const std::vector& args) const @@ -2742,6 +2749,17 @@ namespace return Expr{ "`" + body + "`" }; } + std::string Writer::declaredWithNothing(LSLIdentifier* id, LSLIType type) + { + if (type == LST_KEY) + { + noteOnce(id, "SluaKeyDeclared", + "A key declared with nothing is NULL_KEY here, where LSL's held \"\": it equals NULL_KEY and reads as NULL_KEY's " + "text, where LSL's did neither."); + } + return defaultOf(type); + } + bool Writer::keyText(LSLExpression* at, std::string_view text) { if (text.empty()) @@ -2764,6 +2782,13 @@ namespace Expr Writer::coerced(LSLExpression* e, LSLIType to) { + // llList2Key's key made text: its item read as text instead, as LSL + // gave it, where the text of SLua's List2Key of "" is NULL_KEY's. + if (LSLExpression* inner = unbracketed(e); to == LST_STRING && inner && inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && + std::string_view(static_cast(inner)->getIdentifier()->getName()) == "llList2Key") + { + mTextItems.insert(inner); + } Expr out = value(e); const LSLIType from = slType(e); if (to == LST_KEY && from == LST_STRING) @@ -3852,7 +3877,7 @@ namespace return *written; } } - // llList2Key's item as text, where keys are held as text: as + // llList2Key's item as text, where its key is made text: as // List2String reads it, which differs only for an item neither a // string nor a key. if (textKeyItem(e)) @@ -3860,7 +3885,7 @@ namespace if ((itemTypes(argumentAt(e, 0)) & ~(ItemString | ItemKey)) != 0) { noteOnce(e, "SluaList2KeyText", - "Keys are held as text, so llList2Key's item is read as List2String reads it, as LSL gave it, where SLua's " + "Its key is made text, so llList2Key's item is read as List2String reads it, as LSL gave it, where SLua's " "List2Key answers NULL_KEY for text that is no UUID; a number's or a vector's text is given, where LSL gave \"\"."); } lsl = "llList2String"; @@ -5354,7 +5379,7 @@ namespace } else { - line("local " + name + typed(type) + " = " + (isNull(init) ? defaultOf(type) : oneValue(coerced(init, type)).text)); + line("local " + name + typed(type) + " = " + (isNull(init) ? declaredWithNothing(id, type) : oneValue(coerced(init, type)).text)); } writeSteps(steps.after); return; @@ -7699,12 +7724,16 @@ namespace return LST_KEY; } LSLExpression* inner = unbracketed(e); - // Where the script's keys are held as text: a cast to a key, a call - // of its own that gives one, and llList2Key's item (textKeyItem). + // Where the script's keys are held as text: a cast to a key, and a + // call of its own that gives one. if (!mOptions.uuidKeys && inner && e->getIType() == LST_KEY && - (inner->getNodeSubType() == NODE_TYPECAST_EXPRESSION || - (inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && - (readOf(inner) || textKeyItem(static_cast(inner)))))) + (inner->getNodeSubType() == NODE_TYPECAST_EXPRESSION || (inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && readOf(inner)))) + { + return LST_STRING; + } + // llList2Key's item, read as text (textKeyItem). + if (inner && e->getIType() == LST_KEY && inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && + textKeyItem(static_cast(inner))) { return LST_STRING; } @@ -7752,20 +7781,11 @@ namespace }); // What each key variable is given, and each key function of the // script's own returns; each that is given text, until none more - // is. One declared with nothing holds LSL's "", no UUID, which a - // uuid cannot: uuid("") is NULL_KEY. + // is. One declared with nothing is NULL_KEY, as Tapple asked, though + // LSL's held "" (Writer::declaredWithNothing). std::vector> given; - std::vector unset; const auto key = [&](LSLIdentifier* id, LSLExpression* init) { - if (id->getIType() != LST_KEY || !id->getSymbol()) - { - return; - } - if (isNull(init)) - { - unset.push_back(id->getSymbol()); - } - else + if (id->getIType() == LST_KEY && id->getSymbol() && !isNull(init)) { given.emplace_back(id->getSymbol(), init); } @@ -7862,8 +7882,7 @@ namespace // written out -- a variable's, a call's -- or given such a key. // Given on to a key held as text, uuid() would have made "" NULL_KEY // on the way, so such a key holds text too. - boost::unordered_flat_set loose(unset.begin(), unset.end()); - mTextKeys.insert(unset.begin(), unset.end()); + boost::unordered_flat_set loose; const auto loosely = [&](LSLExpression* e) { e = unbracketed(e); if (e && e->getIType() == LST_STRING) @@ -7928,7 +7947,7 @@ namespace else { const LSLIType type = varType(id->getSymbol(), id->getIType()); - line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? defaultOf(type) : oneValue(coerced(init, type)).text)); + line("local " + nameOf(id) + typed(type) + " = " + (isNull(init) ? declaredWithNothing(id, type) : oneValue(coerced(init, type)).text)); } commentsAfter(global, from); lined(global, from); diff --git a/indra/alscript/luau/alluauservice.cpp b/indra/alscript/luau/alluauservice.cpp index 450230948c..2a6e3c531c 100644 --- a/indra/alscript/luau/alluauservice.cpp +++ b/indra/alscript/luau/alluauservice.cpp @@ -1831,6 +1831,31 @@ namespace } return true; } + + // A field's type as the table or extern type it is on has it, where the + // field itself was given none. + std::optional fieldTypeOf(const Luau::Module& module, const Luau::AstExprIndexName& field) + { + const Luau::TypeId* table = module.astTypes.find(field.expr); + if (!table) + { + return std::nullopt; + } + const Luau::TypeId type = Luau::follow(*table); + if (const Luau::TableType* t = Luau::getTableType(type)) + { + const auto it = t->props.find(field.index.value); + return it == t->props.end() ? std::nullopt : it->second.readTy; + } + if (const Luau::ExternType* cls = Luau::get(type)) + { + if (const Luau::Property* prop = Luau::lookupExternTypeProp(cls, field.index.value)) + { + return prop->readTy; + } + } + return std::nullopt; + } } ALScriptHover ALLuauService::hover(std::string_view source, S32 line, S32 column) @@ -1878,6 +1903,42 @@ ALScriptHover ALLuauService::hover(std::string_view source, S32 line, S32 column } } } + // A field a function statement gives a function -- `function + // LLEvents.on_rez(n)` -- which the old solver types only the table of: + // the field as the table has it. And a field that holds a function or + // nil, as an event's handler does, by its function: what is written + // there, and what is said of it. + if (Luau::AstExprIndexName* field = expr ? expr->as() : nullptr) + { + if (!type) + { + type = fieldTypeOf(*module, *field); + } + if (type) + { + if (const Luau::UnionType* either = Luau::get(Luau::follow(*type))) + { + std::optional function; + bool other = false; + for (Luau::TypeId option : either->options) + { + const Luau::TypeId one = Luau::follow(option); + if (Luau::get(one) && !function) + { + function = one; + } + else if (!Luau::isNil(one)) + { + other = true; + } + } + if (function && !other) + { + type = function; + } + } + } + } if (!type) { return answer; diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 50eeac7a68..8c4122f771 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -1210,8 +1210,8 @@ namespace { INFO("compared as text"); CHECK(has(r, "if id == DOMAIN then")); - // gOwner declared with nothing holds text, as LSL's "" did. - CHECK(has(r, "if id == gOwner then")); + // gOwner declared with nothing is NULL_KEY, a uuid, made text. + CHECK(has(r, "if id == tostring(gOwner) then")); } { INFO("a key given the id, text"); @@ -1956,13 +1956,8 @@ namespace CHECK(has(r, "table.insert(params, n)")); } { - // LSL's key declared with nothing holds "", not NULL_KEY, and - // is printed as no text; a uuid cannot hold it. - INFO("a key given nothing, no text"); - CHECK(has(r, "local gSitter = \"\"")); - CHECK(has(r, "{gSitter}`)")); - CHECK_FALSE(has(r, "tostring(gSitter)")); - CHECK_FALSE(has(r, "NULL_KEY")); + INFO("the null key, for a key declared with nothing (QJ1)"); + CHECK(has(r, "local gSitter = NULL_KEY")); CHECK_FALSE(has(r, "uuid(\"\")")); } { @@ -4361,7 +4356,7 @@ namespace INFO("a call that changes the world, named with the other"); CHECK(noted(r, "SluaRightFirstWorld", "say()")); CHECK(has(r, "-- LSL: LSL ran the right side of this before the left, and Luau runs the left first: say() may change what " - "llKey2Name() does or finds outside the script.\n print(ll.Key2Name(gSender :: any) .. say())")); + "llKey2Name() does or finds outside the script.\n print(ll.Key2Name(gSender) .. say())")); } { INFO("a call that sets what the other's reads"); @@ -4499,14 +4494,14 @@ namespace CHECK_FALSE(has(r, "{ uuid(")); } { - // LSL's key declared with nothing holds "", which a uuid cannot: - // uuid("") is NULL_KEY, and d == NULL_KEY was FALSE. - INFO("a key given nothing, no text, against text as text"); - CHECK(has(r, "local d = \"\"")); - CHECK(has(r, "if d == \"\" then")); - CHECK(has(r, "if \"\" ~= d then")); - CHECK(has(r, "if d == tostring(NULL_KEY) then")); - CHECK_FALSE(has(r, "local d = NULL_KEY")); + // Tapple's (QJ1): as `key d = NULL_KEY;`, said; d == NULL_KEY, + // FALSE in LSL, is true now, as the note says. + INFO("a key given nothing, NULL_KEY, against no text as a uuid"); + CHECK(has(r, "local d = NULL_KEY")); + CHECK(noted(r, "SluaKeyDeclared")); + CHECK(has(r, "if d == uuid(\"\") then")); + CHECK(has(r, "if uuid(\"\") ~= d then")); + CHECK(has(r, "if d == NULL_KEY then")); } UNSCOPED_INFO("a uuid key against a UUID written out, a uuid"); CHECK(has(r, "if u == uuid(\"a2e76fcd-9360-4f6d-a924-938f923df11d\") then")); @@ -4604,10 +4599,9 @@ namespace const ALLSLToSLua::Result uuids = require_converted(lsl); CAPTURE(uuids.text); { - // A key declared with nothing holds LSL's "" either way, which - // no uuid holds. + // A key declared with nothing is NULL_KEY, as Tapple asked. INFO("uuids by default"); - CHECK(has(uuids, "local gOwner = \"\"\n")); + CHECK(has(uuids, "local gOwner = NULL_KEY\n")); CHECK(has(uuids, "local cast = uuid(s)")); } } @@ -4705,7 +4699,7 @@ namespace "}\n"; ALLSLToSLua::Options o; o.types = true; - for (const char* global : { "key driver = \"\";\n", "key driver;\n" }) + for (const char* global : { "key driver = \"\";\n" }) { const ALLSLToSLua::Result r = ALLSLToSLua::convert(global + body, o); CAPTURE(global, r.text); @@ -4739,7 +4733,8 @@ namespace } require_checks_clean(r); } - for (const char* global : { "key driver = NULL_KEY;\n", "key driver = \"00000000-0000-0000-0000-000000000000\";\n" }) + // Declared with nothing, NULL_KEY too (QJ1). + for (const char* global : { "key driver = NULL_KEY;\n", "key driver = \"00000000-0000-0000-0000-000000000000\";\n", "key driver;\n" }) { const ALLSLToSLua::Result r = ALLSLToSLua::convert(global + body, o); CAPTURE(global, r.text); @@ -4946,10 +4941,15 @@ namespace const ALLSLToSLua::Result uuids = require_converted(lsl); CAPTURE(uuids.text); { - INFO("uuids by default: List2Key's uuid"); - CHECK(has(uuids, "local msg = tostring(llcompat.List2Key(l, 0))")); + INFO("uuids by default: List2Key's uuid for a key"); CHECK(has(uuids, "local k = llcompat.List2Key(l, 0)")); - CHECK_FALSE(noted(uuids, "SluaList2KeyText")); + } + { + INFO("and its item's text where it is made text, which tostring of List2Key's NULL_KEY was not (QJ1)"); + CHECK(has(uuids, "local msg = l[1] or \"\"\n")); + CHECK(has(uuids, "print(llcompat.List2String(gMixed, 0))")); + CHECK(noted(uuids, "SluaList2KeyText")); + CHECK_FALSE(has(uuids, "tostring(llcompat.List2Key(")); } } @@ -4980,27 +4980,48 @@ namespace require_checks_clean(close); } - TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a key declared with nothing holds no text, as LSL's did, not NULL_KEY", "[ALLSLToSLua]") + // Tapple's (QA8, and again in QJ1): a key declared with nothing is NULL_KEY where keys are + // uuids, as `key k = NULL_KEY;` is, though LSL's held "" -- said at the declaration. + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a key declared with nothing is NULL_KEY, and said so", "[ALLSLToSLua]") { - const ALLSLToSLua::Result r = require_converted("key gTarget;\n" - "default { touch_start(integer n) {\n" - " if (gTarget == NULL_KEY) llOwnerSay(\"null\");\n" - " else llOwnerSay(\"none yet: \" + (string)gTarget);\n" - " gTarget = llDetectedKey(0);\n" - " if (gTarget) llOwnerSay(\"set\");\n" - "} }\n"); + const std::string lsl = "key gTarget;\n" + "default { touch_start(integer n) {\n" + " key other;\n" + " if (gTarget == NULL_KEY) llOwnerSay(\"null\");\n" + " else llOwnerSay(\"none yet: \" + (string)gTarget);\n" + " gTarget = llDetectedKey(0);\n" + " if (gTarget) llOwnerSay(\"set\");\n" + " other = gTarget;\n" + "} }\n"; + const ALLSLToSLua::Result r = require_converted(lsl); CAPTURE(r.text); - UNSCOPED_INFO("held as text, no text"); - CHECK(has(r, "local gTarget = \"\"\n")); - UNSCOPED_INFO("against NULL_KEY as text: FALSE, as LSL's"); - CHECK(has(r, "if gTarget == tostring(NULL_KEY) then")); - UNSCOPED_INFO("printed as it is"); - CHECK(has(r, "\"none yet: \" .. gTarget")); - UNSCOPED_INFO("a key given it, as text"); - CHECK(has(r, "gTarget = tostring(detected[1]:getKey())")); - UNSCOPED_INFO("its truth as LSL's key's"); - CHECK(has(r, "if (touuid(gTarget) or NULL_KEY).istruthy then")); + UNSCOPED_INFO("a uuid, NULL_KEY"); + CHECK(has(r, "local gTarget = NULL_KEY\n")); + CHECK(has(r, "local other = NULL_KEY\n")); + CHECK(noted(r, "SluaKeyDeclared")); + { + INFO("said once, over the first"); + const size_t said = r.text.find("A key declared with nothing is NULL_KEY here"); + REQUIRE(said != std::string::npos); + CHECK(said < r.text.find("local gTarget = NULL_KEY")); + CHECK(r.text.find("A key declared with nothing", said + 1) == std::string::npos); + } + UNSCOPED_INFO("against NULL_KEY as a uuid"); + CHECK(has(r, "if gTarget == NULL_KEY then")); + UNSCOPED_INFO("a key given it as it is"); + CHECK(has(r, "gTarget = detected[1]:getKey()")); + CHECK(has(r, "if gTarget.istruthy then")); require_checks_clean(r); + + ALLSLToSLua::Options text; + text.uuidKeys = false; + const ALLSLToSLua::Result as_text = ALLSLToSLua::convert(lsl, text); + CAPTURE(as_text.text); + REQUIRE(as_text.converted); + UNSCOPED_INFO("keys as text: no text, as LSL's held, and nothing said"); + CHECK(has(as_text, "local gTarget = \"\"\n")); + CHECK_FALSE(noted(as_text, "SluaKeyDeclared")); + require_checks_clean(as_text); } TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a loop body's local is named apart from what its check or steps read after it", "[ALLSLToSLua]") @@ -5120,4 +5141,51 @@ namespace CHECK(has(r, "print(tostring(_))\n")); require_checks_clean(r); } + + // Tapple's: a key held as text given llList2Key of a list of "" took + // tostring of SLua's List2Key, whose "" is NULL_KEY, so it was no + // longer "" and went into the list. Declared with nothing, the key is + // NULL_KEY, as Tapple asked, and keeps List2Key's uuid. + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a key held as text given llList2Key's item holds the item's text", "[ALLSLToSLua]") + { + const std::string body = " list newlyStood = [];\n" + " list newAssignments = [\"\", \"\", \"\", \"\"];\n" + " av = llList2Key(newAssignments, 0);\n" + " if (av != \"\") {\n" + " newlyStood += [av];\n" + " }\n" + " llOwnerSay(llList2Json(JSON_ARRAY, newlyStood));\n" + " seated(llList2Key(newAssignments, 1));\n" + "} }\n"; + const std::string seated = "seated(key who) { if (who == \"\") llOwnerSay(\"empty\"); }\n"; + ALLSLToSLua::Options o; + o.types = true; + for (const char* decl : { "key av = \"\";\n" }) + { + const ALLSLToSLua::Result r = ALLSLToSLua::convert(seated + "default { state_entry() {\n " + decl + body, o); + CAPTURE(decl, r.text); + REQUIRE(r.converted); + CHECK(has(r, "local av: string = \"\"\n")); + CHECK(has(r, "\nav = newAssignments[1] or \"\"\n")); + CHECK(has(r, "\nif av ~= \"\" then\n")); + UNSCOPED_INFO("a parameter held as a uuid keeps List2Key's uuid, which equals \"\" as LSL's did"); + CHECK(has(r, "local function seated(who: uuid)")); + CHECK(has(r, "seated(llcompat.List2Key(newAssignments, 1))")); + CHECK(has(r, "if who == uuid(\"\") then")); + CHECK_FALSE(has(r, "tostring(llcompat.List2Key(")); + require_checks_clean(r); + } + for (const char* decl : { "key av;\n", "key av = NULL_KEY;\n" }) + { + INFO("a uuid key keeps List2Key, as Tapple asked"); + const ALLSLToSLua::Result r = + ALLSLToSLua::convert(std::string("default { state_entry() {\n ") + decl + body.substr(0, body.find(" seated")) + "} }\n", o); + CAPTURE(decl, r.text); + REQUIRE(r.converted); + CHECK(has(r, "local av: uuid = NULL_KEY\n")); + CHECK(has(r, "\nav = llcompat.List2Key(newAssignments, 0)\n")); + CHECK(has(r, "\nif av ~= uuid(\"\") then\n")); + require_checks_clean(r); + } + } } diff --git a/indra/alscript/tests/alluauservice_test.cpp b/indra/alscript/tests/alluauservice_test.cpp index c4a39a3902..b8f1170838 100644 --- a/indra/alscript/tests/alluauservice_test.cpp +++ b/indra/alscript/tests/alluauservice_test.cpp @@ -657,6 +657,33 @@ namespace CHECK(hover.label.rfind(newSolver ? "function print<" : "function print(", 0) == 0); } + // Tapple's: an event's handler, as it is written, tells its parameters + // under either solver; the old one typed only LLEvents there. + TEST_CASE_METHOD(alluauservice_data, "alluauservice: hover on an event's handler being written gives its parameters", "[alluauservice]") + { + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); + const std::string script = "function LLEvents.run_time_permissions()\n" + "end\n" + "print(LLEvents.touch_start)\n"; + for (const bool solver : { newSolver, !newSolver }) + { + std::string why; + REQUIRE(service.setNewSolver(solver, why)); + CAPTURE(solver); + ALScriptHover hover = service.hover(script, 0, 20); // run_time_permissions + REQUIRE(hover.found); + UNSCOPED_INFO("the handler by its signature: " + hover.label); + CHECK(hover.label.rfind("function LLEvents.run_time_permissions(perm: number)", 0) == 0); + hover = service.hover(script, 2, 18); // touch_start, read + REQUIRE(hover.found); + UNSCOPED_INFO("and read: " + hover.label); + CHECK(hover.label.find("detected: {DetectedEvent}") != std::string::npos); + } + std::string why; + CHECK(service.setNewSolver(newSolver, why)); + } + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the configuration adds globals and turns lints off until it is replaced", "[alluauservice]") { UNSCOPED_INFO("definitions loaded: " + error); diff --git a/indra/newview/alfloaterscriptstudio.cpp b/indra/newview/alfloaterscriptstudio.cpp index 1e6c4f4636..f9acff3e73 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -896,11 +896,13 @@ void ALFloaterScriptStudio::listenToWorkspace() // A file's tab says how many scripts it is the master of, which links // made and let go of change. mLinkBadgesConnection = ALScriptLinkBadges::instance().onChanged([this]() { fillTabs(); }); - // A file's tab stands in for the one script it masters, which links - // made and let go of change. + // A file's tab stands in for the one script it masters, and keeps the + // prims of every script it masters listed, which links made and let go + // of change. mMastersConnection = ALScriptDiskMasters::instance().onChanged([this]() { restandAll(); refreshToolbar(); + relistExplorer(); }); // New definitions from the region: the analyzers reload, the words // are rebuilt, and every script is checked again. diff --git a/indra/newview/alscriptexplorerpane.cpp b/indra/newview/alscriptexplorerpane.cpp index 7c99db0d21..807070d89e 100644 --- a/indra/newview/alscriptexplorerpane.cpp +++ b/indra/newview/alscriptexplorerpane.cpp @@ -442,12 +442,24 @@ void ALScriptExplorerPane::relist(bool refetch, bool from_region) }; listing.nameless = [this](const LLUUID& root) { return nameGivenTo(root); }; listing.selected = mRoots; + // A prim holding a script open, or open through its file: a file's tab + // is open for each script it masters, which has no tab of its own. for (const ALScriptStudioDoc* doc : mServices->openDocs()) { if (!doc->ref.inInventory()) { listing.open.push_back(doc->ref.object); } + else if (!doc->file.empty()) + { + for (const ALMasterLink& link : ALScriptDiskMasters::instance().mastering(doc->file)) + { + if (link.state != ALMasterLink::State::Orphaned && link.object.notNull()) + { + listing.open.push_back(link.object); + } + } + } } listing.unnamed = mServices->words("ObjectUnnamed"); mModel.list(listing); diff --git a/indra/newview/alscriptnoticebar.cpp b/indra/newview/alscriptnoticebar.cpp index e805813549..d03d5c6caf 100644 --- a/indra/newview/alscriptnoticebar.cpp +++ b/indra/newview/alscriptnoticebar.cpp @@ -31,6 +31,7 @@ #include "llbutton.h" #include "llfloater.h" #include "llfontgl.h" +#include "lllayoutstack.h" #include "lltextbox.h" static LLPanelInjector t_script_studio_notice("script_studio_notice"); @@ -98,4 +99,40 @@ void ALScriptNoticeBar::show(const Notice& notice) } const LLRect words = mText->getRect(); mText->setShape(LLRect(words.mLeft, words.mTop, llmax(words.mLeft + 40, right - 6), words.mBottom)); + fitWords(); +} + +void ALScriptNoticeBar::reshape(S32 width, S32 height, bool called_from_parent) +{ + const bool resized = width != getRect().getWidth(); + LLPanel::reshape(width, height, called_from_parent); + // The words wrap again at the width they follow. + if (resized && mText && getParent() && getParent()->getVisible()) + { + fitWords(); + } +} + +void ALScriptNoticeBar::fitWords() +{ + // As many lines as the words take at their width, up to MAX_LINES; the + // rest is in the tip. + const LLRect words = mText->getRect(); + const S32 full = mText->getTextPixelHeight(); + const S32 lines = llmax(1, mText->getLineCount()); + const S32 tall = llmax(1, full / lines) * llmin(lines, MAX_LINES); + if (tall != words.getHeight()) + { + mText->setShape(LLRect(words.mLeft, words.mTop, words.mRight, words.mTop - tall)); + } + // The bar as far above the words as below them. + if (LLLayoutPanel* holder = ALViewType::as(getParent())) + { + const S32 margin = getRect().getHeight() - words.mTop; + const S32 wanted = llmax(MIN_HEIGHT, margin + tall + margin); + if (wanted != holder->getTargetDim()) + { + holder->setTargetDim(wanted); + } + } } diff --git a/indra/newview/alscriptnoticebar.h b/indra/newview/alscriptnoticebar.h index 2db768b478..60fa3638cf 100644 --- a/indra/newview/alscriptnoticebar.h +++ b/indra/newview/alscriptnoticebar.h @@ -37,8 +37,9 @@ class LLTextBox; // front has to reckon with -- a text kept from an earlier session, its // object out of sight, its item gone, the connection lost, the file gone // from disk -- with up to four things to be done about it and a way to hide -// it. Its layout panel is shown while it has something to say. What it -// says is ALScriptStudioOrphans's; what its buttons do is the window's. +// it. Its layout panel is shown while it has something to say, as tall as +// its words wrapped, up to three lines. What it says is +// ALScriptStudioOrphans's; what its buttons do is the window's. class ALScriptNoticeBar : public LLPanel { public: @@ -68,11 +69,18 @@ class ALScriptNoticeBar : public LLPanel }; bool postBuild() override; + void reshape(S32 width, S32 height, bool called_from_parent = true) override; void show(const Notice& notice); // What each button does now, from the first. const std::string& action(size_t which) const { return mActions[which < BUTTONS ? which : 0]; } private: + // The words' lines, and the bar's layout panel as tall as they are. + void fitWords(); + + static constexpr S32 MAX_LINES = 3; + static constexpr S32 MIN_HEIGHT = 28; + ALScriptStudioServices* mServices = nullptr; Window* mWindow = nullptr; LLTextBox* mText = nullptr; diff --git a/indra/newview/alscriptoutputpane.cpp b/indra/newview/alscriptoutputpane.cpp index 38f032f04c..5298b6b05f 100644 --- a/indra/newview/alscriptoutputpane.cpp +++ b/indra/newview/alscriptoutputpane.cpp @@ -359,6 +359,7 @@ void ALScriptOutputPane::said(const std::string& text, bool failure, const ALScr : action == "make_strict" ? "ActionMakeStrict" : action == "master_link_hint" ? "ActionMasterLink" : action == "master_send" ? "ActionMasterSend" + : action == "master_take_world" ? "ActionMasterTakeWorld" : action == "master_compare" ? "ActionMasterCompare" : action == "master_compare_file" ? "ActionMasterCompareFile" : action == "master_give_way" ? "ActionMasterGiveWay" diff --git a/indra/newview/alscriptstudiomasters.cpp b/indra/newview/alscriptstudiomasters.cpp index c7816fa74e..8f8947a14d 100644 --- a/indra/newview/alscriptstudiomasters.cpp +++ b/indra/newview/alscriptstudiomasters.cpp @@ -257,7 +257,8 @@ void ALScriptStudioMasters::linkTo(Doc& doc, std::string path, ALMasterLink::Mad { said += " " + mServices.words("MasterLinkDiffers", args); } - mServices.report(said, bridged, tab, differs && tab ? std::vector{ "master_send", "master_compare" } : std::vector()); + mServices.report(said, bridged, tab, + differs && tab ? std::vector{ "master_send", "master_take_world", "master_compare" } : std::vector()); } void ALScriptStudioMasters::unlink(Doc& doc) @@ -499,6 +500,10 @@ void ALScriptStudioMasters::offer(Doc& doc, const std::string& action) { compareWithWorld(doc, ref); } + else if (action == "master_take_world" && !doc.file.empty() && !ref.isNull()) + { + takeWorld(doc, ref); + } else if (action == "master_compare_file" && doc.file.empty() && !ref.isNull()) { compareWithFile(doc, ref); @@ -566,16 +571,26 @@ void ALScriptStudioMasters::offer(Doc& doc, const std::string& action) } } -void ALScriptStudioMasters::compareWithWorld(Doc& doc, const ALScriptRef& ref) +void ALScriptStudioMasters::loadWorld(Doc& doc, const ALScriptRef& ref, + std::function then) { // Answered later, the tab found again by its id: the window may have // closed meanwhile, or the tab. const std::string id = doc.id; const std::weak_ptr alive = mAlive; - mWindow.loadWorldText(ref, [this, alive, id](const ALScriptLoaded& loaded) { + mWindow.loadWorldText(ref, [this, alive, id, then = std::move(then)](const ALScriptLoaded& loaded) { Doc* found = alive.lock() ? mServices.findDoc(id) : nullptr; - if (!found || !loaded.error.empty()) + if (!found) + { + return; + } + // Not read: said, rather than the button having done nothing. + if (!loaded.error.empty()) { + LLStringUtil::format_map_t args; + args["[NAME]"] = loaded.name.empty() ? found->name : loaded.name; + args["[WHY]"] = loaded.error; + mServices.report(mServices.words("MasterWorldNotRead", args), true, found); return; } // The author's text as it is in the world, out of its envelope. @@ -584,13 +599,40 @@ void ALScriptStudioMasters::compareWithWorld(Doc& doc, const ALScriptRef& ref) { theirs = envelope->source; } + then(*found, theirs, loaded.name); + }); +} + +void ALScriptStudioMasters::compareWithWorld(Doc& doc, const ALScriptRef& ref) +{ + loadWorld(doc, ref, [this](Doc& tab, const std::string& theirs, const std::string& name) { LLStringUtil::format_map_t args; - args["[NAME]"] = loaded.name; - mWindow.compare(*found, found->editor->wholeText(), theirs, mServices.words("CompareMasterFile", args), + args["[NAME]"] = name; + mWindow.compare(tab, tab.editor->wholeText(), theirs, mServices.words("CompareMasterFile", args), mServices.words("CompareMasterWorld", args)); }); } +void ALScriptStudioMasters::takeWorld(Doc& doc, const ALScriptRef& ref) +{ + loadWorld(doc, ref, [this](Doc& tab, const std::string& theirs, const std::string& name) { + LLStringUtil::format_map_t args; + args["[NAME]"] = name; + args["[FILE]"] = fileNameOf(tab.file); + if (theirs == tab.editor->wholeText()) + { + mServices.report(mServices.words("MasterTookSame", args), false, &tab); + return; + } + // The file's text a step back, as an edit would leave it; the file + // on disk written only by a save of it, which then sends what the + // script already holds. + tab.editor->setSelection(ALTextRange(tab.editor->document().start(), tab.editor->document().end())); + tab.editor->insertText(theirs); + mServices.report(mServices.words("MasterTookWorld", args), false, &tab); + }); +} + void ALScriptStudioMasters::compareWithFile(Doc& doc, const ALScriptRef& ref) { const std::optional link = mDiskMasters.linkOf(ref); @@ -708,8 +750,8 @@ void ALScriptStudioMasters::giveWayTo(Doc& doc, const ALMasterLink& link) args["[NAME]"] = doc.name; args["[FILE]"] = fileNameOf(link.master); // Saved from here, the world holds what the file does not: the file's - // tab offers to send the file over it, or to compare the two, as a - // change heard in the world does. + // tab offers to send the file over it, to take it into the file, or to + // compare the two, as a change heard in the world does. const ALScriptRef ref = doc.ref; mWindow.closeTab(doc); mWindow.openMasterFile(link.master, link.lua); @@ -720,7 +762,7 @@ void ALScriptStudioMasters::giveWayTo(Doc& doc, const ALMasterLink& link) { tab->master->offerFor = ref; said += " " + mServices.words("MasterLinkDiffers", args); - offers = { "master_send", "master_compare" }; + offers = { "master_send", "master_take_world", "master_compare" }; } mServices.report(said, false, tab, offers); } @@ -827,10 +869,10 @@ void ALScriptStudioMasters::heard(const DiskMasters::Outcome& outcome) said("MasterSendFailed", true, { "master_send" }); return; case What::Skipped: said("MasterSkipped", false); return; - case What::Held: said("MasterHeld", true, { "master_send", "master_compare", "master_unlink" }); return; + case What::Held: said("MasterHeld", true, { "master_send", "master_take_world", "master_compare", "master_unlink" }); return; case What::Differing: args["[WHO]"] = mServices.words(whoSaid(outcome.by)); - said("MasterDiffering", true, { "master_send", "master_compare", "master_unlink" }); + said("MasterDiffering", true, { "master_send", "master_take_world", "master_compare", "master_unlink" }); return; case What::Suspended: said("MasterSuspended", true, { "master_find", "master_unlink" }); return; case What::Orphaned: said("MasterOrphaned", false, { "master_unlink" }); return; diff --git a/indra/newview/alscriptstudiomasters.h b/indra/newview/alscriptstudiomasters.h index b5d72baa92..e53cb1ffa2 100644 --- a/indra/newview/alscriptstudiomasters.h +++ b/indra/newview/alscriptstudiomasters.h @@ -234,6 +234,13 @@ class ALScriptStudioMasters // and its master on disk beside what was typed in the item's. void compareWithWorld(Doc& doc, const ALScriptRef& ref); void compareWithFile(Doc& doc, const ALScriptRef& ref); + // The world's text of a script put in its master's tab, as one step to + // undo, for a save of the file to write. + void takeWorld(Doc& doc, const ALScriptRef& ref); + // The world's text of a script, out of its envelope, given to `then` + // with the master's tab where both are still there; a read that failed + // said on the tab. + void loadWorld(Doc& doc, const ALScriptRef& ref, std::function then); ALScriptStudioServices& mServices; ALScriptStudioAnalysis& mAnalysis; diff --git a/indra/newview/alscriptstudioorphans.cpp b/indra/newview/alscriptstudioorphans.cpp index 2d7868a575..db37da6e4d 100644 --- a/indra/newview/alscriptstudioorphans.cpp +++ b/indra/newview/alscriptstudioorphans.cpp @@ -88,6 +88,7 @@ namespace : action == "make_strict" ? "NoticeMakeStrict" : action == "master_link_hint" ? "NoticeMasterLink" : action == "master_send" ? "NoticeMasterSend" + : action == "master_take_world" ? "NoticeMasterTakeWorld" : action == "master_compare" ? "NoticeCompare" : action == "master_give_way" ? "NoticeMasterGiveWay" : action == "master_compare_file" ? "NoticeMasterCompareFile" diff --git a/indra/newview/skins/default/xui/en/floater_script_studio.xml b/indra/newview/skins/default/xui/en/floater_script_studio.xml index b46dfae4e7..6f37a2b049 100644 --- a/indra/newview/skins/default/xui/en/floater_script_studio.xml +++ b/indra/newview/skins/default/xui/en/floater_script_studio.xml @@ -643,9 +643,9 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is [FILE] is not an LSL file, and [NAME] is LSL. [FILE] is not a notecard's file: a notecard is linked to a .txt, .notecard or .json file. [NAME] carries items, which a file on disk cannot hold, so it cannot be linked to one. - [NAME] is linked to [FILE]: from now on it is changed through the file, and saving the file sends it. + [NAME] is linked to [FILE]: each save of the file sends it. VS Code holds [NAME] too: whichever saves it last is what it holds. - [FILE] is not what [NAME] holds now: the next save of the file sends it, and keeps what [NAME] had in History. + They differ now: the next save sends the file, keeping what [NAME] held in History. [NAME] is no longer linked to [FILE]. Save [NAME] before sending it: what goes up is the file on disk. Sending [NAME] to its script. @@ -673,6 +673,9 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is From [NAME]'s next save, its script runs under [EXPERIENCE]. From [NAME]'s next save, its script runs under no experience. [FILE] could not be read to compare with [NAME]. + [NAME] could not be read in-world: [WHY] + [FILE] now holds what [NAME] holds in-world, not saved yet: undo takes it back. + [FILE] already holds what [NAME] holds in-world. Asking what [OBJECTS] holds... Reading the scripts of [OBJECTS]: [DONE] of [COUNT]... Looking on disk for the file of 1 script... @@ -751,6 +754,8 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is Link this script to the file it names: from then on it is changed through the file Send File Send the file on disk to the script now, keeping what the script holds in History + Update File + Put what the script holds in-world in the file's tab, as one step to undo; saving writes the file Unlink Let the script go from this file; neither is changed Switch to File @@ -765,6 +770,8 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is Link [NAME] to the file it names Send file Send the file on disk to the script now + Update file + Put what the script holds in-world in [NAME]'s tab Compare Compare [NAME] with what the script holds in-world Switch to file @@ -2279,8 +2286,8 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is name="notice_text" text_color="AlertCautionTextColor" top="6" - use_ellipses="true" - width="300" /> + width="300" + word_wrap="true" />