From 6599a1cbd513d16eca9c1a38e128077ef914dd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 26 Apr 2026 15:44:51 +0200 Subject: [PATCH 01/10] Handle syntax coloring of expressions (search is broken) --- .../Events/ExpressionSyntaxColoringHelper.h | 311 ++++++++++++++++++ GDevelop.js/Bindings/Bindings.idl | 31 ++ GDevelop.js/Bindings/Wrapper.cpp | 6 + GDevelop.js/scripts/generate-types.js | 18 + GDevelop.js/types.d.ts | 24 ++ ...ioncolorationdescription_colorationkind.js | 2 + .../gdexpressioncolorationdescription.js | 13 + .../types/gdexpressionsyntaxcoloringhelper.js | 7 + ...gdvectorexpressioncolorationdescription.js | 7 + GDevelop.js/types/libgdevelop.js | 4 + .../src/EventsSheet/EventsTree/Instruction.js | 7 +- .../ParameterFields/DefaultField.js | 102 +++++- .../ParameterInlineRenderer.flow.js | 1 + 13 files changed, 529 insertions(+), 4 deletions(-) create mode 100644 Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h create mode 100644 GDevelop.js/types/expressioncolorationdescription_colorationkind.js create mode 100644 GDevelop.js/types/gdexpressioncolorationdescription.js create mode 100644 GDevelop.js/types/gdexpressionsyntaxcoloringhelper.js create mode 100644 GDevelop.js/types/gdvectorexpressioncolorationdescription.js diff --git a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h new file mode 100644 index 000000000000..7c9324fa0244 --- /dev/null +++ b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h @@ -0,0 +1,311 @@ +/* + * GDevelop Core + * Copyright 2008-present Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +#include +#include + +#include "GDCore/Events/Parsers/ExpressionParser2.h" +#include "GDCore/Events/Parsers/ExpressionParser2Node.h" +#include "GDCore/Events/Parsers/ExpressionParser2NodePrinter.h" +#include "GDCore/Events/Parsers/ExpressionParser2NodeWorker.h" +#include "GDCore/Events/Parsers/GrammarTerminals.h" +#include "GDCore/Extensions/Metadata/ExpressionMetadata.h" +#include "GDCore/Extensions/Metadata/InstructionMetadata.h" +#include "GDCore/Extensions/Metadata/ValueTypeMetadata.h" +#include "GDCore/IDE/Events/ExpressionTypeFinder.h" +#include "GDCore/IDE/Events/ExpressionVariableOwnerFinder.h" +#include "GDCore/IDE/Events/ExpressionVariablePathFinder.h" +#include "GDCore/Project/ProjectScopedContainers.h" +#include "GDCore/Project/Variable.h" + +namespace gd { +class Expression; +class ObjectsContainer; +class Platform; +class ParameterMetadata; +class ExpressionMetadata; +class ObjectConfiguration; +} // namespace gd + +namespace gd { + +/** + * \brief Describe colorations to be shown to the user. + */ +struct GD_CORE_API ExpressionColorationDescription { +public: + /** + * The different kind of colorations that can be described. + * The IDE is responsible for actually *searching* and showing the colorations + * of colorations with a kind "WithPrefix": these colorations are only + * describing what must be listed. + */ + enum ColorationKind { String, Number, Object, Variable, Operator }; + + /** \brief Return the kind of the coloration */ + ColorationKind GetColorationKind() const { return colorationKind; } + + /** + * \brief Return the first character index of the autocompleted part. + */ + size_t GetStartPosition() const { return startPosition; } + + /** + * \brief Return the first character index after the autocompleted part. + */ + size_t GetEndPosition() const { return endPosition; } + + /** Default constructor, only to be used by Emscripten bindings. */ + ExpressionColorationDescription() : colorationKind(String){}; + + ExpressionColorationDescription(ColorationKind colorationKind_, + size_t replacementStartPosition_, + size_t replacementEndPosition_) + : colorationKind(colorationKind_), + startPosition(replacementStartPosition_), + endPosition(replacementEndPosition_) {} + +private: + ColorationKind colorationKind; + size_t startPosition = 0; + size_t endPosition = 0; +}; + +/** + * \brief Returns the list of coloration descriptions for an expression node. + * + * \see gd::ExpressionColorationDescription + */ +class GD_CORE_API ExpressionSyntaxColoringHelper + : public ExpressionParser2NodeWorker { +public: + /** + * \brief Given the expression, find the node at the specified location + * and returns colorations for it. + */ + static std::vector + GetColorationDescriptionsFor( + const gd::Platform &platform, + const gd::ProjectScopedContainers &projectScopedContainers, + const gd::String &rootType, gd::ExpressionNode &node) { + gd::ExpressionSyntaxColoringHelper colorationHelper( + platform, projectScopedContainers, rootType); + node.Visit(colorationHelper); + return colorationHelper.GetColorationDescriptions(); + } + + /** + * \brief Return the colorations found for the visited node. + */ + const std::vector & + GetColorationDescriptions() { + return colorations; + }; + + virtual ~ExpressionSyntaxColoringHelper(){}; + +protected: + void OnVisitSubExpressionNode(SubExpressionNode &node) override { + // TODO node.location should includes the parenthesis. + { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Operator, + node.location.GetStartPosition() - 1, + node.expression->location.GetStartPosition()); + colorations.push_back(coloration); + } + node.expression->Visit(*this); + { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Operator, + node.expression->location.GetEndPosition(), + node.location.GetEndPosition() + 1); + colorations.push_back(coloration); + } + } + void OnVisitOperatorNode(OperatorNode &node) override { + node.leftHandSide->Visit(*this); + + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Operator, + node.leftHandSide->location.GetEndPosition(), + node.rightHandSide->location.GetStartPosition()); + colorations.push_back(coloration); + + node.rightHandSide->Visit(*this); + } + void OnVisitUnaryOperatorNode(UnaryOperatorNode &node) override { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Operator, + node.location.GetStartPosition(), + node.factor->location.GetStartPosition()); + colorations.push_back(coloration); + + node.factor->Visit(*this); + } + void OnVisitNumberNode(NumberNode &node) override { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Number, + node.location.GetStartPosition(), node.location.GetEndPosition()); + colorations.push_back(coloration); + } + + void OnVisitTextNode(TextNode &node) override { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::String, + node.location.GetStartPosition(), node.location.GetEndPosition()); + colorations.push_back(coloration); + } + void OnVisitVariableNode(VariableNode &node) override { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.nameLocation.GetStartPosition(), + node.nameLocation.GetEndPosition()); + colorations.push_back(coloration); + + if (node.child) { + node.child->Visit(*this); + } + } + void OnVisitVariableAccessorNode(VariableAccessorNode &node) override { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.dotLocation.GetStartPosition(), + node.nameLocation.GetEndPosition()); + colorations.push_back(coloration); + + if (node.child) { + node.child->Visit(*this); + } + } + void OnVisitVariableBracketAccessorNode( + VariableBracketAccessorNode &node) override { + { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.location.GetStartPosition(), + node.expression->location.GetStartPosition()); + colorations.push_back(coloration); + } + node.expression->Visit(*this); + { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.expression->location.GetEndPosition(), + node.child ? node.child->location.GetStartPosition() + : node.location.GetEndPosition()); + colorations.push_back(coloration); + } + if (node.child) { + node.child->Visit(*this); + } + } + void OnVisitIdentifierNode(IdentifierNode &node) override { + const auto &objectsContainersList = + projectScopedContainers.GetObjectsContainersList(); + auto type = gd::ExpressionTypeFinder::GetType( + platform, projectScopedContainers, rootType, node); + if (gd::ParameterMetadata::IsObject(type)) { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Object, + node.identifierNameLocation.GetStartPosition(), + node.identifierNameLocation.GetStartPosition()); + colorations.push_back(coloration); + } else if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.location.GetStartPosition(), node.location.GetStartPosition()); + colorations.push_back(coloration); + } else { + // Might be: + // - An object variable, object behavior or object expression. + // - Or a variable with a child. + projectScopedContainers.MatchIdentifierWithName( + node.identifierName, + [&]() { + // This is an object. + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Object, + node.identifierNameLocation.GetStartPosition(), + node.identifierNameLocation.GetEndPosition()); + colorations.push_back(coloration); + + if (node.childIdentifierNameLocation.IsValid()) { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.childIdentifierNameLocation.GetStartPosition(), + node.childIdentifierNameLocation.GetEndPosition()); + colorations.push_back(coloration); + } + }, + [&]() { + // This is a variable. + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.location.GetStartPosition(), + node.location.GetEndPosition()); + colorations.push_back(coloration); + }, + [&]() { + // This is a property. + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.location.GetStartPosition(), + node.location.GetEndPosition()); + colorations.push_back(coloration); + }, + [&]() { + // This is a parameter. + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Variable, + node.location.GetStartPosition(), + node.location.GetEndPosition()); + colorations.push_back(coloration); + }, + [&]() { + // Ignore unrecognised identifiers here. + }); + } + } + void OnVisitObjectFunctionNameNode(ObjectFunctionNameNode &node) override { + // No coloration + } + void OnVisitFunctionCallNode(FunctionCallNode &node) override { + if (node.objectNameLocation.IsValid()) { + gd::ExpressionColorationDescription coloration( + gd::ExpressionColorationDescription::ColorationKind::Object, + node.objectNameLocation.GetStartPosition(), + node.objectNameLocation.GetEndPosition()); + colorations.push_back(coloration); + } + for (auto &¶meter : node.parameters) { + parameter->Visit(*this); + } + } + void OnVisitEmptyNode(EmptyNode &node) override {} + +private: + ExpressionSyntaxColoringHelper( + const gd::Platform &platform_, + const gd::ProjectScopedContainers &projectScopedContainers_, + const gd::String &rootType_) + : platform(platform_), projectScopedContainers(projectScopedContainers_), + rootType(rootType_), + rootObjectName("") // Always empty, might be changed if variable fields + // in the editor are changed to use coloration. + {}; + + std::vector colorations; + size_t searchedPosition; + + const gd::Platform &platform; + const gd::ProjectScopedContainers &projectScopedContainers; + const gd::String rootType; + const gd::String rootObjectName; +}; + +} // namespace gd diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 319db755359a..e69e6647dab3 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -3245,6 +3245,37 @@ interface ExpressionCompletionFinder { //Inherited from ExpressionParser2NodeWorker: }; +enum ExpressionColorationDescription_ColorationKind { + "ExpressionColorationDescription::String", + "ExpressionColorationDescription::Number", + "ExpressionColorationDescription::Object", + "ExpressionColorationDescription::Variable", + "ExpressionColorationDescription::Operator", +}; + +interface ExpressionColorationDescription { + ExpressionColorationDescription_ColorationKind GetColorationKind(); + unsigned long GetStartPosition(); + unsigned long GetEndPosition(); +}; + +interface VectorExpressionColorationDescription { + unsigned long size(); + [Value] ExpressionColorationDescription at(unsigned long index); +}; + +interface ExpressionSyntaxColoringHelper { + [Value] VectorExpressionColorationDescription STATIC_GetColorationDescriptionsFor( + [Const, Ref] Platform platform, [Const, Ref] + ProjectScopedContainers projectScopedContainers, + [Const] DOMString rootType, + [Ref] ExpressionNode node); + + [Const, Ref] VectorExpressionColorationDescription GetColorationDescriptions(); + + //Inherited from ExpressionParser2NodeWorker: +}; + interface ExpressionNodeLocationFinder { ExpressionNode STATIC_GetNodeAtPosition([Ref] ExpressionNode node, unsigned long searchedPosition); }; diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index 4f3802aec1e1..a3cb97500bee 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -512,6 +513,10 @@ typedef ExpressionCompletionDescription::CompletionKind ExpressionCompletionDescription_CompletionKind; typedef std::vector VectorExpressionCompletionDescription; +typedef ExpressionColorationDescription::ColorationKind + ExpressionColorationDescription_ColorationKind; +typedef std::vector + VectorExpressionColorationDescription; typedef std::map> MapExtensionProperties; typedef gd::Variable::Type Variable_Type; @@ -893,6 +898,7 @@ typedef std::vector VectorPropertyDescriptorChoice IsExtensionLifecycleEventsFunction #define STATIC_GetCompletionDescriptionsFor GetCompletionDescriptionsFor +#define STATIC_GetColorationDescriptionsFor GetColorationDescriptionsFor #define STATIC_GetType GetType #define STATIC_GetNodeAtPosition GetNodeAtPosition diff --git a/GDevelop.js/scripts/generate-types.js b/GDevelop.js/scripts/generate-types.js index 86770840ca55..6a67ecbd32f4 100644 --- a/GDevelop.js/scripts/generate-types.js +++ b/GDevelop.js/scripts/generate-types.js @@ -229,6 +229,24 @@ type ExpressionCompletionDescription_CompletionKind = 0 | 1 | 2 | 3 | 4 | 5 | 6` ].join('\n'), 'types/gdexpressioncompletiondescription.js' ); + fs.writeFileSync( + 'types/expressioncolorationdescription_colorationkind.js', + `// Automatically generated by GDevelop.js/scripts/generate-types.js +type ExpressionColorationDescription_ColorationKind = 0 | 1 | 2 | 3 | 4 | 5 | 6` + ); + shell.sed( + '-i', + 'declare class gdExpressionColorationDescription {', + [ + 'declare class gdExpressionColorationDescription {', + ' static String: 0;', + ' static Number: 1;', + ' static Object: 2;', + ' static Variable: 3;', + ' static Operator: 4;', + ].join('\n'), + 'types/gdexpressioncolorationdescription.js' + ); fs.writeFileSync( 'types/expressionparsererror_errortype.js', `// Automatically generated by GDevelop.js/scripts/generate-types.js diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index b941c9f28bf8..3360dc30e509 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -112,6 +112,14 @@ export enum ExpressionCompletionDescription_CompletionKind { Parameter = 6, } +export enum ExpressionColorationDescription_ColorationKind { + String = 0, + Number = 1, + Object = 2, + Variable = 3, + Operator = 4, +} + export enum EventsFunction_FunctionType { Action = 0, Condition = 1, @@ -2337,6 +2345,22 @@ export class ExpressionCompletionFinder extends EmscriptenObject { getCompletionDescriptions(): VectorExpressionCompletionDescription; } +export class ExpressionColorationDescription extends EmscriptenObject { + getColorationKind(): ExpressionColorationDescription_ColorationKind; + getStartPosition(): number; + getEndPosition(): number; +} + +export class VectorExpressionColorationDescription extends EmscriptenObject { + size(): number; + at(index: number): ExpressionColorationDescription; +} + +export class ExpressionSyntaxColoringHelper extends EmscriptenObject { + static getColorationDescriptionsFor(platform: Platform, projectScopedContainers: ProjectScopedContainers, rootType: string, node: ExpressionNode): VectorExpressionColorationDescription; + getColorationDescriptions(): VectorExpressionColorationDescription; +} + export class ExpressionNodeLocationFinder extends EmscriptenObject { static getNodeAtPosition(node: ExpressionNode, searchedPosition: number): ExpressionNode; } diff --git a/GDevelop.js/types/expressioncolorationdescription_colorationkind.js b/GDevelop.js/types/expressioncolorationdescription_colorationkind.js new file mode 100644 index 000000000000..2841b8a2c5f4 --- /dev/null +++ b/GDevelop.js/types/expressioncolorationdescription_colorationkind.js @@ -0,0 +1,2 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +type ExpressionColorationDescription_ColorationKind = 0 | 1 | 2 | 3 | 4 | 5 | 6 \ No newline at end of file diff --git a/GDevelop.js/types/gdexpressioncolorationdescription.js b/GDevelop.js/types/gdexpressioncolorationdescription.js new file mode 100644 index 000000000000..45a327be016e --- /dev/null +++ b/GDevelop.js/types/gdexpressioncolorationdescription.js @@ -0,0 +1,13 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdExpressionColorationDescription { + static String: 0; + static Number: 1; + static Object: 2; + static Variable: 3; + static Operator: 4; + getColorationKind(): ExpressionColorationDescription_ColorationKind; + getStartPosition(): number; + getEndPosition(): number; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdexpressionsyntaxcoloringhelper.js b/GDevelop.js/types/gdexpressionsyntaxcoloringhelper.js new file mode 100644 index 000000000000..65fa66f2cda3 --- /dev/null +++ b/GDevelop.js/types/gdexpressionsyntaxcoloringhelper.js @@ -0,0 +1,7 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdExpressionSyntaxColoringHelper { + static getColorationDescriptionsFor(platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, rootType: string, node: gdExpressionNode): gdVectorExpressionColorationDescription; + getColorationDescriptions(): gdVectorExpressionColorationDescription; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdvectorexpressioncolorationdescription.js b/GDevelop.js/types/gdvectorexpressioncolorationdescription.js new file mode 100644 index 000000000000..e2b2c48edb50 --- /dev/null +++ b/GDevelop.js/types/gdvectorexpressioncolorationdescription.js @@ -0,0 +1,7 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdVectorExpressionColorationDescription { + size(): number; + at(index: number): gdExpressionColorationDescription; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js index f29308178010..98ffce48e956 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -236,6 +236,10 @@ declare class libGDevelop { ExpressionCompletionDescription: Class; VectorExpressionCompletionDescription: Class; ExpressionCompletionFinder: Class; + ExpressionColorationDescription_ColorationKind: Class; + ExpressionColorationDescription: Class; + VectorExpressionColorationDescription: Class; + ExpressionSyntaxColoringHelper: Class; ExpressionNodeLocationFinder: Class; ExpressionTypeFinder: Class; ExpressionNode: Class; diff --git a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js index 2e7a3c51b1ea..3354f416c21b 100644 --- a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js +++ b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js @@ -378,9 +378,11 @@ const Instruction = (props: Props): React.Node => { key={i} className={classNames({ [selectableArea]: true, - [instructionParameter]: true, + [instructionParameter]: + parameterType !== 'number' && parameterType !== 'string', // $FlowFixMe[invalid-computed-prop] - [parameterType]: true, + [parameterType]: + parameterType !== 'number' && parameterType !== 'string', })} onClick={domEvent => { props.onParameterClick(domEvent, parameterIndex); @@ -403,6 +405,7 @@ const Instruction = (props: Props): React.Node => { {ParameterRenderingService.renderInlineParameter({ scope, value: formattedValue, + expression: instruction.getParameter(parameterIndex), expressionIsValid, hasDeprecationWarning, parameterMetadata, diff --git a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js index 959628840e36..fda84b04692a 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js @@ -10,6 +10,11 @@ import { } from './ParameterFieldCommons'; import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow'; import { highlightSearchText } from '../../Utils/HighlightSearchText'; +import { mapVector } from '../../Utils/MapFor'; +import classNames from 'classnames'; +import { instructionParameter } from '../EventsTree/ClassNames'; + +const gd: libGDevelop = global.gd; export default (React.forwardRef( function DefaultField(props: ParameterFieldProps, ref) { @@ -46,6 +51,86 @@ export default (React.forwardRef( +ref?: React.RefSetter, }>); +const getColorationName = ( + colorationKind: ExpressionColorationDescription_ColorationKind +) => { + switch (colorationKind) { + case gd.ExpressionColorationDescription.Number: + return 'number'; + + case gd.ExpressionColorationDescription.Object: + return 'object'; + + case gd.ExpressionColorationDescription.Variable: + return 'variable'; + + case gd.ExpressionColorationDescription.Operator: + return 'operator'; + + case gd.ExpressionColorationDescription.String: + default: + return 'string'; + } +}; + +export const applySyntaxColoring = ({ + text, + platform, + projectScopedContainers, + rootType, + expression, +}: { + text: string, + platform: gdPlatform, + projectScopedContainers: gdProjectScopedContainers, + rootType: string, + expression: gdExpression, +}): React.Node => { + const colorationDescriptions = gd.ExpressionSyntaxColoringHelper.getColorationDescriptionsFor( + platform, + projectScopedContainers, + rootType, + expression.getRootNode() + ); + let nextCharacterIndex = 0; + const coloredTextParts = []; + let partIndex = 0; + mapVector(colorationDescriptions, colorationDescription => { + const startPosition = colorationDescription.getStartPosition(); + if (startPosition > nextCharacterIndex) { + coloredTextParts.push( + + {text.substring(nextCharacterIndex, startPosition)} + + ); + partIndex++; + nextCharacterIndex = startPosition; + } + const endPosition = colorationDescription.getEndPosition(); + coloredTextParts.push( + + {text.substring(nextCharacterIndex, endPosition)} + + ); + partIndex++; + nextCharacterIndex = endPosition; + }); + if (nextCharacterIndex < text.length) { + coloredTextParts.push( + + {text.substring(nextCharacterIndex)} + + ); + } + return coloredTextParts; +}; + export const renderInlineDefaultField = ({ value, expressionIsValid, @@ -56,6 +141,9 @@ export const renderInlineDefaultField = ({ MissingParameterValue, highlightedSearchText, highlightedSearchMatchCase, + scope, + projectScopedContainersAccessor, + expression, }: ParameterInlineRendererProps): string | React.Node => { if (!value && !parameterMetadata.isOptional()) { return ; @@ -78,7 +166,17 @@ export const renderInlineDefaultField = ({ ); } - return highlightSearchText(value, highlightedSearchText, { - matchCase: highlightedSearchMatchCase, + return applySyntaxColoring({ + text: value, + expression, + rootType: parameterMetadata.getValueTypeMetadata().getName(), + platform: scope.project.getCurrentPlatform(), + projectScopedContainers: projectScopedContainersAccessor.get(), }); + + // TODO Merge coloration and search highlighting. + + // return highlightSearchText(value, highlightedSearchText, { + // matchCase: highlightedSearchMatchCase, + // }); }; diff --git a/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js b/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js index 36c4860a641d..4d9778ca07d9 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/ParameterInlineRenderer.flow.js @@ -20,6 +20,7 @@ export type ParameterInlineRendererProps = {| projectScopedContainersAccessor: ProjectScopedContainersAccessor, parameterMetadata: gdParameterMetadata, value: string, + expression: gdExpression, expressionIsValid: boolean, hasDeprecationWarning: boolean, renderObjectThumbnail: string => React.Node, From 1713c353f160b11e43fa6392734bfae9a5dd6055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Mon, 27 Apr 2026 00:21:56 +0200 Subject: [PATCH 02/10] Fix the search --- .../ParameterFields/DefaultField.js | 84 +++++----- newIDE/app/src/Utils/HighlightSearchText.js | 147 +++++++++++++++--- 2 files changed, 172 insertions(+), 59 deletions(-) diff --git a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js index fda84b04692a..5cee12f2e199 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js @@ -9,7 +9,13 @@ import { type FieldFocusFunction, } from './ParameterFieldCommons'; import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow'; -import { highlightSearchText } from '../../Utils/HighlightSearchText'; +import { + highlightSearchText, + renderStylizedText, + type TextStyle, + mergeStylizedText, + getHighlightSearchTextParts, +} from '../../Utils/HighlightSearchText'; import { mapVector } from '../../Utils/MapFor'; import classNames from 'classnames'; import { instructionParameter } from '../EventsTree/ClassNames'; @@ -85,7 +91,7 @@ export const applySyntaxColoring = ({ projectScopedContainers: gdProjectScopedContainers, rootType: string, expression: gdExpression, -}): React.Node => { +}): Array => { const colorationDescriptions = gd.ExpressionSyntaxColoringHelper.getColorationDescriptionsFor( platform, projectScopedContainers, @@ -93,40 +99,40 @@ export const applySyntaxColoring = ({ expression.getRootNode() ); let nextCharacterIndex = 0; - const coloredTextParts = []; - let partIndex = 0; + const coloredTextParts: Array = []; mapVector(colorationDescriptions, colorationDescription => { const startPosition = colorationDescription.getStartPosition(); if (startPosition > nextCharacterIndex) { - coloredTextParts.push( - - {text.substring(nextCharacterIndex, startPosition)} - - ); - partIndex++; + coloredTextParts.push({ + startIndex: nextCharacterIndex, + endIndex: startPosition, + props: {}, + key: `color-part--${coloredTextParts.length}`, + }); nextCharacterIndex = startPosition; } const endPosition = colorationDescription.getEndPosition(); - coloredTextParts.push( - - {text.substring(nextCharacterIndex, endPosition)} - - ); - partIndex++; + }), + }, + key: `color-part--${coloredTextParts.length}`, + }); nextCharacterIndex = endPosition; }); if (nextCharacterIndex < text.length) { - coloredTextParts.push( - - {text.substring(nextCharacterIndex)} - - ); + coloredTextParts.push({ + startIndex: nextCharacterIndex, + endIndex: text.length, + props: {}, + key: `color-part--${coloredTextParts.length}`, + }); } return coloredTextParts; }; @@ -166,17 +172,19 @@ export const renderInlineDefaultField = ({ ); } - return applySyntaxColoring({ - text: value, - expression, - rootType: parameterMetadata.getValueTypeMetadata().getName(), - platform: scope.project.getCurrentPlatform(), - projectScopedContainers: projectScopedContainersAccessor.get(), - }); - - // TODO Merge coloration and search highlighting. - - // return highlightSearchText(value, highlightedSearchText, { - // matchCase: highlightedSearchMatchCase, - // }); + return renderStylizedText( + value, + mergeStylizedText( + getHighlightSearchTextParts(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + }), + applySyntaxColoring({ + text: value, + expression, + rootType: parameterMetadata.getValueTypeMetadata().getName(), + platform: scope.project.getCurrentPlatform(), + projectScopedContainers: projectScopedContainersAccessor.get(), + }) + ) + ); }; diff --git a/newIDE/app/src/Utils/HighlightSearchText.js b/newIDE/app/src/Utils/HighlightSearchText.js index f650f9ae2026..1293d7331d0d 100644 --- a/newIDE/app/src/Utils/HighlightSearchText.js +++ b/newIDE/app/src/Utils/HighlightSearchText.js @@ -16,6 +16,125 @@ type HighlightSpanProps = {| matchCase?: boolean, |}; +export type TextStyle = { + startIndex: number, + endIndex: number, + props: { + className?: string, + style?: { +[key: string]: string | number }, + }, + key: string, + children?: Array, +}; + +export const getHighlightSearchTextParts = ( + text: string, + searchText: ?string, + spanProps?: HighlightSpanProps +): Array => { + const query = searchText ? searchText.trim() : ''; + if (!query) return []; + + const matchCase = spanProps?.matchCase ?? false; + const flags = matchCase ? 'g' : 'gi'; + const regex = new RegExp(`(${escapeRegExpForSearch(query)})`, flags); + + const { matchCase: _matchCase, ...spanPropsForDom } = spanProps || {}; + const props: { + className?: string, + style?: { +[key: string]: string | number }, + } = + spanProps && (spanProps.className != null || spanProps.style != null) + ? spanPropsForDom + : { className: GLOBAL_SEARCH_MATCH_CLASS_NAME }; + + let nextCharacterIndex = 0; + const parts: Array = []; + for (const match of text.matchAll(regex)) { + if (nextCharacterIndex < match.index) { + parts.push({ + startIndex: nextCharacterIndex, + endIndex: match.index, + props: {}, + key: `${query}-${parts.length}`, + }); + } + const endIndex = match.index + match[0].length; + parts.push({ + startIndex: match.index, + endIndex, + props, + key: `${query}-${parts.length}`, + }); + nextCharacterIndex = endIndex; + } + if (nextCharacterIndex < text.length) { + parts.push({ + startIndex: nextCharacterIndex, + endIndex: text.length, + props: {}, + key: `${query}-${parts.length}`, + }); + } + return parts; +}; + +export const renderStylizedText = ( + text: string, + parts: Array +): React.Node => { + if (parts.length === 0) { + return text; + } + return parts.map(({ startIndex, endIndex, props, key, children }, index) => ( + + {children + ? renderStylizedText(text, children) + : text.substring(startIndex, endIndex)} + + )); +}; + +export const mergeStylizedText = ( + parts: Array, + subParts: Array +): Array => { + if (parts.length === 0) { + return subParts; + } + if (subParts.length === 0) { + return parts; + } + const mergedParts: Array = []; + let subPartsIndex = 0; + for (const part of parts) { + if (part.children) { + throw new Error('Unimplemented'); + } + const children = []; + do { + const subPart = subParts[subPartsIndex]; + children.push({ + ...subPart, + startIndex: Math.max(subPart.startIndex, part.startIndex), + endIndex: Math.min(subPart.endIndex, part.endIndex), + }); + subPartsIndex++; + } while ( + subPartsIndex < subParts.length && + subParts[subPartsIndex].startIndex < part.endIndex + ); + if ( + subPartsIndex === subParts.length || + subParts[subPartsIndex].startIndex > part.endIndex + ) { + subPartsIndex--; + } + mergedParts.push({ ...part, children }); + } + return mergedParts; +}; + /** * Splits text by the search query and wraps each match in a span for highlighting. * Uses the same case-sensitivity as C++ EventsRefactorer::SearchInEvents: @@ -31,26 +150,12 @@ export const highlightSearchText = ( searchText: ?string, spanProps?: HighlightSpanProps ): React.Node => { - const query = searchText ? searchText.trim() : ''; - if (!query) return text; - - const matchCase = spanProps?.matchCase ?? false; - const flags = matchCase ? 'g' : 'gi'; - const regex = new RegExp(`(${escapeRegExpForSearch(query)})`, flags); - const parts = text.split(regex); - const { matchCase: _matchCase, ...spanPropsForDom } = spanProps || {}; - const props = - spanProps && (spanProps.className != null || spanProps.style != null) - ? spanPropsForDom - : { className: GLOBAL_SEARCH_MATCH_CLASS_NAME }; - - return parts.map((part, index) => - index % 2 === 1 ? ( - - {part} - - ) : ( - {part} - ) + const parts = getHighlightSearchTextParts(text, searchText, spanProps); + if (parts.length === 0) { + return text; + } + return renderStylizedText( + text, + getHighlightSearchTextParts(text, searchText, spanProps) ); }; From ffe8c12a706b5377cddf4e4ed6976b875c767164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 1 May 2026 16:35:23 +0200 Subject: [PATCH 03/10] Merge consecutive parts with the same color --- .../Events/ExpressionSyntaxColoringHelper.h | 115 +++++++++--------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h index 7c9324fa0244..c827c05835c4 100644 --- a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h +++ b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h @@ -59,15 +59,17 @@ struct GD_CORE_API ExpressionColorationDescription { */ size_t GetEndPosition() const { return endPosition; } + void SetEndPosition(size_t endPosition_) { endPosition = endPosition_; } + /** Default constructor, only to be used by Emscripten bindings. */ ExpressionColorationDescription() : colorationKind(String){}; ExpressionColorationDescription(ColorationKind colorationKind_, - size_t replacementStartPosition_, - size_t replacementEndPosition_) + size_t startPosition_, + size_t endPosition_) : colorationKind(colorationKind_), - startPosition(replacementStartPosition_), - endPosition(replacementEndPosition_) {} + startPosition(startPosition_), + endPosition(endPosition_) {} private: ColorationKind colorationKind; @@ -87,7 +89,7 @@ class GD_CORE_API ExpressionSyntaxColoringHelper * \brief Given the expression, find the node at the specified location * and returns colorations for it. */ - static std::vector + static const std::vector GetColorationDescriptionsFor( const gd::Platform &platform, const gd::ProjectScopedContainers &projectScopedContainers, @@ -112,71 +114,59 @@ class GD_CORE_API ExpressionSyntaxColoringHelper void OnVisitSubExpressionNode(SubExpressionNode &node) override { // TODO node.location should includes the parenthesis. { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Operator, node.location.GetStartPosition() - 1, node.expression->location.GetStartPosition()); - colorations.push_back(coloration); } node.expression->Visit(*this); { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Operator, node.expression->location.GetEndPosition(), node.location.GetEndPosition() + 1); - colorations.push_back(coloration); } } void OnVisitOperatorNode(OperatorNode &node) override { node.leftHandSide->Visit(*this); - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::Operator, - node.leftHandSide->location.GetEndPosition(), - node.rightHandSide->location.GetStartPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Operator, + node.leftHandSide->location.GetEndPosition(), + node.rightHandSide->location.GetStartPosition()); node.rightHandSide->Visit(*this); } void OnVisitUnaryOperatorNode(UnaryOperatorNode &node) override { - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::Operator, - node.location.GetStartPosition(), - node.factor->location.GetStartPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Operator, + node.location.GetStartPosition(), + node.factor->location.GetStartPosition()); node.factor->Visit(*this); } void OnVisitNumberNode(NumberNode &node) override { - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::Number, - node.location.GetStartPosition(), node.location.GetEndPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Number, + node.location.GetStartPosition(), + node.location.GetEndPosition()); } void OnVisitTextNode(TextNode &node) override { - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::String, - node.location.GetStartPosition(), node.location.GetEndPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::String, + node.location.GetStartPosition(), + node.location.GetEndPosition()); } void OnVisitVariableNode(VariableNode &node) override { - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::Variable, - node.nameLocation.GetStartPosition(), - node.nameLocation.GetEndPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Variable, + node.nameLocation.GetStartPosition(), + node.nameLocation.GetEndPosition()); if (node.child) { node.child->Visit(*this); } } void OnVisitVariableAccessorNode(VariableAccessorNode &node) override { - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::Variable, - node.dotLocation.GetStartPosition(), - node.nameLocation.GetEndPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Variable, + node.dotLocation.GetStartPosition(), + node.nameLocation.GetEndPosition()); if (node.child) { node.child->Visit(*this); @@ -185,20 +175,18 @@ class GD_CORE_API ExpressionSyntaxColoringHelper void OnVisitVariableBracketAccessorNode( VariableBracketAccessorNode &node) override { { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.location.GetStartPosition(), node.expression->location.GetStartPosition()); - colorations.push_back(coloration); } node.expression->Visit(*this); { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.expression->location.GetEndPosition(), node.child ? node.child->location.GetStartPosition() : node.location.GetEndPosition()); - colorations.push_back(coloration); } if (node.child) { node.child->Visit(*this); @@ -210,16 +198,15 @@ class GD_CORE_API ExpressionSyntaxColoringHelper auto type = gd::ExpressionTypeFinder::GetType( platform, projectScopedContainers, rootType, node); if (gd::ParameterMetadata::IsObject(type)) { - gd::ExpressionColorationDescription coloration( - gd::ExpressionColorationDescription::ColorationKind::Object, - node.identifierNameLocation.GetStartPosition(), - node.identifierNameLocation.GetStartPosition()); - colorations.push_back(coloration); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Object, + node.identifierNameLocation.GetStartPosition(), + node.identifierNameLocation.GetStartPosition()); + } else if (gd::ValueTypeMetadata::IsTypeLegacyPreScopedVariable(type)) { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.location.GetStartPosition(), node.location.GetStartPosition()); - colorations.push_back(coloration); + } else { // Might be: // - An object variable, object behavior or object expression. @@ -228,43 +215,38 @@ class GD_CORE_API ExpressionSyntaxColoringHelper node.identifierName, [&]() { // This is an object. - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Object, node.identifierNameLocation.GetStartPosition(), node.identifierNameLocation.GetEndPosition()); - colorations.push_back(coloration); if (node.childIdentifierNameLocation.IsValid()) { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.childIdentifierNameLocation.GetStartPosition(), node.childIdentifierNameLocation.GetEndPosition()); - colorations.push_back(coloration); } }, [&]() { // This is a variable. - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.location.GetStartPosition(), node.location.GetEndPosition()); - colorations.push_back(coloration); }, [&]() { // This is a property. - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.location.GetStartPosition(), node.location.GetEndPosition()); - colorations.push_back(coloration); }, [&]() { // This is a parameter. - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Variable, node.location.GetStartPosition(), node.location.GetEndPosition()); - colorations.push_back(coloration); }, [&]() { // Ignore unrecognised identifiers here. @@ -276,11 +258,10 @@ class GD_CORE_API ExpressionSyntaxColoringHelper } void OnVisitFunctionCallNode(FunctionCallNode &node) override { if (node.objectNameLocation.IsValid()) { - gd::ExpressionColorationDescription coloration( + AddColoration( gd::ExpressionColorationDescription::ColorationKind::Object, node.objectNameLocation.GetStartPosition(), node.objectNameLocation.GetEndPosition()); - colorations.push_back(coloration); } for (auto &¶meter : node.parameters) { parameter->Visit(*this); @@ -299,6 +280,20 @@ class GD_CORE_API ExpressionSyntaxColoringHelper // in the editor are changed to use coloration. {}; + void AddColoration( + gd::ExpressionColorationDescription::ColorationKind colorationKind, + size_t startPosition, size_t endPosition) { + if (!colorations.empty() && + colorations.back().GetColorationKind() == colorationKind && + colorations.back().GetEndPosition() == startPosition) { + colorations.back().SetEndPosition(endPosition); + } else { + gd::ExpressionColorationDescription coloration( + colorationKind, startPosition, endPosition); + colorations.push_back(coloration); + } + } + std::vector colorations; size_t searchedPosition; From 045e610a30829a4c81d801ff9e53d910afe7cb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 1 May 2026 17:50:13 +0200 Subject: [PATCH 04/10] Add a default color for parameters. --- .../src/EventsSheet/EventsTree/Instruction.js | 7 +- .../ParameterFields/DefaultField.js | 112 ++---------------- .../ParameterFields/VariableField.js | 37 +++++- .../app/src/UI/Theme/Global/EventsSheet.css | 4 + newIDE/app/src/Utils/HighlightSearchText.js | 85 +++++++++++++ 5 files changed, 138 insertions(+), 107 deletions(-) diff --git a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js index 3354f416c21b..f62832470ce2 100644 --- a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js +++ b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js @@ -378,11 +378,12 @@ const Instruction = (props: Props): React.Node => { key={i} className={classNames({ [selectableArea]: true, - [instructionParameter]: - parameterType !== 'number' && parameterType !== 'string', + [instructionParameter]: true, // $FlowFixMe[invalid-computed-prop] [parameterType]: - parameterType !== 'number' && parameterType !== 'string', + parameterType !== 'number' && + parameterType !== 'string' && + !parameterMetadata.getValueTypeMetadata().isVariable(), })} onClick={domEvent => { props.onParameterClick(domEvent, parameterIndex); diff --git a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js index 5cee12f2e199..86ac7d7f5474 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js @@ -10,17 +10,11 @@ import { } from './ParameterFieldCommons'; import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow'; import { - highlightSearchText, renderStylizedText, - type TextStyle, mergeStylizedText, getHighlightSearchTextParts, + applySyntaxColoring, } from '../../Utils/HighlightSearchText'; -import { mapVector } from '../../Utils/MapFor'; -import classNames from 'classnames'; -import { instructionParameter } from '../EventsTree/ClassNames'; - -const gd: libGDevelop = global.gd; export default (React.forwardRef( function DefaultField(props: ParameterFieldProps, ref) { @@ -57,86 +51,6 @@ export default (React.forwardRef( +ref?: React.RefSetter, }>); -const getColorationName = ( - colorationKind: ExpressionColorationDescription_ColorationKind -) => { - switch (colorationKind) { - case gd.ExpressionColorationDescription.Number: - return 'number'; - - case gd.ExpressionColorationDescription.Object: - return 'object'; - - case gd.ExpressionColorationDescription.Variable: - return 'variable'; - - case gd.ExpressionColorationDescription.Operator: - return 'operator'; - - case gd.ExpressionColorationDescription.String: - default: - return 'string'; - } -}; - -export const applySyntaxColoring = ({ - text, - platform, - projectScopedContainers, - rootType, - expression, -}: { - text: string, - platform: gdPlatform, - projectScopedContainers: gdProjectScopedContainers, - rootType: string, - expression: gdExpression, -}): Array => { - const colorationDescriptions = gd.ExpressionSyntaxColoringHelper.getColorationDescriptionsFor( - platform, - projectScopedContainers, - rootType, - expression.getRootNode() - ); - let nextCharacterIndex = 0; - const coloredTextParts: Array = []; - mapVector(colorationDescriptions, colorationDescription => { - const startPosition = colorationDescription.getStartPosition(); - if (startPosition > nextCharacterIndex) { - coloredTextParts.push({ - startIndex: nextCharacterIndex, - endIndex: startPosition, - props: {}, - key: `color-part--${coloredTextParts.length}`, - }); - nextCharacterIndex = startPosition; - } - const endPosition = colorationDescription.getEndPosition(); - coloredTextParts.push({ - startIndex: startPosition, - endIndex: endPosition, - props: { - className: classNames({ - [instructionParameter]: true, - //$FlowFixMe[invalid-computed-prop] - [getColorationName(colorationDescription.getColorationKind())]: true, - }), - }, - key: `color-part--${coloredTextParts.length}`, - }); - nextCharacterIndex = endPosition; - }); - if (nextCharacterIndex < text.length) { - coloredTextParts.push({ - startIndex: nextCharacterIndex, - endIndex: text.length, - props: {}, - key: `color-part--${coloredTextParts.length}`, - }); - } - return coloredTextParts; -}; - export const renderInlineDefaultField = ({ value, expressionIsValid, @@ -157,22 +71,16 @@ export const renderInlineDefaultField = ({ if (!expressionIsValid) { return ( - {highlightSearchText(value, highlightedSearchText, { - matchCase: highlightedSearchMatchCase, - })} + {renderStylizedText( + value, + getHighlightSearchTextParts(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + }) + )} ); } - if (hasDeprecationWarning) { - return ( - - {highlightSearchText(value, highlightedSearchText, { - matchCase: highlightedSearchMatchCase, - })} - - ); - } - return renderStylizedText( + const stylizedText = renderStylizedText( value, mergeStylizedText( getHighlightSearchTextParts(value, highlightedSearchText, { @@ -187,4 +95,8 @@ export const renderInlineDefaultField = ({ }) ) ); + if (hasDeprecationWarning) { + return {stylizedText}; + } + return stylizedText; }; diff --git a/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js index 628c24a4d878..d9661ae0c6c9 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js @@ -14,6 +14,7 @@ import { icon, nameAndIconContainer, instructionWarningParameter, + instructionParameter, } from '../EventsTree/ClassNames'; import SemiControlledAutoComplete, { type SemiControlledAutoCompleteInterface, @@ -21,7 +22,12 @@ import SemiControlledAutoComplete, { } from '../../UI/SemiControlledAutoComplete'; import { TextFieldWithButtonLayout } from '../../UI/Layout'; import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow'; -import { highlightSearchText } from '../../Utils/HighlightSearchText'; +import { + renderStylizedText, + mergeStylizedText, + getHighlightSearchTextParts, + applySyntaxColoring, +} from '../../Utils/HighlightSearchText'; import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal'; import SelectField from '../../UI/SelectField'; import SelectOption from '../../UI/SelectOption'; @@ -526,6 +532,7 @@ export default (React.forwardRef( export const renderVariableWithIcon = ( { value, + expression, parameterMetadata, expressionIsValid, hasDeprecationWarning, @@ -535,6 +542,7 @@ export const renderVariableWithIcon = ( projectScopedContainersAccessor, highlightedSearchText, highlightedSearchMatchCase, + scope, }: ParameterInlineRendererProps, tooltip: string, getVariableSourceFromIdentifier: ( @@ -575,11 +583,32 @@ export const renderVariableWithIcon = ( - {highlightSearchText(value, highlightedSearchText, { - matchCase: highlightedSearchMatchCase, - })} + {expressionIsValid + ? renderStylizedText( + value, + mergeStylizedText( + getHighlightSearchTextParts(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + }), + applySyntaxColoring({ + text: value, + expression, + rootType: parameterMetadata.getValueTypeMetadata().getName(), + platform: scope.project.getCurrentPlatform(), + projectScopedContainers: projectScopedContainersAccessor.get(), + }) + ) + ) + : renderStylizedText( + value, + getHighlightSearchTextParts(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + }) + )} ); diff --git a/newIDE/app/src/UI/Theme/Global/EventsSheet.css b/newIDE/app/src/UI/Theme/Global/EventsSheet.css index 9ddcf11d86d0..48ef102bcafa 100644 --- a/newIDE/app/src/UI/Theme/Global/EventsSheet.css +++ b/newIDE/app/src/UI/Theme/Global/EventsSheet.css @@ -143,6 +143,10 @@ color: var(--event-sheet-instruction-parameter-base-color); } +.gd-events-sheet .instruction-parameter.string { + color: var(--event-sheet-instruction-parameter-string-color); +} + .gd-events-sheet .instruction-parameter.number { color: var(--event-sheet-instruction-parameter-number-color); } diff --git a/newIDE/app/src/Utils/HighlightSearchText.js b/newIDE/app/src/Utils/HighlightSearchText.js index 1293d7331d0d..54fd54e63237 100644 --- a/newIDE/app/src/Utils/HighlightSearchText.js +++ b/newIDE/app/src/Utils/HighlightSearchText.js @@ -1,5 +1,10 @@ // @flow import * as React from 'react'; +import { mapVector } from '../Utils/MapFor'; +import classNames from 'classnames'; +import { instructionParameter } from '../EventsSheet/EventsTree/ClassNames'; + +const gd: libGDevelop = global.gd; const GLOBAL_SEARCH_MATCH_CLASS_NAME = 'global-search-text-match'; @@ -159,3 +164,83 @@ export const highlightSearchText = ( getHighlightSearchTextParts(text, searchText, spanProps) ); }; + +const getColorationName = ( + colorationKind: ExpressionColorationDescription_ColorationKind +) => { + switch (colorationKind) { + case gd.ExpressionColorationDescription.Number: + return 'number'; + + case gd.ExpressionColorationDescription.Object: + return 'object'; + + case gd.ExpressionColorationDescription.Variable: + return 'variable'; + + case gd.ExpressionColorationDescription.Operator: + return 'operator'; + + case gd.ExpressionColorationDescription.String: + default: + return 'string'; + } +}; + +export const applySyntaxColoring = ({ + text, + platform, + projectScopedContainers, + rootType, + expression, +}: { + text: string, + platform: gdPlatform, + projectScopedContainers: gdProjectScopedContainers, + rootType: string, + expression: gdExpression, +}): Array => { + const colorationDescriptions = gd.ExpressionSyntaxColoringHelper.getColorationDescriptionsFor( + platform, + projectScopedContainers, + rootType, + expression.getRootNode() + ); + let nextCharacterIndex = 0; + const coloredTextParts: Array = []; + mapVector(colorationDescriptions, colorationDescription => { + const startPosition = colorationDescription.getStartPosition(); + if (startPosition > nextCharacterIndex) { + coloredTextParts.push({ + startIndex: nextCharacterIndex, + endIndex: startPosition, + props: {}, + key: `color-part--${coloredTextParts.length}`, + }); + nextCharacterIndex = startPosition; + } + const endPosition = colorationDescription.getEndPosition(); + coloredTextParts.push({ + startIndex: startPosition, + endIndex: endPosition, + props: { + className: classNames({ + [instructionParameter]: true, + //$FlowFixMe[invalid-computed-prop] + [getColorationName(colorationDescription.getColorationKind())]: true, + }), + }, + key: `color-part--${coloredTextParts.length}`, + }); + nextCharacterIndex = endPosition; + }); + if (nextCharacterIndex < text.length) { + coloredTextParts.push({ + startIndex: nextCharacterIndex, + endIndex: text.length, + props: {}, + key: `color-part--${coloredTextParts.length}`, + }); + } + return coloredTextParts; +}; From 969f3023e00be9cb3af029ec6372d985e92e9d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 1 May 2026 19:01:39 +0200 Subject: [PATCH 05/10] Fix resource field color. --- GDevelop.js/Bindings/Bindings.idl | 1 + GDevelop.js/types.d.ts | 1 + GDevelop.js/types/gdvaluetypemetadata.js | 1 + .../src/EventsSheet/EventsTree/Instruction.js | 6 ++- .../ParameterFields/ImageResourceField.js | 39 +++++++++++++++++++ .../EventsSheet/ParameterRenderingService.js | 13 +++++-- .../app/src/UI/Theme/Global/EventsSheet.css | 3 +- 7 files changed, 58 insertions(+), 6 deletions(-) diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index e69e6647dab3..eea48a629a97 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -1976,6 +1976,7 @@ interface ValueTypeMetadata { boolean IsNumber(); boolean IsString(); boolean IsVariable(); + boolean IsResource(); boolean STATIC_IsTypeObject([Const] DOMString parameterType); boolean STATIC_IsTypeBehavior([Const] DOMString parameterType); diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index 3360dc30e509..7b70047b1688 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -1648,6 +1648,7 @@ export class ValueTypeMetadata extends EmscriptenObject { isNumber(): boolean; isString(): boolean; isVariable(): boolean; + isResource(): boolean; static isTypeObject(parameterType: string): boolean; static isTypeBehavior(parameterType: string): boolean; static isTypeExpression(type: string, parameterType: string): boolean; diff --git a/GDevelop.js/types/gdvaluetypemetadata.js b/GDevelop.js/types/gdvaluetypemetadata.js index e5fc47205742..03788457007b 100644 --- a/GDevelop.js/types/gdvaluetypemetadata.js +++ b/GDevelop.js/types/gdvaluetypemetadata.js @@ -14,6 +14,7 @@ declare class gdValueTypeMetadata { isNumber(): boolean; isString(): boolean; isVariable(): boolean; + isResource(): boolean; static isTypeObject(parameterType: string): boolean; static isTypeBehavior(parameterType: string): boolean; static isTypeExpression(type: string, parameterType: string): boolean; diff --git a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js index f62832470ce2..42e70fbd285e 100644 --- a/newIDE/app/src/EventsSheet/EventsTree/Instruction.js +++ b/newIDE/app/src/EventsSheet/EventsTree/Instruction.js @@ -379,8 +379,12 @@ const Instruction = (props: Props): React.Node => { className={classNames({ [selectableArea]: true, [instructionParameter]: true, + // Resources are string literals they use the same color as strings. // $FlowFixMe[invalid-computed-prop] - [parameterType]: + [parameterMetadata.getValueTypeMetadata().isResource() + ? 'resource' + : parameterType]: + // Variables, numbers and strings are expressions with syntax coloring. parameterType !== 'number' && parameterType !== 'string' && !parameterMetadata.getValueTypeMetadata().isVariable(), diff --git a/newIDE/app/src/EventsSheet/ParameterFields/ImageResourceField.js b/newIDE/app/src/EventsSheet/ParameterFields/ImageResourceField.js index ced63202e68b..c205e439f61b 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/ImageResourceField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/ImageResourceField.js @@ -11,6 +11,8 @@ import { type ParameterFieldInterface, type FieldFocusFunction, } from './ParameterFieldCommons'; +import { type ParameterInlineRendererProps } from './ParameterInlineRenderer.flow'; +import { highlightSearchText } from '../../Utils/HighlightSearchText'; const ImageResourceField: React.ComponentType<{ ...ParameterFieldProps, @@ -53,3 +55,40 @@ const ImageResourceField: React.ComponentType<{ ); export default ImageResourceField; + +export const renderInlineResourceField = ({ + value, + expressionIsValid, + hasDeprecationWarning, + parameterMetadata, + InvalidParameterValue, + DeprecatedParameterValue, + MissingParameterValue, + highlightedSearchText, + highlightedSearchMatchCase, +}: ParameterInlineRendererProps): string | React.Node => { + if (!value && !parameterMetadata.isOptional()) { + return ; + } + if (!expressionIsValid) { + return ( + + {highlightSearchText(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + })} + + ); + } + if (hasDeprecationWarning) { + return ( + + {highlightSearchText(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + })} + + ); + } + return highlightSearchText(value, highlightedSearchText, { + matchCase: highlightedSearchMatchCase, + }); +}; diff --git a/newIDE/app/src/EventsSheet/ParameterRenderingService.js b/newIDE/app/src/EventsSheet/ParameterRenderingService.js index 17aca1eee085..5d519ff54ccf 100644 --- a/newIDE/app/src/EventsSheet/ParameterRenderingService.js +++ b/newIDE/app/src/EventsSheet/ParameterRenderingService.js @@ -53,7 +53,9 @@ import ObjectVariableField, { renderInlineObjectVariable, } from './ParameterFields/ObjectVariableField'; import LayerField from './ParameterFields/LayerField'; -import ImageResourceField from './ParameterFields/ImageResourceField'; +import ImageResourceField, { + renderInlineResourceField, +} from './ParameterFields/ImageResourceField'; import AudioResourceField from './ParameterFields/AudioResourceField'; import VideoResourceField from './ParameterFields/VideoResourceField'; import JsonResourceField from './ParameterFields/JsonResourceField'; @@ -153,6 +155,7 @@ const inlineRenderers: { [string]: ParameterInlineRenderer } = { mouse: renderInlineMouse, mouseButton: renderInlineMouseButton, object: renderInlineObjectWithThumbnail, + resource: renderInlineResourceField, yesorno: renderInlineYesNo, trueorfalse: renderInlineTrueFalse, operator: renderInlineOperator, @@ -218,10 +221,12 @@ const ParameterRenderingService = { else return components.default; }, renderInlineParameter: (props: ParameterInlineRendererProps): React.Node => { - const rawType = props.parameterMetadata.getType(); - const fieldType = gd.ParameterMetadata.isObject(rawType) + const valueTypeMetadata = props.parameterMetadata.getValueTypeMetadata(); + const fieldType = valueTypeMetadata.isObject() ? 'object' - : rawType; + : valueTypeMetadata.isResource() + ? 'resource' + : valueTypeMetadata.getName(); const inlineRenderer = inlineRenderers[fieldType] || inlineRenderers.default; diff --git a/newIDE/app/src/UI/Theme/Global/EventsSheet.css b/newIDE/app/src/UI/Theme/Global/EventsSheet.css index 48ef102bcafa..3d080eb25a04 100644 --- a/newIDE/app/src/UI/Theme/Global/EventsSheet.css +++ b/newIDE/app/src/UI/Theme/Global/EventsSheet.css @@ -143,7 +143,8 @@ color: var(--event-sheet-instruction-parameter-base-color); } -.gd-events-sheet .instruction-parameter.string { +.gd-events-sheet .instruction-parameter.string, +.gd-events-sheet .instruction-parameter.resource { color: var(--event-sheet-instruction-parameter-string-color); } From 37967cbb477239059532baafab9c26bb72ec3dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 3 May 2026 17:01:09 +0200 Subject: [PATCH 06/10] Fix factor parsing --- .../GDCore/Events/Parsers/ExpressionParser2.h | 26 ++++++++----------- .../Events/ExpressionSyntaxColoringHelper.h | 5 ++-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/Core/GDCore/Events/Parsers/ExpressionParser2.h b/Core/GDCore/Events/Parsers/ExpressionParser2.h index 97e1b467cfa9..9bd9295b3d4b 100644 --- a/Core/GDCore/Events/Parsers/ExpressionParser2.h +++ b/Core/GDCore/Events/Parsers/ExpressionParser2.h @@ -199,8 +199,14 @@ class GD_CORE_API ExpressionParser2 { std::unique_ptr factor = ReadNumber(); return factor; } else if (CheckIfChar(IsOpeningParenthesis)) { + size_t expressionStartPosition = GetCurrentPosition(); SkipChar(); - std::unique_ptr factor = SubExpression(); + + // The expression inside the parentheses excluding them. + auto expression = Expression(); + + // The expression and its parentheses. + auto factor = gd::make_unique(std::move(expression)); if (!CheckIfChar(IsClosingParenthesis)) { factor->diagnostic = @@ -208,7 +214,10 @@ class GD_CORE_API ExpressionParser2 { "parenthesis for each opening parenthesis.")); } SkipIfChar(IsClosingParenthesis); - return factor; + factor->location = ExpressionParserLocation(expressionStartPosition, + GetCurrentPosition()); + + return std::move(factor); } else if (CheckIfChar(IsAllowedInIdentifier)) { return Identifier(); } @@ -217,19 +226,6 @@ class GD_CORE_API ExpressionParser2 { return factor; } - std::unique_ptr SubExpression() { - size_t expressionStartPosition = GetCurrentPosition(); - - auto expression = Expression(); - - auto subExpression = - gd::make_unique(std::move(expression)); - subExpression->location = - ExpressionParserLocation(expressionStartPosition, GetCurrentPosition()); - - return std::move(subExpression); - }; - std::unique_ptr Identifier() { auto identifierAndLocation = ReadIdentifierName(); diff --git a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h index c827c05835c4..f54f0bb93ee0 100644 --- a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h +++ b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h @@ -112,11 +112,10 @@ class GD_CORE_API ExpressionSyntaxColoringHelper protected: void OnVisitSubExpressionNode(SubExpressionNode &node) override { - // TODO node.location should includes the parenthesis. { AddColoration( gd::ExpressionColorationDescription::ColorationKind::Operator, - node.location.GetStartPosition() - 1, + node.location.GetStartPosition(), node.expression->location.GetStartPosition()); } node.expression->Visit(*this); @@ -124,7 +123,7 @@ class GD_CORE_API ExpressionSyntaxColoringHelper AddColoration( gd::ExpressionColorationDescription::ColorationKind::Operator, node.expression->location.GetEndPosition(), - node.location.GetEndPosition() + 1); + node.location.GetEndPosition()); } } void OnVisitOperatorNode(OperatorNode &node) override { From abfc6160fe040354810213f2bbca293937ec2f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 3 May 2026 17:05:19 +0200 Subject: [PATCH 07/10] Fix typo in comments --- .../IDE/Events/ExpressionSyntaxColoringHelper.h | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h index f54f0bb93ee0..7245f01960b5 100644 --- a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h +++ b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h @@ -34,28 +34,22 @@ class ObjectConfiguration; namespace gd { /** - * \brief Describe colorations to be shown to the user. + * \brief Describe colorations to be shown to users. */ struct GD_CORE_API ExpressionColorationDescription { public: - /** - * The different kind of colorations that can be described. - * The IDE is responsible for actually *searching* and showing the colorations - * of colorations with a kind "WithPrefix": these colorations are only - * describing what must be listed. - */ enum ColorationKind { String, Number, Object, Variable, Operator }; /** \brief Return the kind of the coloration */ ColorationKind GetColorationKind() const { return colorationKind; } /** - * \brief Return the first character index of the autocompleted part. + * \brief Return the first character index of the colorized part. */ size_t GetStartPosition() const { return startPosition; } /** - * \brief Return the first character index after the autocompleted part. + * \brief Return the first character index after the colorized part. */ size_t GetEndPosition() const { return endPosition; } From 220343ddaec24e6a3c28a17cd98e35297b30f361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 3 May 2026 17:53:44 +0200 Subject: [PATCH 08/10] Add test on the coloration --- .../Events/ExpressionSyntaxColoringHelper.h | 24 ++-- Core/tests/ExpressionSyntaxColoringHelper.cpp | 122 ++++++++++++++++++ 2 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 Core/tests/ExpressionSyntaxColoringHelper.cpp diff --git a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h index 7245f01960b5..b0c2d54dd076 100644 --- a/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h +++ b/Core/GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h @@ -55,14 +55,23 @@ struct GD_CORE_API ExpressionColorationDescription { void SetEndPosition(size_t endPosition_) { endPosition = endPosition_; } + gd::String ToString() const { + return (colorationKind == 0 ? "String" + : colorationKind == 1 ? "Number" + : colorationKind == 2 ? "Object" + : colorationKind == 3 ? "Variable" + : colorationKind == 4 ? "Operator" + : "Unknown") + + gd::String(" [") + gd::String::From(startPosition) + " " + + gd::String::From(endPosition) + "["; + } + /** Default constructor, only to be used by Emscripten bindings. */ ExpressionColorationDescription() : colorationKind(String){}; ExpressionColorationDescription(ColorationKind colorationKind_, - size_t startPosition_, - size_t endPosition_) - : colorationKind(colorationKind_), - startPosition(startPosition_), + size_t startPosition_, size_t endPosition_) + : colorationKind(colorationKind_), startPosition(startPosition_), endPosition(endPosition_) {} private: @@ -251,10 +260,9 @@ class GD_CORE_API ExpressionSyntaxColoringHelper } void OnVisitFunctionCallNode(FunctionCallNode &node) override { if (node.objectNameLocation.IsValid()) { - AddColoration( - gd::ExpressionColorationDescription::ColorationKind::Object, - node.objectNameLocation.GetStartPosition(), - node.objectNameLocation.GetEndPosition()); + AddColoration(gd::ExpressionColorationDescription::ColorationKind::Object, + node.objectNameLocation.GetStartPosition(), + node.objectNameLocation.GetEndPosition()); } for (auto &¶meter : node.parameters) { parameter->Visit(*this); diff --git a/Core/tests/ExpressionSyntaxColoringHelper.cpp b/Core/tests/ExpressionSyntaxColoringHelper.cpp new file mode 100644 index 000000000000..516981c950af --- /dev/null +++ b/Core/tests/ExpressionSyntaxColoringHelper.cpp @@ -0,0 +1,122 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/IDE/Events/ExpressionSyntaxColoringHelper.h" + +#include + +#include "DummyPlatform.h" +#include "GDCore/Events/Parsers/ExpressionParser2.h" +#include "GDCore/Extensions/Metadata/MetadataProvider.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Project/Layout.h" +#include "GDCore/Project/Project.h" +#include "GDCore/Project/ProjectScopedContainers.h" +#include "catch.hpp" + +TEST_CASE("ExpressionSyntaxColoringHelper", "[common][events]") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &scene = project.InsertNewLayout("Layout1", 0); + scene.GetVariables().InsertNew("MySceneVariable"); + auto &object1 = scene.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyObject", 0); + object1.GetVariables().InsertNew("MyObjectVariable"); + + gd::ProjectScopedContainers projectScopedContainers = + gd::ProjectScopedContainers:: + MakeNewProjectScopedContainersForProjectAndLayout(project, scene); + + gd::ExpressionParser2 parser; + + auto getColorationsFor = [&](const gd::String &type, + const gd::String &expression) { + auto node = parser.ParseExpression(expression); + REQUIRE(node != nullptr); + auto colorations = + gd::ExpressionSyntaxColoringHelper::GetColorationDescriptionsFor( + platform, projectScopedContainers, type, *node); + std::vector colorationsAsString; + for (const auto &coloration : colorations) { + colorationsAsString.push_back(coloration.ToString()); + } + return colorationsAsString; + }; + + SECTION("Can colorize scene variables") { + // clang-format off + std::vector expectedCompletions{ + "Variable [0 15[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "MySceneVariable") == + expectedCompletions); + } + SECTION("Can colorize variable with children") { + // clang-format off + std::vector expectedCompletions{ + "Variable [0 31[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "MySceneVariable.MyChild.MyChild") == + expectedCompletions); + } + SECTION("Can colorize variable with bracket access") { + // clang-format off + std::vector expectedCompletions{ + "Variable [0 16[", + "String [16 25[", + "Variable [25 26[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "MySceneVariable[\"MyChild\"]") == + expectedCompletions); + } + SECTION("Can colorize object variables") { + // clang-format off + std::vector expectedCompletions{ + "Object [0 8[", + "Variable [9 25[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "MyObject.MyObjectVariable") == + expectedCompletions); + } + SECTION("Can colorize object function") { + // clang-format off + std::vector expectedCompletions{ + "Object [0 8[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "MyObject.MyFunction()") == + expectedCompletions); + } + SECTION("Can colorize function parameters") { + // clang-format off + std::vector expectedCompletions{ + "String [14 23[", + "Number [25 26[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "CameraCenterX(\"MyLayer\", 0)") == + expectedCompletions); + } + SECTION("Can colorize numbers and operators") { + // clang-format off + std::vector expectedCompletions{ + "Operator [0 1[", + "Number [1 4[", + "Operator [4 7[", + "Number [7 10[", + "Operator [10 14[", + "Number [14 15[", + }; + // clang-format on + REQUIRE(getColorationsFor("number", "(123 + 456) * 2") == + expectedCompletions); + } +} From b501c3d79623f777710a24c2653814614ed53cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 3 May 2026 19:03:47 +0200 Subject: [PATCH 09/10] Add test on color and search merging --- .../ParameterFields/DefaultField.js | 2 +- .../ParameterFields/VariableField.js | 2 +- newIDE/app/src/Utils/HighlightSearchText.js | 12 +- .../app/src/Utils/HighlightSearchText.spec.js | 322 ++++++++++++++++++ 4 files changed, 330 insertions(+), 8 deletions(-) create mode 100644 newIDE/app/src/Utils/HighlightSearchText.spec.js diff --git a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js index 86ac7d7f5474..8738f12eb554 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/DefaultField.js @@ -88,7 +88,7 @@ export const renderInlineDefaultField = ({ }), applySyntaxColoring({ text: value, - expression, + rootNode: expression.getRootNode(), rootType: parameterMetadata.getValueTypeMetadata().getName(), platform: scope.project.getCurrentPlatform(), projectScopedContainers: projectScopedContainersAccessor.get(), diff --git a/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js index d9661ae0c6c9..2316ffc8faf9 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/VariableField.js @@ -596,7 +596,7 @@ export const renderVariableWithIcon = ( }), applySyntaxColoring({ text: value, - expression, + rootNode: expression.getRootNode(), rootType: parameterMetadata.getValueTypeMetadata().getName(), platform: scope.project.getCurrentPlatform(), projectScopedContainers: projectScopedContainersAccessor.get(), diff --git a/newIDE/app/src/Utils/HighlightSearchText.js b/newIDE/app/src/Utils/HighlightSearchText.js index 54fd54e63237..d608cc641757 100644 --- a/newIDE/app/src/Utils/HighlightSearchText.js +++ b/newIDE/app/src/Utils/HighlightSearchText.js @@ -192,19 +192,19 @@ export const applySyntaxColoring = ({ platform, projectScopedContainers, rootType, - expression, + rootNode, }: { text: string, platform: gdPlatform, projectScopedContainers: gdProjectScopedContainers, rootType: string, - expression: gdExpression, + rootNode: gdExpressionNode, }): Array => { const colorationDescriptions = gd.ExpressionSyntaxColoringHelper.getColorationDescriptionsFor( platform, projectScopedContainers, rootType, - expression.getRootNode() + rootNode ); let nextCharacterIndex = 0; const coloredTextParts: Array = []; @@ -215,7 +215,7 @@ export const applySyntaxColoring = ({ startIndex: nextCharacterIndex, endIndex: startPosition, props: {}, - key: `color-part--${coloredTextParts.length}`, + key: `color-part-${coloredTextParts.length}`, }); nextCharacterIndex = startPosition; } @@ -230,7 +230,7 @@ export const applySyntaxColoring = ({ [getColorationName(colorationDescription.getColorationKind())]: true, }), }, - key: `color-part--${coloredTextParts.length}`, + key: `color-part-${coloredTextParts.length}`, }); nextCharacterIndex = endPosition; }); @@ -239,7 +239,7 @@ export const applySyntaxColoring = ({ startIndex: nextCharacterIndex, endIndex: text.length, props: {}, - key: `color-part--${coloredTextParts.length}`, + key: `color-part-${coloredTextParts.length}`, }); } return coloredTextParts; diff --git a/newIDE/app/src/Utils/HighlightSearchText.spec.js b/newIDE/app/src/Utils/HighlightSearchText.spec.js new file mode 100644 index 000000000000..66898802cc29 --- /dev/null +++ b/newIDE/app/src/Utils/HighlightSearchText.spec.js @@ -0,0 +1,322 @@ +// @flow +import { + getHighlightSearchTextParts, + mergeStylizedText, + applySyntaxColoring, +} from './HighlightSearchText'; +import { makeTestProject } from '../fixtures/TestProject'; + +const gd: libGDevelop = global.gd; + +describe('HighlightSearchText', () => { + describe('getHighlightSearchTextParts', () => { + it('can find an occurrence in a text', () => { + expect( + getHighlightSearchTextParts( + '"Lorem ipsum" + "dolor sit amet"', + 'ipsum', + { + className: 'Highlighted', + } + ) + ).toMatchInlineSnapshot( + [ + { + startIndex: 0, + endIndex: 7, + props: {}, + key: 'ipsum-0', + }, + { + startIndex: 7, + endIndex: 12, + props: { + className: 'Highlighted', + }, + key: 'ipsum-1', + }, + { + startIndex: 12, + endIndex: 32, + props: {}, + key: 'ipsum-2', + }, + ], + ` + Array [ + Object { + "endIndex": 7, + "key": "ipsum-0", + "props": Object {}, + "startIndex": 0, + }, + Object { + "endIndex": 12, + "key": "ipsum-1", + "props": Object { + "className": "Highlighted", + }, + "startIndex": 7, + }, + Object { + "endIndex": 32, + "key": "ipsum-2", + "props": Object {}, + "startIndex": 12, + }, + ] + ` + ); + }); + }); + + describe('applySyntaxColoring', () => { + it('can apply syntax coloring on an expression', () => { + const { + project, + testSceneProjectScopedContainersAccessor, + } = makeTestProject(gd); + const text = '"Lorem ipsum" + "dolor sit amet"'; + const parser = new gd.ExpressionParser2(); + const rootNode = parser.parseExpression(text).get(); + expect( + applySyntaxColoring({ + text, + platform: project.getCurrentPlatform(), + projectScopedContainers: testSceneProjectScopedContainersAccessor.get(), + rootType: 'number', + rootNode, + }) + ).toMatchInlineSnapshot( + ` + Array [ + Object { + "endIndex": 13, + "key": "color-part-0", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 0, + }, + Object { + "endIndex": 16, + "key": "color-part-1", + "props": Object { + "className": "instruction-parameter operator", + }, + "startIndex": 13, + }, + Object { + "endIndex": 32, + "key": "color-part-2", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 16, + }, + ] + ` + ); + parser.delete(); + }); + }); + + describe('mergeStylizedText', () => { + it('can merge syntax coloring and search highlight with highlight over 1 color', () => { + const { + project, + testSceneProjectScopedContainersAccessor, + } = makeTestProject(gd); + const text = '"Lorem ipsum" + "dolor sit amet"'; + const parser = new gd.ExpressionParser2(); + const rootNode = parser.parseExpression(text).get(); + expect( + mergeStylizedText( + getHighlightSearchTextParts( + '"Lorem ipsum" + "dolor sit amet"', + 'ipsum', + { + className: 'Highlighted', + } + ), + applySyntaxColoring({ + text, + platform: project.getCurrentPlatform(), + projectScopedContainers: testSceneProjectScopedContainersAccessor.get(), + rootType: 'number', + rootNode, + }) + ) + ).toMatchInlineSnapshot( + ` + Array [ + Object { + "children": Array [ + Object { + "endIndex": 7, + "key": "color-part-0", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 0, + }, + ], + "endIndex": 7, + "key": "ipsum-0", + "props": Object {}, + "startIndex": 0, + }, + Object { + "children": Array [ + Object { + "endIndex": 12, + "key": "color-part-0", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 7, + }, + ], + "endIndex": 12, + "key": "ipsum-1", + "props": Object { + "className": "Highlighted", + }, + "startIndex": 7, + }, + Object { + "children": Array [ + Object { + "endIndex": 13, + "key": "color-part-0", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 12, + }, + Object { + "endIndex": 16, + "key": "color-part-1", + "props": Object { + "className": "instruction-parameter operator", + }, + "startIndex": 13, + }, + Object { + "endIndex": 32, + "key": "color-part-2", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 16, + }, + ], + "endIndex": 32, + "key": "ipsum-2", + "props": Object {}, + "startIndex": 12, + }, + ] + ` + ); + parser.delete(); + }); + it('can merge syntax coloring and search highlight with highlight over several colors', () => { + const { + project, + testSceneProjectScopedContainersAccessor, + } = makeTestProject(gd); + const text = '"Lorem ipsum" + "dolor sit amet"'; + const parser = new gd.ExpressionParser2(); + const rootNode = parser.parseExpression(text).get(); + expect( + mergeStylizedText( + getHighlightSearchTextParts( + '"Lorem ipsum" + "dolor sit amet"', + 'ipsum" + "dolor', + { + className: 'Highlighted', + } + ), + applySyntaxColoring({ + text, + platform: project.getCurrentPlatform(), + projectScopedContainers: testSceneProjectScopedContainersAccessor.get(), + rootType: 'number', + rootNode, + }) + ) + ).toMatchInlineSnapshot(` + Array [ + Object { + "children": Array [ + Object { + "endIndex": 7, + "key": "color-part-0", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 0, + }, + ], + "endIndex": 7, + "key": "ipsum\\" + \\"dolor-0", + "props": Object {}, + "startIndex": 0, + }, + Object { + "children": Array [ + Object { + "endIndex": 13, + "key": "color-part-0", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 7, + }, + Object { + "endIndex": 16, + "key": "color-part-1", + "props": Object { + "className": "instruction-parameter operator", + }, + "startIndex": 13, + }, + Object { + "endIndex": 22, + "key": "color-part-2", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 16, + }, + ], + "endIndex": 22, + "key": "ipsum\\" + \\"dolor-1", + "props": Object { + "className": "Highlighted", + }, + "startIndex": 7, + }, + Object { + "children": Array [ + Object { + "endIndex": 32, + "key": "color-part-2", + "props": Object { + "className": "instruction-parameter string", + }, + "startIndex": 22, + }, + ], + "endIndex": 32, + "key": "ipsum\\" + \\"dolor-2", + "props": Object {}, + "startIndex": 22, + }, + ] + `); + parser.delete(); + }); + }); +}); From 69a4a64fab6846132cfd2994ba199a1f4dc1b7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 3 May 2026 22:15:19 +0200 Subject: [PATCH 10/10] Fix missing color in themes --- newIDE/app/scripts/theme-templates/theme.json | 5 +++++ newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/DeepBlueTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/NordTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/OneDarkTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/RosePineTheme/theme.json | 5 +++++ newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json | 5 +++++ 9 files changed, 45 insertions(+) diff --git a/newIDE/app/scripts/theme-templates/theme.json b/newIDE/app/scripts/theme-templates/theme.json index 4ab8f0aecee1..eef6bde9ad4c 100644 --- a/newIDE/app/scripts/theme-templates/theme.json +++ b/newIDE/app/scripts/theme-templates/theme.json @@ -541,6 +541,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#93e500" } diff --git a/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json b/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json index 4dc06564fbe9..6d070781ddc5 100644 --- a/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json @@ -707,6 +707,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#93e500" } diff --git a/newIDE/app/src/UI/Theme/DeepBlueTheme/theme.json b/newIDE/app/src/UI/Theme/DeepBlueTheme/theme.json index f47a0fcdc574..493c6fc33104 100644 --- a/newIDE/app/src/UI/Theme/DeepBlueTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DeepBlueTheme/theme.json @@ -582,6 +582,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#79c0ff" } diff --git a/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json b/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json index d077bf506a2e..a5ca39ff9903 100644 --- a/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json @@ -764,6 +764,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#0ECD7A" } diff --git a/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json b/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json index 273e4ade7f74..f43eb0a88c5e 100644 --- a/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json @@ -762,6 +762,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#000000" + } + }, + "string": { "color": { "value": "#00724E" } diff --git a/newIDE/app/src/UI/Theme/NordTheme/theme.json b/newIDE/app/src/UI/Theme/NordTheme/theme.json index c1d04af13e0f..245444909708 100644 --- a/newIDE/app/src/UI/Theme/NordTheme/theme.json +++ b/newIDE/app/src/UI/Theme/NordTheme/theme.json @@ -698,6 +698,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#8FBCBB" } diff --git a/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json b/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json index 4d5ce48a6811..6a5effe080e0 100644 --- a/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json @@ -703,6 +703,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#98C379" } diff --git a/newIDE/app/src/UI/Theme/RosePineTheme/theme.json b/newIDE/app/src/UI/Theme/RosePineTheme/theme.json index bc76efbfd865..b6bc4f3c27d4 100644 --- a/newIDE/app/src/UI/Theme/RosePineTheme/theme.json +++ b/newIDE/app/src/UI/Theme/RosePineTheme/theme.json @@ -699,6 +699,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#9ccfd8" } diff --git a/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json b/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json index d0448c5f7bae..cf851651b090 100644 --- a/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json @@ -700,6 +700,11 @@ }, "instruction-parameter": { "base": { + "color": { + "value": "#ffffff" + } + }, + "string": { "color": { "value": "#219186" }