diff --git a/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h b/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h index ded6b2544df8..a1a92a9d97f3 100644 --- a/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionCompletionFinder.h @@ -487,16 +487,10 @@ class GD_CORE_API ExpressionCompletionFinder platform, projectScopedContainers, rootType, node); if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { - if (type == "globalvar") { - const auto* variablesContainer = + if (type == "globalvar" || type == "scenevar") { + const auto* variablesContainer = type == "globalvar" ? projectScopedContainers.GetVariablesContainersList() - .GetTopMostVariablesContainer(); - if (variablesContainer) { - AddCompletionsForVariablesMatchingSearch( - *variablesContainer, node.name, node.nameLocation); - } - } else if (type == "scenevar") { - const auto* variablesContainer = + .GetTopMostVariablesContainer() : projectScopedContainers.GetVariablesContainersList() .GetBottomMostVariablesContainer(); if (variablesContainer) { @@ -519,37 +513,14 @@ class GD_CORE_API ExpressionCompletionFinder } } void OnVisitVariableAccessorNode(VariableAccessorNode& node) override { - // Use this finder or another one to walk back and make the list of - // variables: RootVariable: from `OnVisitVariableNode`: bracket accessor (in - // case of array or structure, take the first children) variable accessor - // Once we have this, walk through the variable. - // if (node.parent) node.parent->Visit(*this); - - const gd::Variable* variable = - gd::ExpressionVariableOwnerFinder::GetVariableUnderNode( + VariableOrVariablesContainer variableOrVariablesContainer = + gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( platform, projectScopedContainers, rootObjectName, node); - if (variable) { - std::cout << "Autocomplete for (variable accessor) " << &variable << std::endl; - if (variable->GetType() == gd::Variable::Structure) { - for (const auto& name : variable->GetAllChildrenNames()) { - const auto& childVariable = variable->GetChild(name); - ExpressionCompletionDescription description( - ExpressionCompletionDescription::Variable, - node.nameLocation.GetStartPosition(), - node.nameLocation.GetEndPosition()); - description.SetCompletion(name); - description.SetVariableType(childVariable.GetType()); - completions.push_back(description); - } - } - // TODO: array - } + AddCompletionsForChildrenVariablesOf(variableOrVariablesContainer, node.nameLocation); } void OnVisitVariableBracketAccessorNode( VariableBracketAccessorNode& node) override { - // TODO - // if (node.parent) node.parent->Visit(*this); } void OnVisitIdentifierNode(IdentifierNode& node) override { const auto& objectsContainersList = @@ -561,23 +532,26 @@ class GD_CORE_API ExpressionCompletionFinder AddCompletionsForObjectMatchingSearch( node.identifierName, type, node.location); } else if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { - if (type == "globalvar") { - const auto* variablesContainer = + if (type == "globalvar" || type == "scenevar") { + const auto* variablesContainer = type == "globalvar" ? projectScopedContainers.GetVariablesContainersList() - .GetTopMostVariablesContainer(); - if (variablesContainer) { - AddCompletionsForVariablesMatchingSearch(*variablesContainer, - node.identifierName, - node.identifierNameLocation); - } - } else if (type == "scenevar") { - const auto* variablesContainer = + .GetTopMostVariablesContainer() : projectScopedContainers.GetVariablesContainersList() .GetBottomMostVariablesContainer(); if (variablesContainer) { - AddCompletionsForVariablesMatchingSearch(*variablesContainer, - node.identifierName, - node.identifierNameLocation); + if (IsCaretOn(node.identifierNameDotLocation) || + IsCaretOn(node.childIdentifierNameLocation)) { + // Complete a potential child variable: + if (variablesContainer->Has(node.identifierName)) { + AddCompletionsForChildrenVariablesOf(&variablesContainer->Get(node.identifierName), + node.childIdentifierNameLocation); + } + } else { + // Complete a root variable of the scene or project: + AddCompletionsForVariablesMatchingSearch(*variablesContainer, + node.identifierName, + node.identifierNameLocation); + } } } else if (type == "objectvar") { auto objectName = gd::ExpressionVariableOwnerFinder::GetObjectName( @@ -586,11 +560,22 @@ class GD_CORE_API ExpressionCompletionFinder rootObjectName, node); - AddCompletionsForObjectOrGroupVariablesMatchingSearch( - objectsContainersList, - objectName, - node.identifierName, - node.identifierNameLocation); + if (IsCaretOn(node.identifierNameDotLocation) || + IsCaretOn(node.childIdentifierNameLocation)) { + // Complete a potential child variable: + const auto* variablesContainer = objectsContainersList.GetObjectOrGroupVariablesContainer(objectName); + if (variablesContainer && variablesContainer->Has(node.identifierName)) { + AddCompletionsForChildrenVariablesOf(&variablesContainer->Get(node.identifierName), + node.childIdentifierNameLocation); + } + } else { + // Complete a root variable of the object: + AddCompletionsForObjectOrGroupVariablesMatchingSearch( + objectsContainersList, + objectName, + node.identifierName, + node.identifierNameLocation); + } } } else { // Object function, behavior name, variable, object variable. @@ -637,27 +622,11 @@ class GD_CORE_API ExpressionCompletionFinder }, [&]() { // This is a variable. - - const gd::Variable* variable = - gd::ExpressionVariableOwnerFinder::GetVariableUnderNode( + VariableOrVariablesContainer variableOrVariablesContainer = + gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( platform, projectScopedContainers, rootObjectName, node); - if (variable) { - std::cout << "Autocomplete for (identifier) " << &variable << std::endl; - if (variable->GetType() == gd::Variable::Structure) { - for (const auto& name : variable->GetAllChildrenNames()) { - const auto& childVariable = variable->GetChild(name); - ExpressionCompletionDescription description( - ExpressionCompletionDescription::Variable, - node.childIdentifierNameLocation.GetStartPosition(), - node.childIdentifierNameLocation.GetEndPosition()); - description.SetCompletion(name); - description.SetVariableType(childVariable.GetType()); - completions.push_back(description); - } - } - // TODO: array - } + AddCompletionsForChildrenVariablesOf(variableOrVariablesContainer, node.childIdentifierNameLocation); }, [&]() { // Ignore properties here. @@ -817,6 +786,36 @@ class GD_CORE_API ExpressionCompletionFinder (inclusive && searchedPosition <= location.GetEndPosition()))); } + void AddCompletionsForChildrenVariablesOf(VariableOrVariablesContainer variableOrVariablesContainer, + const ExpressionParserLocation& location) { + if (variableOrVariablesContainer.variable) { + return AddCompletionsForChildrenVariablesOf(variableOrVariablesContainer.variable, location); + } else if (variableOrVariablesContainer.variablesContainer) { + return AddCompletionsForVariablesMatchingSearch(*variableOrVariablesContainer.variablesContainer, "", location); + } + } + + void AddCompletionsForChildrenVariablesOf(const gd::Variable* variable, + const ExpressionParserLocation& location) { + if (!variable) return; + + if (variable->GetType() == gd::Variable::Structure) { + for (const auto& name : variable->GetAllChildrenNames()) { + const auto& childVariable = variable->GetChild(name); + ExpressionCompletionDescription description( + ExpressionCompletionDescription::Variable, + location.GetStartPosition(), + location.GetEndPosition()); + description.SetCompletion(name); + description.SetVariableType(childVariable.GetType()); + completions.push_back(description); + } + } else { + // TODO: we could do a "comment only completion" to indicate that nothing + // can/should be completed? + } + } + void AddCompletionsForVariablesMatchingSearch( const gd::VariablesContainer& variablesContainer, const gd::String& search, diff --git a/Core/GDCore/IDE/Events/ExpressionVariableOwnerFinder.h b/Core/GDCore/IDE/Events/ExpressionVariableOwnerFinder.h index 56cc867dc745..6e966b243f7d 100644 --- a/Core/GDCore/IDE/Events/ExpressionVariableOwnerFinder.h +++ b/Core/GDCore/IDE/Events/ExpressionVariableOwnerFinder.h @@ -29,17 +29,17 @@ class ExpressionMetadata; namespace gd { -struct ChildVariableName { - gd::String childName; +struct VariableOrVariablesContainer { + const gd::VariablesContainer* variablesContainer; + const gd::Variable* variable; }; -// TODO: rename to ExpressionVariableFinder? - /** * \brief Find the object name that should be used as a context of the - * expression or sub-expression that a given node represents. + * expression or sub-expression that a given node represents, or find + * the last parent of a node representing a variable. * - * This is needed because of the legacy convention where a "objectvar" + * Object name can be needed because of the legacy convention where a "objectvar" * parameter represents a variable of the object represented by the previous * "object" parameter. * @@ -63,7 +63,7 @@ class GD_CORE_API ExpressionVariableOwnerFinder return typeFinder.objectName; } - static const gd::Variable* GetVariableUnderNode( + static VariableOrVariablesContainer GetLastParentOfNode( const gd::Platform& platform, const gd::ProjectScopedContainers& projectScopedContainers, const gd::String& rootObjectName, @@ -71,7 +71,7 @@ class GD_CORE_API ExpressionVariableOwnerFinder gd::ExpressionVariableOwnerFinder typeFinder( platform, projectScopedContainers, rootObjectName); node.Visit(typeFinder); - return typeFinder.variableUnderNode; + return typeFinder.lastParentOfNode; } virtual ~ExpressionVariableOwnerFinder(){}; @@ -83,12 +83,11 @@ class GD_CORE_API ExpressionVariableOwnerFinder const gd::String& rootObjectName_) : platform(platform_), projectScopedContainers(projectScopedContainers_), - rootObjectName(rootObjectName_), - objectName(""), + objectName(rootObjectName_), // If no other object is found, the node + // is linked to the rootObjectName. variableNode(nullptr), thisIsALegacyPrescopedVariable(false), - legacyPrescopedVariablesContainer(nullptr), - variableUnderNode(nullptr) {}; + legacyPrescopedVariablesContainer(nullptr){}; void OnVisitSubExpressionNode(SubExpressionNode& node) override {} void OnVisitOperatorNode(OperatorNode& node) override {} @@ -100,42 +99,22 @@ class GD_CORE_API ExpressionVariableOwnerFinder // This is not possible return; } - if (node.parent == nullptr) { - objectName = rootObjectName; - - // TODO: this represents a identifier/variable node for an object - // if rootObjectName is not null, or otherwise a scene/global variable? - if (objectName.empty()) { - std::cout << "UNEXPECTED USE CASE (variable): " << node.name << std::endl; - std::cout << "rootObjectName empty" << std::endl; - } - // childVariableNames.insert(childVariableNames.begin(), { - // .childName = node.name - // }); - // if - // (projectScopedContainers.GetVariablesContainersList().Has(node.name)) { - // variableUnderNode = - // WalkThroughVariables(projectScopedContainers.GetVariablesContainersList().Get(node.name), - // childVariableNames); - // } - return; - } variableNode = &node; // Check if the parent is a function call, in which we might be dealing // with a legacy pre-scoped variable parameter: - node.parent->Visit(*this); + if (node.parent) node.parent->Visit(*this); // TODO: factor if (thisIsALegacyPrescopedVariable) { // The identifier represents a variable name, and the variables container // containing it was identified in the FunctionCallNode. - childVariableNames.insert(childVariableNames.begin(), - {.childName = node.name}); + childVariableNames.insert(childVariableNames.begin(), node.name); if (legacyPrescopedVariablesContainer) - variableUnderNode = WalkThroughVariables( + lastParentOfNode = WalkUntilLastParent( *legacyPrescopedVariablesContainer, childVariableNames); } else { + // Otherwise, the identifier is to be interpreted as usual: projectScopedContainers.MatchIdentifierWithName( node.name, [&]() { @@ -146,14 +125,14 @@ class GD_CORE_API ExpressionVariableOwnerFinder projectScopedContainers.GetObjectsContainersList() .GetObjectOrGroupVariablesContainer(objectName); if (variablesContainer) - variableUnderNode = - WalkThroughVariables(*variablesContainer, childVariableNames); + lastParentOfNode = + WalkUntilLastParent(*variablesContainer, childVariableNames); }, [&]() { // This is a variable. if (projectScopedContainers.GetVariablesContainersList().Has( node.name)) { - variableUnderNode = WalkThroughVariables( + lastParentOfNode = WalkUntilLastParent( projectScopedContainers.GetVariablesContainersList().Get( node.name), childVariableNames); @@ -171,8 +150,7 @@ class GD_CORE_API ExpressionVariableOwnerFinder } } void OnVisitVariableAccessorNode(VariableAccessorNode& node) override { - childVariableNames.insert(childVariableNames.begin(), - {.childName = node.name}); + childVariableNames.insert(childVariableNames.begin(), node.name); if (node.parent) node.parent->Visit(*this); } void OnVisitIdentifierNode(IdentifierNode& node) override { @@ -180,50 +158,29 @@ class GD_CORE_API ExpressionVariableOwnerFinder // This is not possible return; } - if (node.parent == nullptr) { - objectName = rootObjectName; - - // TODO: this represents a identifier/variable node for an object - // if rootObjectName is not null, or otherwise a scene/global variable? - if (objectName.empty()) { - std::cout << "UNEXPECTED USE CASE (identifier): " << node.identifierName << std::endl; - std::cout << "rootObjectName empty" << std::endl; - } - // childVariableNames.insert(childVariableNames.begin(), { - // .childName = node.identifierName - // }); - // if - // (projectScopedContainers.GetVariablesContainersList().Has(node.identifierName)) - // { - // variableUnderNode = - // WalkThroughVariables(projectScopedContainers.GetVariablesContainersList().Get(node.identifierName), - // childVariableNames); - // } - - return; - } - // This node is not necessarily a variable node. // It will be checked when visiting the FunctionCallNode, just after. variableNode = &node; // Check if the parent is a function call, in which we might be dealing // with a legacy pre-scoped variable parameter: - node.parent->Visit(*this); + if (node.parent) node.parent->Visit(*this); if (thisIsALegacyPrescopedVariable) { // The identifier represents a variable name, and the variables container // containing it was identified in the FunctionCallNode. if (!node.childIdentifierName.empty()) childVariableNames.insert(childVariableNames.begin(), - {.childName = node.childIdentifierName}); + node.childIdentifierName); childVariableNames.insert(childVariableNames.begin(), - {.childName = node.identifierName}); + node.identifierName); if (legacyPrescopedVariablesContainer) - variableUnderNode = WalkThroughVariables( + lastParentOfNode = WalkUntilLastParent( *legacyPrescopedVariablesContainer, childVariableNames); + } else { + // Otherwise, the identifier is to be interpreted as usual: projectScopedContainers.MatchIdentifierWithName( node.identifierName, [&]() { @@ -231,24 +188,24 @@ class GD_CORE_API ExpressionVariableOwnerFinder objectName = node.identifierName; if (!node.childIdentifierName.empty()) childVariableNames.insert(childVariableNames.begin(), - {.childName = node.childIdentifierName}); + node.childIdentifierName); const auto* variablesContainer = projectScopedContainers.GetObjectsContainersList() .GetObjectOrGroupVariablesContainer(objectName); if (variablesContainer) - variableUnderNode = - WalkThroughVariables(*variablesContainer, childVariableNames); + lastParentOfNode = + WalkUntilLastParent(*variablesContainer, childVariableNames); }, [&]() { // This is a variable. if (!node.childIdentifierName.empty()) childVariableNames.insert(childVariableNames.begin(), - {.childName = node.childIdentifierName}); + node.childIdentifierName); if (projectScopedContainers.GetVariablesContainersList().Has( node.identifierName)) { - variableUnderNode = WalkThroughVariables( + lastParentOfNode = WalkUntilLastParent( projectScopedContainers.GetVariablesContainersList().Get( node.identifierName), childVariableNames); @@ -269,7 +226,7 @@ class GD_CORE_API ExpressionVariableOwnerFinder void OnVisitObjectFunctionNameNode(ObjectFunctionNameNode& node) override {} void OnVisitVariableBracketAccessorNode( VariableBracketAccessorNode& node) override { - childVariableNames.insert(childVariableNames.begin(), {.childName = ""}); + childVariableNames.insert(childVariableNames.begin(), ""); if (node.parent) node.parent->Visit(*this); } void OnVisitFunctionCallNode(FunctionCallNode& functionCall) override { @@ -342,24 +299,22 @@ class GD_CORE_API ExpressionVariableOwnerFinder } private: - const gd::Variable* WalkThroughVariables( + static VariableOrVariablesContainer WalkUntilLastParent( const gd::Variable& variable, - const std::vector& childVariableNames, + const std::vector& childVariableNames, size_t startIndex = 0) { const gd::Variable* currentVariable = &variable; - size_t index = startIndex; - std::cout << "walk through from" << &variable << std::endl; - std::cout << "Will walk through: " << std::endl; - for (const auto& name: childVariableNames) { - std::cout << name.childName << std::endl; - } - for (size_t index = startIndex;index < childVariableNames.size() - 1;++index) { - const gd::String& childName = childVariableNames[index].childName; - std::cout << "childName" << childName << std::endl; + + // Walk until size - 1 as we want the last parent. + for (size_t index = startIndex; index + 1 < childVariableNames.size(); + ++index) { + const gd::String& childName = childVariableNames[index]; + if (childName.empty()) { if (currentVariable->GetChildrenCount() == 0) { - // The array or structure is empty, we can't walk through it. - return nullptr; + // The array or structure is empty, we can't walk through it - there + // is no "parent". + return {}; } if (currentVariable->GetType() == gd::Variable::Array) { @@ -370,44 +325,47 @@ class GD_CORE_API ExpressionVariableOwnerFinder } } else { if (!currentVariable->HasChild(childName)) { - // Non existing child. - return nullptr; + // Non existing child - there is no "parent". + return {}; } currentVariable = ¤tVariable->GetChild(childName); } } - return currentVariable; + // Return the last parent of the chain of variables (so not the last variable + // but the one before it). + return {.variable = currentVariable}; } - const gd::Variable* WalkThroughVariables( + static VariableOrVariablesContainer WalkUntilLastParent( const gd::VariablesContainer& variablesContainer, - const std::vector& childVariableNames) { - std::cout << "Will walk through (var container): " << std::endl; - for (const auto& name: childVariableNames) { - std::cout << name.childName << std::endl; - } - - const gd::String& firstChildName = childVariableNames.begin()->childName; + const std::vector& childVariableNames) { + if (childVariableNames.empty()) + return {}; // There is no "parent" to the variables container itself. + if (childVariableNames.size() == 1) + return {// Only one child: the parent is the variables container itself. + .variablesContainer = &variablesContainer}; + + const gd::String& firstChildName = *childVariableNames.begin(); if (!variablesContainer.Has(firstChildName)) { - return nullptr; + // The child does not exist - there is no "parent". + return {}; } - return WalkThroughVariables( + return WalkUntilLastParent( variablesContainer.Get(firstChildName), childVariableNames, 1); } gd::String objectName; gd::ExpressionNode* variableNode; - std::vector childVariableNames; + std::vector childVariableNames; bool thisIsALegacyPrescopedVariable; const gd::VariablesContainer* legacyPrescopedVariablesContainer; - const gd::Variable* variableUnderNode; + VariableOrVariablesContainer lastParentOfNode; const gd::Platform& platform; const gd::ProjectScopedContainers& projectScopedContainers; - const gd::String& rootObjectName; }; } // namespace gd diff --git a/Core/GDCore/Project/ObjectsContainersList.cpp b/Core/GDCore/Project/ObjectsContainersList.cpp index 7a48f116765d..e061d7005e4c 100644 --- a/Core/GDCore/Project/ObjectsContainersList.cpp +++ b/Core/GDCore/Project/ObjectsContainersList.cpp @@ -140,6 +140,7 @@ ObjectsContainersList::GetObjectOrGroupVariablesContainer( return &(*it)->GetObject(objectOrGroupName).GetVariables(); } if ((*it)->GetObjectGroups().Has(objectOrGroupName)) { + // TODO: for completion for groups. // Could be adapted if objects groups have variables in the future. // This would allow handling the renaming of variables of an object group. } diff --git a/Core/tests/ExpressionParser2.cpp b/Core/tests/ExpressionParser2.cpp index c18523697eb2..918b5c82ed11 100644 --- a/Core/tests/ExpressionParser2.cpp +++ b/Core/tests/ExpressionParser2.cpp @@ -2809,8 +2809,8 @@ TEST_CASE("ExpressionParser2", "[common][events]") { // as ExpressionVariableOwnerFinder depends on this parameter type // information. auto node = parser.ParseExpression( - "MyExtension::GetStringWith2ObjectParamAnd2ObjectVarParam(MyObject1, " - "MyVar1, MyObject2, MyVar2)"); + "MyExtension::GetStringWith2ObjectParamAnd2ObjectVarParam(MySpriteObject, " + "MyVariable, MySpriteObject2, MyVariable2)"); REQUIRE(node != nullptr); auto &functionNode = dynamic_cast(*node); auto &identifierObject1Node = @@ -2822,17 +2822,25 @@ TEST_CASE("ExpressionParser2", "[common][events]") { auto &variable2Node = dynamic_cast(*functionNode.parameters[3]); - REQUIRE(identifierObject1Node.identifierName == "MyObject1"); - REQUIRE(identifierObject2Node.identifierName == "MyObject2"); - REQUIRE(variable1Node.identifierName == "MyVar1"); - REQUIRE(variable2Node.identifierName == "MyVar2"); + REQUIRE(identifierObject1Node.identifierName == "MySpriteObject"); + REQUIRE(identifierObject2Node.identifierName == "MySpriteObject2"); + REQUIRE(variable1Node.identifierName == "MyVariable"); + REQUIRE(variable2Node.identifierName == "MyVariable2"); auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers, "", variable1Node); - REQUIRE(variable1ObjectName == "MyObject1"); + REQUIRE(variable1ObjectName == "MySpriteObject"); auto variable2ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers, "", variable2Node); - REQUIRE(variable2ObjectName == "MyObject2"); + REQUIRE(variable2ObjectName == "MySpriteObject2"); + + // Also check the ability to find the last parent of the variables: + auto lastParentOfVariable1Node = gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( + platform, projectScopedContainers, "", variable1Node); + REQUIRE(lastParentOfVariable1Node.variablesContainer == &mySpriteObject.GetVariables()); + auto lastParentOfVariable2Node = gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( + platform, projectScopedContainers, "", variable2Node); + REQUIRE(lastParentOfVariable2Node.variablesContainer == &mySpriteObject2.GetVariables()); gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); @@ -2843,8 +2851,8 @@ TEST_CASE("ExpressionParser2", "[common][events]") { SECTION("Valid function call with 2 object variable from the same object") { { auto node = parser.ParseExpression( - "MyExtension::GetStringWith1ObjectParamAnd2ObjectVarParam(MyObject1, " - "MyVar1, MyVar2)"); + "MyExtension::GetStringWith1ObjectParamAnd2ObjectVarParam(MySpriteObject, " + "MyVariable, MyVariable2)"); REQUIRE(node != nullptr); auto &functionNode = dynamic_cast(*node); auto &identifierObject1Node = @@ -2854,16 +2862,24 @@ TEST_CASE("ExpressionParser2", "[common][events]") { auto &variable2Node = dynamic_cast(*functionNode.parameters[2]); - REQUIRE(identifierObject1Node.identifierName == "MyObject1"); - REQUIRE(variable1Node.identifierName == "MyVar1"); - REQUIRE(variable2Node.identifierName == "MyVar2"); + REQUIRE(identifierObject1Node.identifierName == "MySpriteObject"); + REQUIRE(variable1Node.identifierName == "MyVariable"); + REQUIRE(variable2Node.identifierName == "MyVariable2"); auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers, "", variable1Node); - REQUIRE(variable1ObjectName == "MyObject1"); + REQUIRE(variable1ObjectName == "MySpriteObject"); auto variable2ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers, "", variable2Node); - REQUIRE(variable2ObjectName == "MyObject1"); + REQUIRE(variable2ObjectName == "MySpriteObject"); + + // Also check the ability to find the last parent of the variables: + auto lastParentOfVariable1Node = gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( + platform, projectScopedContainers, "", variable1Node); + REQUIRE(lastParentOfVariable1Node.variablesContainer == &mySpriteObject.GetVariables()); + auto lastParentOfVariable2Node = gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( + platform, projectScopedContainers, "", variable2Node); + REQUIRE(lastParentOfVariable2Node.variablesContainer == &mySpriteObject.GetVariables()); gd::ExpressionValidator validator(platform, projectScopedContainers, "string"); node->Visit(validator); @@ -2874,18 +2890,23 @@ TEST_CASE("ExpressionParser2", "[common][events]") { SECTION("Valid object function call with 1 object variable from the object of the function") { { auto node = parser.ParseExpression( - "MySpriteObject.GetObjectVariableAsNumber(MyVar1)"); + "MySpriteObject.GetObjectVariableAsNumber(MyVariable)"); REQUIRE(node != nullptr); auto &functionNode = dynamic_cast(*node); auto &variable1Node = dynamic_cast(*functionNode.parameters[0]); - REQUIRE(variable1Node.identifierName == "MyVar1"); + REQUIRE(variable1Node.identifierName == "MyVariable"); auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers, "MySpriteObject", variable1Node); REQUIRE(variable1ObjectName == "MySpriteObject"); + // Also check the ability to find the last parent of the variable: + auto lastParentOfVariable1Node = gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( + platform, projectScopedContainers, "MySpriteObject", variable1Node); + REQUIRE(lastParentOfVariable1Node.variablesContainer == &mySpriteObject.GetVariables()); + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); @@ -2895,19 +2916,24 @@ TEST_CASE("ExpressionParser2", "[common][events]") { SECTION("Valid object function call with 1 object variable from the object of the function with a child") { { auto node = parser.ParseExpression( - "MySpriteObject.GetObjectVariableAsNumber(MyVar1.MyChild)"); + "MySpriteObject.GetObjectVariableAsNumber(MyVariable.MyChild)"); REQUIRE(node != nullptr); auto &functionNode = dynamic_cast(*node); auto &variable1Node = dynamic_cast(*functionNode.parameters[0]); - REQUIRE(variable1Node.identifierName == "MyVar1"); + REQUIRE(variable1Node.identifierName == "MyVariable"); REQUIRE(variable1Node.childIdentifierName == "MyChild"); auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName( platform, projectScopedContainers, "MySpriteObject", variable1Node); REQUIRE(variable1ObjectName == "MySpriteObject"); + // Also check the ability to find the last parent of the variable: + auto lastParentOfVariable1Node = gd::ExpressionVariableOwnerFinder::GetLastParentOfNode( + platform, projectScopedContainers, "MySpriteObject", variable1Node); + REQUIRE(lastParentOfVariable1Node.variable == &mySpriteObject.GetVariables().Get("MyVariable")); + gd::ExpressionValidator validator(platform, projectScopedContainers, "number"); node->Visit(validator); REQUIRE(validator.GetFatalErrors().size() == 0); diff --git a/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js b/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js index aa886eed3cd8..7379fb180a75 100644 --- a/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js +++ b/GDevelop.js/__tests__/ExpressionCompletionFinder.spec.js @@ -60,6 +60,398 @@ describe('gd.ExpressionCompletionFinder', function () { return completionDescriptionAsStrings; } + describe('Variable completion tests', () => { + beforeAll(() => { + project = new gd.ProjectHelper.createNewGDJSProject(); + layout = project.insertNewLayout('Scene', 0); + + const object = layout.insertNewObject( + project, + 'Sprite', + 'MySpriteObject', + 0 + ); + + const makeChildrenTestVariables = (structure) => { + structure.castTo('Structure'); + const structureChild1 = structure.getChild('Child1Structure'); + structureChild1.castTo('Structure'); + structureChild1.getChild('Child1StructureChild1'); + structureChild1.getChild('Child1StructureChild2'); + structureChild1.getChild('Child1StructureChild3'); + const structureChild2 = structure.getChild('Child2Array'); + structureChild2.castTo('Array'); + const structureChild3 = structure.getChild('Child3Boolean'); + structureChild3.castTo('Boolean'); + }; + + object + .getVariables() + .insertNew('MyObjectVariableNumber', 0) + .castTo('Number'); + object + .getVariables() + .insertNew('MyObjectVariableString', 1) + .castTo('String'); + makeChildrenTestVariables( + object.getVariables().insertNew('MyObjectVariableStructure', 2) + ); + + layout.getVariables().insertNew('MyVariable', 0); + layout.getVariables().insertNew('MyVariable2', 1); + layout.getVariables().insertNew('UnrelatedVariable3', 2); + makeChildrenTestVariables( + layout.getVariables().insertNew('MyVariableStructure', 3) + ); + + project.getVariables().insertNew('MyGlobalVariable', 0); + project.getVariables().insertNew('MyVariable2', 1); // Will be "shadowed" by the layout variable. + makeChildrenTestVariables( + layout.getVariables().insertNew('MyGlobalVariableStructure', 2) + ); + }); + + afterAll(() => { + project.delete(); + }); + + describe('Object variables completion', () => { + test('Root variables', () => { + expect(testCompletions('number', 'MySpriteObject.|')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyObjectVariableNumber, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyObjectVariableString, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 1, no type, 1, no prefix, no completion, MySpriteObject, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 2, number, 1, no prefix, no completion, MySpriteObject, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect(testCompletions('number', 'MySpriteObject|.')) + .toMatchInlineSnapshot(` + [ + "{ 0, number, 1, no prefix, MySpriteObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }", + ] + `); + }); + + it('Root variables (objectvar parameter)', () => { + expect(testCompletions('number', 'MySpriteObject.Variable(MyObject|')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyObjectVariableNumber, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyObjectVariableString, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Structure, 1 level', () => { + // Completions of children: + expect( + testCompletions( + 'number', + 'MySpriteObject.MyObjectVariableStructure.|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + // Completion of the variable, if we move the cursor back: + + expect( + testCompletions( + 'number', + 'MySpriteObject.MyObjectVariableStructure|.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyObjectVariableNumber, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyObjectVariableString, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect( + testCompletions( + 'number', + 'MySpriteObject|.MyObjectVariableStructure.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 0, number, 1, no prefix, MySpriteObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }", + ] + `); + }); + + test('Structure, 1 level (objectvar parameter)', () => { + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure.|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + + // Completion of the variable, if we move the cursor back: + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure|.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Structure, 2 levels', () => { + expect( + testCompletions( + 'number', + 'MySpriteObject.MyObjectVariableStructure.Child1Structure.|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + // Completion of the previous variables, if we move the cursor back: + expect( + testCompletions( + 'number', + 'MySpriteObject.MyObjectVariableStructure.Child1Structure|.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + + expect( + testCompletions( + 'number', + 'MySpriteObject.MyObjectVariableStructure|.Child1Structure.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyObjectVariableNumber, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyObjectVariableString, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect( + testCompletions( + 'number', + 'MySpriteObject|.MyObjectVariableStructure.Child1Structure.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 0, number, 1, no prefix, MySpriteObject, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, with object configuration }", + ] + `); + }); + + test('Structure, 2 levels (objectvar parameter)', () => { + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure.Child1Structure.|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure.Child1Structure|.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect( + testCompletions( + 'number', + 'MySpriteObject.Variable(MyObjectVariableStructure|.Child1Structure.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyObjectVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + }); + + describe('Scene variables completion', () => { + test('Root variables', () => { + expect(testCompletions('number', 'MyVariab|')).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyVariable2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 2, number, 1, MyVariab, no completion, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Root variables (scenevar parameter)', () => { + expect(testCompletions('number', 'Variable(MyVaria|)')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyVariable2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Structure, 1 level', () => { + expect(testCompletions('number', 'MyVariableStructure.|')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + // Completion of the previous variables, if we move the cursor back: + expect(testCompletions('number', 'MyVariableStructure|.')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Structure, 1 level (scenevar parameter)', () => { + expect(testCompletions('number', 'Variable(MyVariableStructure.|')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + // Completion of the previous variables, if we move the cursor back: + expect(testCompletions('number', 'Variable(MyVariableStructure|.')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Structure, 2 levels', () => { + expect( + testCompletions('number', 'MyVariableStructure.Child1Structure.|') + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + // Completion of the previous variables, if we move the cursor back: + expect( + testCompletions('number', 'MyVariableStructure.Child1Structure|.') + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect( + testCompletions('number', 'MyVariableStructure|.Child1Structure.') + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Structure, 2 levels (scenevar parameter)', () => { + expect( + testCompletions( + 'number', + 'Variable(MyVariableStructure.Child1Structure.|' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, Child1StructureChild1, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child1StructureChild3, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + // Completion of the previous variables, if we move the cursor back: + expect( + testCompletions( + 'number', + 'Variable(MyVariableStructure.Child1Structure|.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, Child1Structure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child2Array, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, Child3Boolean, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + expect( + testCompletions( + 'number', + 'Variable(MyVariableStructure|.Child1Structure.' + ) + ).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + }); + + describe('Global variables completion', () => { + test('Root variables', () => { + expect(testCompletions('number', 'MyGlo|')).toMatchInlineSnapshot(` + [ + "{ 3, no type, 3, no prefix, MyGlobalVariableStructure, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyGlobalVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 2, number, 1, MyGlo, no completion, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + + test('Root variables (globalvar parameter)', () => { + expect(testCompletions('number', 'GlobalVariable(My|)')) + .toMatchInlineSnapshot(` + [ + "{ 3, no type, 1, no prefix, MyGlobalVariable, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + "{ 3, no type, 1, no prefix, MyVariable2, no object name, no behavior name, non-exact, not last parameter, no parameter metadata, no object configuration }", + ] + `); + }); + }); + }); + describe('Various tests', () => { beforeAll(() => { project = new gd.ProjectHelper.createNewGDJSProject();