Merge pull request #3121 from apache/delivery

Sync delivery to release125 for 12.5-beta2
diff --git a/enterprise/glassfish.common/src/org/netbeans/modules/glassfish/common/ServerDetails.java b/enterprise/glassfish.common/src/org/netbeans/modules/glassfish/common/ServerDetails.java
index a5ce6a8..d8199f0 100644
--- a/enterprise/glassfish.common/src/org/netbeans/modules/glassfish/common/ServerDetails.java
+++ b/enterprise/glassfish.common/src/org/netbeans/modules/glassfish/common/ServerDetails.java
@@ -209,7 +209,7 @@
      */
     GLASSFISH_SERVER_6(NbBundle.getMessage(ServerDetails.class, "STR_6_SERVER_NAME", new Object[]{}), // NOI18N
         "deployer:gfv6ee9", // NOI18N
-        6,
+        600,
         "https://repo1.maven.org/maven2/org/glassfish/main/distributions/glassfish/6.0.0/glassfish-6.0.0.zip", // NOI18N
         "https://repo1.maven.org/maven2/org/glassfish/main/distributions/glassfish/6.0.0/glassfish-6.0.0.zip", // NOI18N
         "http://www.eclipse.org/legal/epl-2.0" //NOI18N
@@ -220,7 +220,7 @@
      */
     GLASSFISH_SERVER_6_1_0(NbBundle.getMessage(ServerDetails.class, "STR_610_SERVER_NAME", new Object[]{}), // NOI18N
         "deployer:gfv610ee9", // NOI18N
-        6,
+        610,
         "https://repo1.maven.org/maven2/org/glassfish/main/distributions/glassfish/6.1.0/glassfish-6.1.0.zip", // NOI18N
         "https://repo1.maven.org/maven2/org/glassfish/main/distributions/glassfish/6.1.0/glassfish-6.1.0.zip", // NOI18N
         "http://www.eclipse.org/legal/epl-2.0" //NOI18N
diff --git a/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java b/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java
index 3bf4d48..d11d1ed 100644
--- a/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java
+++ b/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java
@@ -121,11 +121,7 @@
 
     @Override
     public String[] getSupportedActions() {
-        List<? extends GradleActionsProvider> providers = ActionToTaskUtils.actionProviders(project);
-        Set<String> actions = new HashSet<>();
-        for (GradleActionsProvider provider : providers) {
-            actions.addAll(provider.getSupportedActions());
-        }
+        Set<String> actions = new HashSet<>(ActionToTaskUtils.getAllSupportedActions(project));
         // add a fixed 'prime build' action
         actions.add(ActionProvider.COMMAND_PRIME);
         actions.add(COMMAND_DL_SOURCES);
@@ -184,7 +180,7 @@
             LOG.log(Level.FINEST, "Priming build action for {0} is: {1}", new Object[] { project, enabled });
             return enabled;
         }
-        return ActionToTaskUtils.isActionEnabled(command, project, context);
+        return ActionToTaskUtils.isActionEnabled(command, null, project, context);
     }
 
     @NbBundle.Messages({
@@ -259,7 +255,7 @@
             LOG.log(Level.FINE, "Attempt to run a config-disabled action: {0}", action);
             return false;
         }
-        if (!ActionToTaskUtils.isActionEnabled(action, project, context)) {
+        if (!ActionToTaskUtils.isActionEnabled(action, mapping, project, context)) {
             LOG.log(Level.FINE, "Attempt to run action that is not enabled: {0}", action);
             return false;
         }
diff --git a/extide/gradle/src/org/netbeans/modules/gradle/actions/ActionToTaskUtils.java b/extide/gradle/src/org/netbeans/modules/gradle/actions/ActionToTaskUtils.java
index 98c55c6..5c2939d 100644
--- a/extide/gradle/src/org/netbeans/modules/gradle/actions/ActionToTaskUtils.java
+++ b/extide/gradle/src/org/netbeans/modules/gradle/actions/ActionToTaskUtils.java
@@ -23,7 +23,9 @@
 import org.netbeans.modules.gradle.api.execute.ActionMapping;
 import org.netbeans.modules.gradle.spi.actions.GradleActionsProvider;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.api.project.Project;
 import org.netbeans.modules.gradle.api.execute.GradleExecConfiguration;
@@ -47,10 +49,43 @@
         providers.addAll(Lookup.getDefault().lookupAll(GradleActionsProvider.class));
         return providers;
     }
-
-    public static boolean isActionEnabled(String action, Project project, Lookup lookup) {
-        ActionMapping mapping = getActiveMapping(action, project, lookup);
+    
+    public static Set<String> getAllSupportedActions(@NonNull Project project) {
+        Set<String> actions = new HashSet<>();
+        for (GradleActionsProvider provider : actionProviders(project)) {
+            actions.addAll(provider.getSupportedActions());
+        }
+        ProjectActionMappingProvider projectProvider = project.getLookup().lookup(ProjectActionMappingProvider.class);
+        ConfigurableActionProvider contextProvider = project.getLookup().lookup(ConfigurableActionProvider.class);
+        ProjectConfigurationProvider<GradleExecConfiguration> pcp = project.getLookup().lookup(ProjectConfigurationProvider.class);
+        if (contextProvider == null || projectProvider == null) {
+            return actions;
+        }
+        if (pcp == null || contextProvider == null) {
+            actions.addAll(projectProvider.customizedActions());
+        } else {
+            for (GradleExecConfiguration gec : pcp.getConfigurations()) {
+                projectProvider = contextProvider.findActionProvider(gec.getId());
+                if (projectProvider != null) {
+                    actions.addAll(projectProvider.customizedActions());
+                }
+            }
+        }
+        return actions;
+    }
+    
+    public static boolean isCustomMapping(ActionMapping am) {
+        return am.getName().startsWith(ActionMapping.CUSTOM_PREFIX);
+    }
+    
+    public static boolean isActionEnabled(String action, ActionMapping mapping, Project project, Lookup lookup) {
+        if (mapping == null) {
+            mapping = getActiveMapping(action, project, lookup);
+        }
         if (!ActionMapping.isDisabled(mapping)) {
+            if (isCustomMapping(mapping)) {
+                return true;
+            }
             List<? extends GradleActionsProvider> providers = actionProviders(project);
             for (GradleActionsProvider provider : providers) {
                 if (provider.isActionEnabled(action, project, lookup)) {
diff --git a/extide/gradle/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImpl.java b/extide/gradle/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImpl.java
index a488d79..3cdee9e 100644
--- a/extide/gradle/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImpl.java
+++ b/extide/gradle/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImpl.java
@@ -157,12 +157,7 @@
         this.project = project;
         this.projectDirectory = project.getProjectDirectory();
         
-        FileChangeListener wl =  WeakListeners.create(FileChangeListener.class, new FileChangeAdapter() {
-            @Override
-            public void fileDataCreated(FileEvent fe) {
-                actionFileChanged(fe.getFile(), null, false);
-            }
-        }, this.projectDirectory);
+        FileChangeListener wl =  WeakListeners.create(FileChangeListener.class, fcl, this.projectDirectory);
         projectDirectory.addFileChangeListener(wl);
         
         LOG.log(Level.FINER, "Initializing ConfigurableAP for {0}", project);
diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImplTest.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImplTest.java
index d63ecd7..5152e08 100644
--- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImplTest.java
+++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/ConfigurableActionsProviderImplTest.java
@@ -38,10 +38,12 @@
 import org.netbeans.api.project.Project;
 import org.netbeans.api.project.ProjectManager;
 import org.netbeans.api.project.ui.OpenProjects;
+import org.netbeans.modules.gradle.api.execute.ActionMapping;
 import org.netbeans.modules.gradle.api.execute.GradleCommandLine;
 import org.netbeans.modules.gradle.api.execute.GradleExecConfiguration;
 import org.netbeans.modules.gradle.api.execute.RunConfig;
 import org.netbeans.modules.gradle.api.execute.RunUtilsTest;
+import org.netbeans.modules.gradle.customizer.CustomActionMapping;
 import org.netbeans.modules.gradle.execute.ConfigPersistenceUtilsTest;
 import org.netbeans.modules.gradle.execute.GradleExecAccessor;
 import org.netbeans.modules.gradle.execute.GradleExecutor;
@@ -349,4 +351,52 @@
         assertTrue("debug.single is supported for java.distribution / default", Arrays.asList(ap.getSupportedActions()).contains("debug.single"));
         assertTrue("debug.single is enabled for java.distribution / default", ap.isActionEnabled("debug.single", Lookups.singleton(def)));
     }
+    
+    /**
+     * Checks that if a custom action is made/paersisted, it will be visible in
+     * action provider.
+     * @throws IOException 
+     */
+    @Test
+    public void testSaveCustomizedActionVisible() throws Exception {
+        createGradleProject2();
+        
+        CustomActionRegistrationSupport supp = new CustomActionRegistrationSupport(project);
+        CustomActionMapping cam = new CustomActionMapping(ActionMapping.CUSTOM_PREFIX + "1");
+        cam.setArgs("build");
+        
+        ActionProvider ap = project.getLookup().lookup(ActionProvider.class);
+        assertFalse(Arrays.asList(ap.getSupportedActions()).contains(cam.getName()));
+        
+        supp.registerCustomAction(cam);
+        supp.save();
+        
+        assertTrue(Arrays.asList(ap.getSupportedActions()).contains(cam.getName()));
+    }
+    
+    /**
+     * Checks that custom created action is enabled.
+     */
+    @Test
+    public void testCustomizedActionEnabled() throws Exception {
+        createGradleProject2();
+        
+        CustomActionRegistrationSupport supp = new CustomActionRegistrationSupport(project);
+        CustomActionMapping cam = new CustomActionMapping(ActionMapping.CUSTOM_PREFIX + "1");
+        cam.setArgs("build");
+        
+        ActionProvider ap = project.getLookup().lookup(ActionProvider.class);
+        
+        assertFalse("Nonexistent ation must not be enabled", ap.isActionEnabled(cam.getName(), Lookup.EMPTY));
+
+        supp.registerCustomAction(cam);
+        supp.save();
+        
+        assertTrue("Custom actions are always enabled", ap.isActionEnabled(cam.getName(), Lookup.EMPTY));
+        
+        supp.unregisterCustomAction(cam.getName());
+        supp.save();
+        
+        assertFalse("Deleted actions must not be enabled", ap.isActionEnabled(cam.getName(), Lookup.EMPTY));
+    }
 }
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/ASTUtils.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/ASTUtils.java
index 7027aa4..eee0861 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/ASTUtils.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/ASTUtils.java
@@ -300,7 +300,7 @@
                 // how to get only methods from source?
                 // for now, just check line number, if < 0 it is not from source
                 // Second part of condition is for generated accessors
-                if ((!method.isSynthetic() && method.getCode() != null)
+                if ((!method.isSynthetic() && (method.isAbstract() || method.getCode() != null))
                         || (method.isSynthetic() && possibleMethods.contains(method.getName()))) {
                     children.add(method);
                 }
@@ -353,6 +353,46 @@
 
         return offset;
     }
+    
+    /**
+     * Returns a simple name for a class. The result is not defined for local and
+     * anonymous classes and for closures.
+     * @param node the class
+     * @return class' simple name
+     */
+    public static String getSimpleName(ClassNode node) {
+        if (node == null) {
+            return null;
+        }
+        if (node.getOuterClass() == null) {
+            return node.getNameWithoutPackage();
+        } else {
+            String s = node.getName().substring(node.getOuterClass().getName().length());
+            if (s.startsWith("$")) {
+                return s.substring(1);
+            } else {
+                return s;
+            }
+        }
+    }
+
+    /**
+     * Returns class' parent's name. For toplevel classes, returns the package name.
+     * For inner classes, it returns the outer class' name. The result is undefined for
+     * local, anonymous classes or closures.
+     * @param node the class node.
+     * @return parent name.
+     */
+    public static String getClassParentName(ClassNode node) {
+        if (node == null) {
+            return null;
+        }
+        if (node.getOuterClass() == null) {
+            return node.getPackageName();
+        } else {
+            return node.getOuterClass().getName();
+        }
+    }
 
     public static ASTNode getForeignNode(final IndexedElement o) {
 
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/GroovyIndexer.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/GroovyIndexer.java
index 3bbced7..843ddbb 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/GroovyIndexer.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/GroovyIndexer.java
@@ -275,6 +275,7 @@
 
             for (ASTElement child : children) {
                 switch (child.getKind()) {
+                    case INTERFACE:
                     case CLASS:
                         analyzeClass((ASTClass) child);
                         break;
@@ -299,6 +300,10 @@
                     case FIELD:
                         indexField((ASTField) child, document);
                         break;
+                    case INTERFACE:
+                    case CLASS:
+                        analyzeClass((ASTClass) child);
+                        break;
                 }
             }
         }
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/StructureAnalyzer.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/StructureAnalyzer.java
index d25b468..367206e 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/StructureAnalyzer.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/StructureAnalyzer.java
@@ -73,6 +73,7 @@
     private Map<ASTClass, Set<FieldNode>> fields;
     private Map<ASTClass, Set<PropertyNode>> properties;
     private List<ASTMethod> methods;
+    private Map<String, ASTClass> classes = new HashMap<>();
     
     private static final Logger LOG = Logger.getLogger(StructureAnalyzer.class.getName());
 
@@ -170,7 +171,10 @@
             if (node instanceof ClassNode) {
                 ClassNode classNode = (ClassNode) node;
                 ASTClass co = new ASTClass(classNode, classNode.getName());
-
+                classes.put(co.getFqn(), co);
+                if (parent == null && classNode.getOuterClass() != null) {
+                    parent = classes.get(classNode.getOuterClass().getName());
+                }
                 if (parent != null) {
                     parent.addChild(co);
                 } else {
@@ -215,7 +219,9 @@
 
         @SuppressWarnings("unchecked")
         List<ASTNode> list = ASTUtils.children(node);
-
+        
+        // classes are collected from the whole source, but the toplevel classes come
+        // first/earlier than inners.
         for (ASTNode child : list) {
             path.descend(child);
             scan(result, child, path, in, includes, parent);
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/completion/CompletionItem.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/completion/CompletionItem.java
index 71d8fd2..ca59a26 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/completion/CompletionItem.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/completion/CompletionItem.java
@@ -538,6 +538,12 @@
         public ElementKind getKind() {
             return ElementKind.METHOD;
         }
+
+        @Override
+        public int getSortPrioOverride() {
+            // sort meta-methods after normal ones, but before keywords
+            return 550;
+        }
         
         // accessed by CompletionAccessor
         List<MethodParameter> getParameters() {
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTClass.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTClass.java
index 2ad7cff..637dd9d 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTClass.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTClass.java
@@ -21,6 +21,7 @@
 
 import org.codehaus.groovy.ast.ClassNode;
 import org.netbeans.modules.csl.api.ElementKind;
+import org.netbeans.modules.groovy.editor.api.ASTUtils;
 import org.netbeans.modules.groovy.editor.api.elements.common.ClassElement;
 
 public class ASTClass extends ASTElement implements ClassElement {
@@ -29,14 +30,14 @@
 
 
     public ASTClass(ClassNode node, String fqn) {
-        super(node, node.getPackageName(), node.getNameWithoutPackage());
+        super(node, ASTUtils.getClassParentName(node), ASTUtils.getSimpleName(node));
         if (fqn != null) {
             this.fqn = fqn;
         } else {
             this.fqn = getName();
         }
     }
-
+    
     @Override
     public String getFqn() {
         return fqn;
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTField.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTField.java
index 7df4dd7..cefb374 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTField.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTField.java
@@ -23,6 +23,7 @@
 import org.codehaus.groovy.ast.FieldNode;
 import org.netbeans.modules.csl.api.ElementKind;
 import org.netbeans.modules.csl.api.Modifier;
+import org.netbeans.modules.groovy.editor.api.ASTUtils;
 
 public final class ASTField extends ASTElement {
 
@@ -33,7 +34,7 @@
     public ASTField(FieldNode node, String in, boolean isProperty) {
         super(node, in, node.getName());
         this.isProperty = isProperty;
-        this.fieldType = node.getType().getNameWithoutPackage();
+        this.fieldType = ASTUtils.getSimpleName(node.getType());
     }
 
     @Override
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTMethod.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTMethod.java
index 884aa4f..72c4583 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTMethod.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/elements/ast/ASTMethod.java
@@ -22,10 +22,12 @@
 import java.util.ArrayList;
 import java.util.List;
 import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.ConstructorNode;
 import org.codehaus.groovy.ast.MethodNode;
 import org.codehaus.groovy.ast.Parameter;
 import org.netbeans.modules.csl.api.ElementKind;
+import org.netbeans.modules.groovy.editor.api.ASTUtils;
 import org.netbeans.modules.groovy.editor.api.elements.common.MethodElement;
 
 public class ASTMethod extends ASTElement implements MethodElement {
@@ -74,7 +76,7 @@
             for (Parameter parameter : ((MethodNode) node).getParameters()) {
                 String paramName = parameter.getName();
                 String fqnType = parameter.getType().getName();
-                String type = parameter.getType().getNameWithoutPackage();
+                String type = ASTUtils.getSimpleName(parameter.getType());
 
                 parameters.add(new MethodParameter(fqnType, type, paramName));
             }
@@ -115,7 +117,7 @@
     public String getName() {
         if (name == null) {
             if (node instanceof ConstructorNode) {
-                name = ((ConstructorNode) node).getDeclaringClass().getNameWithoutPackage();
+                name = ASTUtils.getSimpleName(((ConstructorNode) node).getDeclaringClass());
             } else if (node instanceof MethodNode) {
                 name = ((MethodNode) node).getName();
             }
@@ -130,7 +132,7 @@
     @Override
     public String getReturnType() {
         if (returnType == null) {
-            returnType = ((MethodNode) node).getReturnType().getNameWithoutPackage();
+            returnType = ASTUtils.getSimpleName(((MethodNode) node).getReturnType());
         }
         return returnType;
     }
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/MethodInference.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/MethodInference.java
index 1ee0e8d..b963917 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/MethodInference.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/MethodInference.java
@@ -19,8 +19,10 @@
 
 package org.netbeans.modules.groovy.editor.completion.inference;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Queue;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
@@ -70,7 +72,7 @@
 
             ClassNode callerType = findCallerType(methodCall.getObjectExpression(), path, baseDocument, offset);
             if (callerType != null) {
-                return findReturnTypeFor(callerType, methodCall.getMethodAsString(), methodCall.getArguments(), path, false, baseDocument, offset);
+                return findReturnTypeFor(callerType.redirect(), methodCall.getMethodAsString(), methodCall.getArguments(), path, false, baseDocument, offset);
             }
         }
 
@@ -93,7 +95,7 @@
         if (expression instanceof StaticMethodCallExpression) {
             StaticMethodCallExpression staticMethodCall = (StaticMethodCallExpression) expression;
 
-            return findReturnTypeFor(staticMethodCall.getOwnerType(), staticMethodCall.getMethod(), staticMethodCall.getArguments(), path, true, baseDocument, offset);
+            return findReturnTypeFor(staticMethodCall.getOwnerType().redirect(), staticMethodCall.getMethod(), staticMethodCall.getArguments(), path, true, baseDocument, offset);
         }
         return null;
     }
@@ -141,7 +143,7 @@
                 }
             }
         }
-
+        
         MethodNode possibleMethod = tryFindPossibleMethod(callerType, methodName, paramTypes, isStatic);
         if (possibleMethod != null) {
             return possibleMethod.getReturnType();
@@ -154,7 +156,13 @@
 
         MethodNode res = null;
         ClassNode node = callerType;
-        do {
+        Queue<ClassNode> tq = new ArrayDeque<>();
+        tq.add(callerType.redirect());
+        while ((node = tq.poll()) != null) {
+            for (ClassNode in : node.getInterfaces()) {
+                // search also in interfaces
+                tq.add(in.redirect());
+            }
             for (MethodNode method : node.getMethods(methodName)) {
                 if (isStatic && !method.isStatic()) {
                     continue;
@@ -194,7 +202,10 @@
                 }
             }
             node = node.getSuperClass();
-        } while (node != null);
+            if (node != null) {
+                tq.add(node.redirect());
+            }
+        };
 
         return res;
     }
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/TypeInferenceVisitor.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/TypeInferenceVisitor.java
index 99573f9..01d3c51 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/TypeInferenceVisitor.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/completion/inference/TypeInferenceVisitor.java
@@ -20,6 +20,7 @@
 package org.netbeans.modules.groovy.editor.completion.inference;
 
 import groovy.lang.Range;
+import java.util.Iterator;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
@@ -38,6 +39,8 @@
 import org.codehaus.groovy.ast.expr.RangeExpression;
 import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.syntax.Types;
 import org.netbeans.editor.BaseDocument;
@@ -112,46 +115,86 @@
     @Override
     public void visitDeclarationExpression(DeclarationExpression expression) {
         if (sameVariableName(leaf, expression.getLeftExpression())) {
-            guessedType = deriveExpressonType(expression.getRightExpression());
+            ClassNode fromExpression = deriveExpressonType(expression.getRightExpression());
+            if (fromExpression != null) {
+                guessedType = fromExpression;
+            }
+        }
+    }
+    
+    private ASTNode leafStatement;
+
+    /**
+     * After each statement certify that the leaf / completion point was not 
+     * passed.
+     */
+    @Override
+    protected void visitStatement(Statement statement) {
+        super.visitStatement(statement);
+        if (statement == leafStatement) {
+            leafReached = true;
         }
     }
 
     @Override
+    public void visitBlockStatement(BlockStatement statement) {
+        ASTNode prev = null;
+        for (Iterator<ASTNode> it = path.iterator(); it.hasNext(); ) {
+            ASTNode n = it.next();
+            if (n == statement) {
+                leafStatement = prev;
+                break;
+            }
+            prev = n;
+        }
+        super.visitBlockStatement(statement);
+    }
+    
+    @Override
     public void visitVariableExpression(VariableExpression expression) {
-            if (expression.isSuperExpression()) {
-                guessedType = expression.getType().getSuperClass();
-            }
-            if (null != expression.getAccessedVariable()) {
-                Variable accessedVariable = expression.getAccessedVariable();
+        boolean guessed = true;
+        if (leaf instanceof VariableExpression && !sameVariableName(leaf, expression)) {
+            return;
+        }
+        if (expression.isSuperExpression()) {
+            guessedType = expression.getType().getSuperClass();
+        }
+        if (null != expression.getAccessedVariable()) {
+            Variable accessedVariable = expression.getAccessedVariable();
 
-                if (accessedVariable.hasInitialExpression()) {
-                    Expression initialExpression = expression.getAccessedVariable().getInitialExpression();
-                    if (initialExpression instanceof ConstantExpression
-                            && !initialExpression.getText().equals("null")) { // NOI18N
-                        guessedType = ((ConstantExpression) initialExpression).getType();
-                    } else if (initialExpression instanceof ConstructorCallExpression) {
-                        guessedType = ClassHelper.make(((ConstructorCallExpression) initialExpression).getType().getName());
-                    } else if (initialExpression instanceof MethodCallExpression) {
-                        int newOffset = ASTUtils.getOffset(doc, initialExpression.getLineNumber(), initialExpression.getColumnNumber());
-                        AstPath newPath = new AstPath(path.root(), newOffset, doc);
-                        guessedType = MethodInference.findCallerType(initialExpression, newPath, doc, newOffset);
-                    } else if (initialExpression instanceof ListExpression) {
-                        guessedType = ((ListExpression) initialExpression).getType();
-                    } else if (initialExpression instanceof MapExpression) {
-                        guessedType = ((MapExpression) initialExpression).getType();
-                    } else if (initialExpression instanceof RangeExpression) {
-                        // this should work, but the type is Object - nut sure why
-                        // guessedType = ((RangeExpression)initialExpression).getType();
-                        guessedType = ClassHelper.makeWithoutCaching(Range.class, true);                
-                    }
-                } else if (accessedVariable instanceof Parameter) {
-                    Parameter param = (Parameter) accessedVariable;
-                    guessedType = param.getType();
+            if (accessedVariable.hasInitialExpression()) {
+                Expression initialExpression = expression.getAccessedVariable().getInitialExpression();
+                if (initialExpression instanceof ConstantExpression
+                        && !initialExpression.getText().equals("null")) { // NOI18N
+                    guessedType = ((ConstantExpression) initialExpression).getType();
+                } else if (initialExpression instanceof ConstructorCallExpression) {
+                    guessedType = ClassHelper.make(((ConstructorCallExpression) initialExpression).getType().getName());
+                } else if (initialExpression instanceof MethodCallExpression) {
+                    int newOffset = ASTUtils.getOffset(doc, initialExpression.getLineNumber(), initialExpression.getColumnNumber());
+                    AstPath newPath = new AstPath(path.root(), newOffset, doc);
+                    guessedType = MethodInference.findCallerType(initialExpression, newPath, doc, newOffset);
+                } else if (initialExpression instanceof ListExpression) {
+                    guessedType = ((ListExpression) initialExpression).getType();
+                } else if (initialExpression instanceof MapExpression) {
+                    guessedType = ((MapExpression) initialExpression).getType();
+                } else if (initialExpression instanceof RangeExpression) {
+                    // this should work, but the type is Object - nut sure why
+                    // guessedType = ((RangeExpression)initialExpression).getType();
+                    guessedType = ClassHelper.makeWithoutCaching(Range.class, true);                
+                } else {
+                    guessed = false;
                 }
-            } else if (!expression.getType().getName().equals("java.lang.Object")) {
-                guessedType = expression.getType();
-
+            } else if (accessedVariable instanceof Parameter) {
+                Parameter param = (Parameter) accessedVariable;
+                guessedType = param.getType();
+            } else {
+                guessed = false;
             }
+        } 
+        if (!guessed && !expression.getType().getName().equals("java.lang.Object")) {
+            guessedType = expression.getType();
+
+        }
         super.visitVariableExpression(expression);
     }
 
@@ -172,6 +215,11 @@
                         guessedType = MethodInference.findCallerType(rightExpression, path, doc, cursorOffset);
                     } else if (rightExpression instanceof StaticMethodCallExpression) {
                         guessedType = MethodInference.findCallerType(rightExpression, path, doc, cursorOffset);
+                    } else {
+                        ClassNode cn = expression.getRightExpression().getType();
+                        if (!cn.equals("java.lang.Object")) {
+                            guessedType = cn;
+                        }
                     }
                 }
             }
diff --git a/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy b/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy
new file mode 100644
index 0000000..dad9bfb
--- /dev/null
+++ b/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy
@@ -0,0 +1,7 @@
+def reassignment() {
+    def varA = "ahoj"
+    def sub = varA.substring(0)
+
+    varA = ["a", "b"];
+    varA.lis
+}
diff --git a/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy.testReassignment_1.completion b/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy.testReassignment_1.completion
new file mode 100644
index 0000000..39ab57d
--- /dev/null
+++ b/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy.testReassignment_1.completion
@@ -0,0 +1,7 @@
+Code completion result for source line:
+def sub = varA.subs|tring(0)
+(QueryType=COMPLETION, prefixSearch=true, caseSensitive=true)
+------------------------------------
+METHOD     subSequence(int, int)           [PUBLIC]   CharSequence
+METHOD     substring(int)                  [PUBLIC]   String
+METHOD     substring(int, int)             [PUBLIC]   String
diff --git a/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy.testReassignment_2.completion b/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy.testReassignment_2.completion
new file mode 100644
index 0000000..a9cb8a7
--- /dev/null
+++ b/groovy/groovy.editor/test/unit/data/testfiles/completion/flow/reassignment/Reassignment.groovy.testReassignment_2.completion
@@ -0,0 +1,6 @@
+Code completion result for source line:
+varA.lis|
+(QueryType=COMPLETION, prefixSearch=true, caseSensitive=true)
+------------------------------------
+METHOD     listIterator()                  [PUBLIC]   ListIterator<E>
+METHOD     listIterator(int)               [PUBLIC]   ListIterator<E>
diff --git a/groovy/groovy.editor/test/unit/data/testfiles/completion/method/methods4/Methods4.groovy b/groovy/groovy.editor/test/unit/data/testfiles/completion/method/methods4/Methods4.groovy
new file mode 100644
index 0000000..a682401
--- /dev/null
+++ b/groovy/groovy.editor/test/unit/data/testfiles/completion/method/methods4/Methods4.groovy
@@ -0,0 +1,21 @@
+class GroovyClass1 {
+    def method1() {}
+    def method2() {}
+    def method3() {}
+    
+    interface ISuper {
+        def methodSuper();
+    }
+    
+    interface I1 extends ISuper {
+        def methodA();
+    }
+}
+
+class GroovyClass2 {
+    def m2() {
+        GroovyClass1.I1 iface;
+        iface.meth
+
+    }
+}
diff --git a/groovy/groovy.editor/test/unit/data/testfiles/completion/method/methods4/Methods4.groovy.testMethods4.completion b/groovy/groovy.editor/test/unit/data/testfiles/completion/method/methods4/Methods4.groovy.testMethods4.completion
new file mode 100644
index 0000000..e8c482f
--- /dev/null
+++ b/groovy/groovy.editor/test/unit/data/testfiles/completion/method/methods4/Methods4.groovy.testMethods4.completion
@@ -0,0 +1,6 @@
+Code completion result for source line:
+iface.meth|
+(QueryType=COMPLETION, prefixSearch=true, caseSensitive=true)
+------------------------------------
+METHOD     methodA()                       [PUBLIC]   Object
+METHOD     methodSuper()                   [PUBLIC]   Object
diff --git a/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/FlowCCTest.java b/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/FlowCCTest.java
index d87f2e3..31abe63 100644
--- a/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/FlowCCTest.java
+++ b/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/FlowCCTest.java
@@ -86,4 +86,12 @@
         checkCompletion(BASE + "CollectionLiterals2.groovy", "map.ent^", false);
     }
     
+    public void testReassignment_1() throws Exception {
+        checkCompletion(BASE + "Reassignment.groovy", "def sub = varA.subs^", false);
+    }
+
+    public void testReassignment_2() throws Exception {
+        checkCompletion(BASE + "Reassignment.groovy", "varA.lis^", false);
+    }
+    
 }
diff --git a/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/MethodCCTest.java b/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/MethodCCTest.java
index 02f1b2a..c3226af 100644
--- a/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/MethodCCTest.java
+++ b/groovy/groovy.editor/test/unit/src/org/netbeans/modules/groovy/editor/api/completion/MethodCCTest.java
@@ -149,5 +149,13 @@
     public void testCompletionNoPrefixString2() throws Exception {
         checkCompletion(BASE + "CompletionNoPrefixString2.groovy", "def name='Petr'.^", false);
     }
+    
+    /**
+     * Checks that the completion contains methods from inner interfaces and their
+     * superinterfaces.
+     */
+    public void testMethods4() throws Exception {
+        checkCompletion(BASE + "Methods4.groovy", "iface.meth^", false);
+    }
 }
 
diff --git a/ide/extexecution/apichanges.xml b/ide/extexecution/apichanges.xml
index 5ff14c9..3801d44 100644
--- a/ide/extexecution/apichanges.xml
+++ b/ide/extexecution/apichanges.xml
@@ -91,7 +91,23 @@
     <!-- ACTUAL CHANGES BEGIN HERE: -->
 
     <changes>
-
+        <change>
+            <api name="extexecution_api"/>
+            <summary>StartupExtender may return unescaped parameters</summary>
+            <version major="1" minor="62"/>
+            <date day="10" month="8" year="2021"/>
+            <author login="sdedic"/>
+            <compatibility addition="yes"/>
+            <description>
+                The <a href="@TOP@/org/netbeans/spi/extexecution/startup/StartupExtenderImplementation.html">StartupExtenderImplementation</a> 
+                javadoc did not specify how to handle parameters with spaces in them. Some of the implementations quote/escape the parameters they 
+                return, but most of them not. This causes issues later when the parameters are processed or re-arranged into the final 
+                commandline. This change allows a <a href="@TOP@/org/netbeans/spi/extexecution/startup/StartupExtenderImplementation.html">StartupExtenderImplementation</a>
+                to declare its quoting policy.
+            </description>
+            <class package="org.netbeans.spi.extexecution.startup" name="StartupExtenderImplementation"/>
+            <class package="org.netbeans.api.extexecution.startup" name="StartupExtender"/>
+        </change>
         <change>
             <api name="extexecution_api"/>
             <summary>postExecution callback with exit code value</summary>
diff --git a/ide/extexecution/src/org/netbeans/api/extexecution/startup/StartupExtender.java b/ide/extexecution/src/org/netbeans/api/extexecution/startup/StartupExtender.java
index 24a2f6c..8256479 100644
--- a/ide/extexecution/src/org/netbeans/api/extexecution/startup/StartupExtender.java
+++ b/ide/extexecution/src/org/netbeans/api/extexecution/startup/StartupExtender.java
@@ -23,8 +23,10 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.modules.extexecution.startup.StartupExtenderRegistrationOptions;
 import org.netbeans.modules.extexecution.startup.StartupExtenderRegistrationProcessor;
 import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.openide.util.BaseUtilities;
 import org.openide.util.Lookup;
 import org.openide.util.NbBundle;
 import org.openide.util.Parameters;
@@ -45,10 +47,13 @@
     private final String description;
 
     private final List<String> arguments;
+    
+    private final List<String> rawArguments;
 
-    private StartupExtender(String description, List<String> arguments) {
+    private StartupExtender(String description, List<String> arguments, List<String> rawArguments) {
         this.description = description;
         this.arguments = arguments;
+        this.rawArguments = rawArguments;
     }
 
     /**
@@ -81,8 +86,30 @@
 
         List<StartupExtender> res = new ArrayList<StartupExtender>();
         for (Lookup.Item<StartupExtenderImplementation> item : lkp.lookupResult(StartupExtenderImplementation.class).allItems()) {
-            StartupExtender extender = new StartupExtender(item.getDisplayName(),
-                                       item.getInstance().getArguments(context, mode));
+            StartupExtenderImplementation impl = item.getInstance();
+            List<String> args = impl.getArguments(context, mode);
+            List<String> rawArgs;
+            
+            if (!(impl instanceof StartupExtenderRegistrationOptions) ||
+                ((StartupExtenderRegistrationOptions)impl).argumentsQuoted()) {
+                rawArgs = new ArrayList<>(args.size());
+                for (String s : args) {
+                    String[] parsed = BaseUtilities.parseParameters(s);
+                    rawArgs.add(String.join(" ", parsed));
+                }
+            } else {
+                rawArgs = args;
+                List<String> quotedArgs = new ArrayList<>();
+                for (String s : args) {
+                    if (s.isEmpty()) {
+                        quotedArgs.add(s);
+                    } else {
+                        quotedArgs.add(BaseUtilities.escapeParameters(new String[] { s }));
+                    }
+                }
+                args = quotedArgs;
+            }
+            StartupExtender extender = new StartupExtender(item.getDisplayName(), args, rawArgs);
             LOG.log(Level.FINE, " {0} => {1}", new Object[] {extender.description, extender.getArguments()});
             res.add(extender);
         }
@@ -108,6 +135,19 @@
     public List<String> getArguments() {
         return arguments;
     }
+    
+    /**
+     * List of arguments. Items of the list are literal values that should
+     * be used by the process, without escaping. They can contain spaces, and any
+     * quote, doublequote or backslashes in their literal meaning. It is up to the
+     * caller to appropriately quote or escape the values.
+     * 
+     * @return list of arguments.
+     * @since 1.62
+     */
+    public List<String> getRawArguments() {
+        return rawArguments;
+    }
 
     /**
      * Class representing the startup mode of the process.
diff --git a/ide/extexecution/src/org/netbeans/modules/extexecution/startup/ProxyStartupExtender.java b/ide/extexecution/src/org/netbeans/modules/extexecution/startup/ProxyStartupExtender.java
index 101660f..e246456 100644
--- a/ide/extexecution/src/org/netbeans/modules/extexecution/startup/ProxyStartupExtender.java
+++ b/ide/extexecution/src/org/netbeans/modules/extexecution/startup/ProxyStartupExtender.java
@@ -80,4 +80,22 @@
             return delegate;
         }
     }
+    
+    /**
+     * Returns false only if the extender registration explicitly says it does not escape arguments.
+     */
+    public boolean argumentsQuoted() {
+        Object o = attributes.get(StartupExtenderRegistrationProcessor.QUOTED_ATTRIBUTE);
+        // older binaries do not contain the attribute, so fall back to 'escaped' mode.
+        return o == null || o != Boolean.FALSE;
+    }
+
+    /**
+     * Enhanced version that declares escaping policy from 1.62
+     */
+    public static class V2 extends ProxyStartupExtender implements StartupExtenderRegistrationOptions {
+        public V2(Map<String, ?> attributes) {
+            super(attributes);
+        }
+    }
 }
diff --git a/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationOptions.java b/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationOptions.java
new file mode 100644
index 0000000..29aeb05
--- /dev/null
+++ b/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationOptions.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.extexecution.startup;
+
+/**
+ * Interface internal to the module connecting {@link StartupExtender} registrations
+ * and API.
+ * @author sdedic
+ */
+public interface StartupExtenderRegistrationOptions {
+    public boolean argumentsQuoted();
+}
diff --git a/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationProcessor.java b/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationProcessor.java
index 52906fd..87f495f 100644
--- a/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationProcessor.java
+++ b/ide/extexecution/src/org/netbeans/modules/extexecution/startup/StartupExtenderRegistrationProcessor.java
@@ -44,6 +44,8 @@
     public static final String DELEGATE_ATTRIBUTE = "delegate"; // NOI18N
 
     public static final String START_MODE_ATTRIBUTE = "startMode"; // NOI18N
+    
+    public static final String QUOTED_ATTRIBUTE = "argumentsQuoted"; // NOI18N
 
     @Override
     protected boolean handleProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws LayerGenerationException {
@@ -68,8 +70,9 @@
             File f = layer(element).instanceFile(PATH, null)
                     .instanceAttribute(DELEGATE_ATTRIBUTE, StartupExtenderImplementation.class, annotation, null)
                     .stringvalue(START_MODE_ATTRIBUTE, builder.toString())
+                    .boolvalue(QUOTED_ATTRIBUTE, annotation.argumentsQuoted())
                     .bundlevalue("displayName", element.getAnnotation(StartupExtenderImplementation.Registration.class).displayName()) // NOI18N
-                    .methodvalue("instanceCreate", "org.netbeans.spi.extexecution.startup.StartupExtender", "createProxy") // NOI18N
+                    .methodvalue("instanceCreate", "org.netbeans.spi.extexecution.startup.StartupExtender", "createProxy2") // NOI18N
                     .position(element.getAnnotation(StartupExtenderImplementation.Registration.class).position()); // NOI18N
             f.write();
         }
diff --git a/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtender.java b/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtender.java
index 372f1e4..719aa39 100644
--- a/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtender.java
+++ b/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtender.java
@@ -27,10 +27,22 @@
  */
 final class StartupExtender {
 
+    /**
+     * Legacy layer factory that is still used by binaries compiled against &lt 1.62
+     * @param map
+     * @return 
+     */
     static StartupExtenderImplementation createProxy(Map<String,?> map) {
         return new ProxyStartupExtender(map);
     }
 
+    /**
+     * New layer factory, allows to declare quoting/escaping policy
+     */
+    static StartupExtenderImplementation createProxy2(Map<String,?> map) {
+        return new ProxyStartupExtender.V2(map);
+    }
+
     private StartupExtender() {}
 
 }
diff --git a/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtenderImplementation.java b/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtenderImplementation.java
index fcfb231..fb79af0 100644
--- a/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtenderImplementation.java
+++ b/ide/extexecution/src/org/netbeans/spi/extexecution/startup/StartupExtenderImplementation.java
@@ -32,7 +32,12 @@
  * startup. Typically the server plugin implementor or project will query
  * the arguments via API counterpart {@link StartupExtender}. Of course it is
  * not mandatory to use such arguments and there is no way to force it.
- *
+ * <p>
+ * The implementation <b>should not quote or escape parameters</b> it returns. Each item in the
+ * {@link #getArguments(org.openide.util.Lookup, org.netbeans.api.extexecution.startup.StartupExtender.StartMode) returned list}
+ * should be passed as it should be seen by the target process and the API user (launcher) decides on quoting appropriate for the
+ * intended purpose (i.e. to construct a command line, depending on OS). 
+ * 
  * @author Petr Hejl
  * @since 1.30
  * @see StartupExtender
@@ -82,5 +87,15 @@
          */
         int position() default Integer.MAX_VALUE;
 
+        /**
+         * Value {@code false} means the extender leaves escaping or quoting arguments
+         * to the user who constructs the commandline or processes the arguments. To
+         * preserve backwards compatibility, the default value is {@code true}.
+         * <p>
+         * Implementors are <b>strongly encouraged</b> to declare escaping as false.
+         * @return false, if the arguments are not escaped. True otherwise.
+         * @since 1.62
+         */
+        boolean argumentsQuoted() default true;
     }
 }
diff --git a/ide/extexecution/test/unit/src/META-INF/MANIFEST.MF b/ide/extexecution/test/unit/src/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..a010841
--- /dev/null
+++ b/ide/extexecution/test/unit/src/META-INF/MANIFEST.MF
@@ -0,0 +1,8 @@
+Manifest-Version: 1.0
+OpenIDE-Module: org.netbeans.modules.extexecution.xtest/1
+OpenIDE-Module-Specification-Version: 1.0
+OpenIDE-Module-Module-Dependencies: org.netbeans.modules.extexecution/2 > 1.62
+OpenIDE-Module-Short-Description: Tests for ExternalExecution API
+OpenIDE-Module-Layer: org/netbeans/api/extexecution/resources/mf-layer.xml
+OpenIDE-Module-Name: External Execution API tests
+OpenIDE-Module-Public-Packages: -
diff --git a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/resources/mf-layer.xml b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/resources/mf-layer.xml
new file mode 100644
index 0000000..ccb043b
--- /dev/null
+++ b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/resources/mf-layer.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+
+
+<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN"
+                            "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
+<filesystem>
+    <folder name="StartupExtender">
+        <!-- This registration was generated by the @StartupExtenderImplementation.Registration before 
+             1.62. Different instance was created -->
+        <file name="org-netbeans-api-extexecution-startup-LegacyStartupExtender2.instance">
+            <!--org.netbeans.api.extexecution.startup.LegacyStartupExtender-->
+            <attr name="delegate" newvalue="org.netbeans.api.extexecution.startup.LegacyStartupExtender2"/>
+            <attr name="startMode" stringvalue="PROFILE"/>
+            <attr name="displayName" stringvalue="Test Legacy"/>
+            <attr
+                methodvalue="org.netbeans.spi.extexecution.startup.StartupExtender.createProxy" name="instanceCreate"/>
+            <attr intvalue="1500" name="position"/>
+        </file>
+    </folder>
+</filesystem>
diff --git a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/LegacyStartupExtender.java b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/LegacyStartupExtender.java
new file mode 100644
index 0000000..ee9fe34
--- /dev/null
+++ b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/LegacyStartupExtender.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.api.extexecution.startup;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author sdedic
+ */
+@StartupExtenderImplementation.Registration(displayName="Test Legacy", startMode=StartupExtender.StartMode.PROFILE, position = 1000)
+public class LegacyStartupExtender implements StartupExtenderImplementation {
+    public static boolean enable = false;
+    
+    @Override
+    public List<String> getArguments(Lookup context, StartupExtender.StartMode mode) {
+        if (!enable) {
+            return Collections.emptyList();
+        } else {
+            return Arrays.asList("\"Quoted parameter\"", "Normal-parameter");
+        }
+    }
+}
diff --git a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/LegacyStartupExtender2.java b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/LegacyStartupExtender2.java
new file mode 100644
index 0000000..9a3574f
--- /dev/null
+++ b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/LegacyStartupExtender2.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.api.extexecution.startup;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LegacyStartupExtender2 implements StartupExtenderImplementation {
+    public static boolean enable = false;
+    
+    @Override
+    public List<String> getArguments(Lookup context, StartupExtender.StartMode mode) {
+        if (!enable) {
+            return Collections.emptyList();
+        } else {
+            return Arrays.asList("\"Quoted parameter l\"", "Normal-parameter-l");
+        }
+    }
+}
diff --git a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/StartupExtenderTest.java b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/StartupExtenderTest.java
index 72d0887..8a8526c 100644
--- a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/StartupExtenderTest.java
+++ b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/StartupExtenderTest.java
@@ -18,6 +18,8 @@
  */
 package org.netbeans.api.extexecution.startup;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import org.netbeans.junit.NbTestCase;
 import org.netbeans.modules.extexecution.startup.ProxyStartupExtender;
@@ -36,6 +38,13 @@
         super(name);
     }
 
+    @Override
+    protected void tearDown() throws Exception {
+        LegacyStartupExtender.enable = false;
+        LegacyStartupExtender2.enable = false;
+        V2StartupExtender.enable = false;
+    }
+
     public void testLaziness() {
         Lookup lookup = Lookups.forPath(StartupExtenderRegistrationProcessor.PATH);
         assertTrue(lookup.lookup(StartupExtenderImplementation.class) instanceof ProxyStartupExtender);
@@ -46,12 +55,12 @@
 
         List<StartupExtender> argsDebug =
                 StartupExtender.getExtenders(context, StartupExtender.StartMode.DEBUG);
-        assertEquals(1, argsDebug.size());
+        assertEquals(4, argsDebug.size());
         assertTrue(argsDebug.get(0).getArguments().isEmpty());
 
         List<StartupExtender> argsNormal =
                 StartupExtender.getExtenders(context, StartupExtender.StartMode.NORMAL);
-        assertEquals(1, argsNormal.size());
+        assertEquals(4, argsNormal.size());
 
         StartupExtender args = argsNormal.get(0);
         assertEquals("Test", args.getDescription());
@@ -60,4 +69,56 @@
         assertEquals("arg1", args.getArguments().get(0));
         assertEquals("arg2", args.getArguments().get(1));
     }
+    
+    /**
+     * Checks that legacy extender's output is properly unquoted.
+     */
+    public void testRawArguments() {
+        LegacyStartupExtender.enable = true;
+        LegacyStartupExtender2.enable = true;
+        V2StartupExtender.enable = true;
+        
+        List<StartupExtender> argsProfile = StartupExtender.getExtenders(Lookup.EMPTY, StartupExtender.StartMode.PROFILE);
+        assertEquals(4, argsProfile.size());
+        List<String> args = new ArrayList<>();
+        List<String> rawArgs = new ArrayList<>();
+
+        for (StartupExtender e : argsProfile) {
+            rawArgs.addAll(e.getRawArguments());
+            args.addAll(e.getArguments());
+        }
+        
+        // raw arguments do not contain quoting even for extenders that supply quoted args
+        assertEquals(Arrays.asList(
+                "Quoted parameter", "Normal-parameter", 
+                "Quoted parameter l", "Normal-parameter-l",
+                "Quoted parameter V2", "Normal-parameter-v2"
+            ), rawArgs);
+    }
+    
+    /**
+     * Checks that the new extender's output is quoted for backwards compatibility
+     */
+    public void testQuotedArguments() {
+        LegacyStartupExtender.enable = true;
+        LegacyStartupExtender2.enable = true;
+        V2StartupExtender.enable = true;
+        
+        List<StartupExtender> argsProfile = StartupExtender.getExtenders(Lookup.EMPTY, StartupExtender.StartMode.PROFILE);
+        assertEquals(4, argsProfile.size());
+        List<String> args = new ArrayList<>();
+        List<String> rawArgs = new ArrayList<>();
+
+        for (StartupExtender e : argsProfile) {
+            rawArgs.addAll(e.getRawArguments());
+            args.addAll(e.getArguments());
+        }
+        
+        assertEquals(Arrays.asList(
+                "\"Quoted parameter\"", "Normal-parameter", 
+                "\"Quoted parameter l\"", "Normal-parameter-l", 
+                "\"Quoted parameter V2\"", "Normal-parameter-v2"
+            ), args);
+
+    }
 }
diff --git a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/TestStartupExtender.java b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/TestStartupExtender.java
index e41a4d2..2e69afd 100644
--- a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/TestStartupExtender.java
+++ b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/TestStartupExtender.java
@@ -28,7 +28,7 @@
  *
  * @author Petr Hejl
  */
-@StartupExtenderImplementation.Registration(displayName="Test", startMode=StartMode.NORMAL)
+@StartupExtenderImplementation.Registration(displayName="Test", startMode=StartMode.NORMAL, position = 0)
 public class TestStartupExtender implements StartupExtenderImplementation {
 
     @Override
diff --git a/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/V2StartupExtender.java b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/V2StartupExtender.java
new file mode 100644
index 0000000..cff94d7
--- /dev/null
+++ b/ide/extexecution/test/unit/src/org/netbeans/api/extexecution/startup/V2StartupExtender.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.api.extexecution.startup;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author sdedic
+ */
+@StartupExtenderImplementation.Registration(displayName="Test V2", 
+        startMode=StartupExtender.StartMode.PROFILE, argumentsQuoted = false, position = 2000)
+public class V2StartupExtender implements StartupExtenderImplementation {
+    public static boolean enable = false;
+    
+    @Override
+    public List<String> getArguments(Lookup context, StartupExtender.StartMode mode) {
+        if (!enable) {
+            return Collections.emptyList();
+        } else {
+            return Arrays.asList("Quoted parameter V2", "Normal-parameter-v2");
+        }
+    }
+}
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/HtmlJavaPagesNodeFactory.java b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/HtmlJavaPagesNodeFactory.java
new file mode 100644
index 0000000..a139619
--- /dev/null
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/HtmlJavaPagesNodeFactory.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.gradle.htmlui;
+
+import java.awt.Image;
+import org.netbeans.modules.gradle.api.NbGradleProject;
+import org.netbeans.modules.gradle.spi.nodes.AbstractGradleNodeList;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collections;
+import java.util.List;
+import javax.swing.event.ChangeListener;
+import org.netbeans.api.project.Project;
+import org.netbeans.spi.project.ui.support.NodeFactory;
+import org.netbeans.spi.project.ui.support.NodeList;
+import org.openide.filesystems.FileObject;
+import org.openide.loaders.DataFolder;
+import org.openide.nodes.FilterNode;
+import org.openide.nodes.Node;
+import org.openide.util.ImageUtilities;
+import org.openide.util.NbBundle;
+
+@NbBundle.Messages({
+    "MSG_HtmlJavaPages=Frontend UI Pages"
+})
+@NodeFactory.Registration(projectType = NbGradleProject.GRADLE_PROJECT_TYPE, position = 77)
+public final class HtmlJavaPagesNodeFactory implements NodeFactory {
+
+    @Override
+    public NodeList<?> createNodes(Project p) {
+        return new HtmlJavaPagesList(p);
+    }
+
+    private static class HtmlJavaPagesList extends AbstractGradleNodeList<FileObject> implements PropertyChangeListener {
+
+        final Project project;
+
+        HtmlJavaPagesList(Project project) {
+            this.project = project;
+        }
+
+        @Override
+        public List<FileObject> keys() {
+            FileObject pages = project.getProjectDirectory().getFileObject("src/main/webapp/pages"); // NOI18N
+            return pages != null ? Collections.singletonList(pages) : Collections.<FileObject>emptyList();
+        }
+
+        @Override
+        public Node node(FileObject key) {
+            DataFolder df = DataFolder.findFolder(key);
+            FilterNode fn = new FilterNode(df.getNodeDelegate().cloneNode()) {
+                @Override
+                public Image getIcon(int type) {
+                    return ImageUtilities.loadImage("org/netbeans/modules/gradle/htmlui/DukeHTML.png"); // NOI18N
+                }
+
+                @Override
+                public Image getOpenedIcon(int type) {
+                    return getIcon(type);
+                }
+
+                @Override
+                public String getName() {
+                    return "pages"; // NOI18N
+                }
+
+                @Override
+                public String getDisplayName() {
+                    return Bundle.MSG_HtmlJavaPages();
+                }
+            };
+            return fn;
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            if (NbGradleProject.PROP_PROJECT_INFO.equals(evt.getPropertyName())) {
+                fireChange();
+            }
+        }
+
+        @Override
+        public void removeChangeListener(ChangeListener list) {
+            NbGradleProject.removePropertyChangeListener(project, this);
+        }
+
+        @Override
+        public void addChangeListener(ChangeListener list) {
+            NbGradleProject.addPropertyChangeListener(project, this);
+        }
+
+    }
+}
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/desktop_build.gradle.fmk b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/desktop_build.gradle.fmk
index f29b5f6..71b86b0 100644
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/desktop_build.gradle.fmk
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/desktop_build.gradle.fmk
@@ -51,8 +51,8 @@
 </#noparse>
 dependencies {
     implementation commonProject
-    implementation "org.netbeans.html:net.java.html.boot:1.6.1"
-    implementation "com.dukescript.api:javafx.beaninfo:0.5"
+    implementation "org.netbeans.html:net.java.html.boot:1.7.2"
+    implementation "com.dukescript.api:javafx.beaninfo:0.6"
     runtimeOnly "com.dukescript.api:javafx.base:8.60.11"
-    runtimeOnly "org.netbeans.html:net.java.html.boot.fx:1.6.1"
+    runtimeOnly "org.netbeans.html:net.java.html.boot.fx:1.7.2"
 }
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle-wrapper.properties.fmk b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle-wrapper.properties.fmk
deleted file mode 100644
index 46ca126..0000000
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle-wrapper.properties.fmk
+++ /dev/null
@@ -1,25 +0,0 @@
-<#--
-
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-
--->
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle.properties.fmk b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle.properties.fmk
index b5379c3..ebe4ca9 100644
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle.properties.fmk
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/gradle.properties.fmk
@@ -18,9 +18,9 @@
     under the License.
 
 -->
-action.run.args=bck2brwsrShow
+action.run.args=bck2brwsrShow -t
 
 action.debug.args=run --debug-jvm
 
 action.custom-1=Run in Browser
-action.custom-1.args=bck2brwsrShow
+action.custom-1.args=bck2brwsrShow -t
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/layer.xml b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/layer.xml
index cbc65ca..2345148 100644
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/layer.xml
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/layer.xml
@@ -44,13 +44,6 @@
                     <file name="settings.gradle" url="settings.gradle.fmk">
                         <attr name="javax.script.ScriptEngine" stringvalue="freemarker"/>
                     </file>
-                    <folder name="gradle">
-                        <folder name="wrapper">
-                            <file name="gradle-wrapper.properties" url="gradle-wrapper.properties.fmk">
-                                <attr name="javax.script.ScriptEngine" stringvalue="freemarker"/>
-                            </file>
-                        </folder>
-                    </folder>
                     <folder name="app">
                         <file name="build.gradle" url="app_build.gradle.fmk">
                             <attr name="javax.script.ScriptEngine" stringvalue="freemarker"/>
@@ -110,6 +103,7 @@
                                         <attr name="javax.script.ScriptEngine" stringvalue="freemarker"/>
                                     </file>
                                     <file name="index.html" url="src_main_webapp_pages_index.html.fmk">
+                                        <attr name="important" boolvalue="true"/>
                                         <attr name="javax.script.ScriptEngine" stringvalue="freemarker"/>
                                     </file>
                                 </folder>
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/root_build.gradle.fmk b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/root_build.gradle.fmk
index eda3807..2ba3db0 100644
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/root_build.gradle.fmk
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/root_build.gradle.fmk
@@ -18,15 +18,14 @@
     under the License.
 
 -->
-<#noparse>
-plugins {
-    id "me.tatarka.retrolambda" version "3.7.1"
-}
-
 apply plugin: 'java'
-
-group 'com.dukescript.demo'
-version '1.0-SNAPSHOT'
+<#if group?has_content>
+group '${group}'
+</#if>
+<#if version?has_content>
+version '${version}'
+</#if>
+<#noparse>
 
 allprojects {
     repositories {
@@ -38,9 +37,10 @@
 sourceCompatibility = '1.8'
 
 dependencies {
-    implementation "org.netbeans.html:net.java.html.json:1.6.1"
+    implementation "org.netbeans.html:net.java.html.json:1.7.2"
     implementation "com.dukescript.api:javafx.base:8.60.11"
-    implementation "com.dukescript.api:javafx.beaninfo:0.5"
-    runtimeOnly "org.netbeans.html:ko4j:1.6.1"
+    implementation "com.dukescript.api:javafx.beaninfo:0.6"
+    annotationProcessor "com.dukescript.api:javafx.beaninfo:0.6"
+    runtimeOnly "org.netbeans.html:ko4j:1.7.2"
 }
 </#noparse>
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/src_main_java_Demo.fmk b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/src_main_java_Demo.fmk
index dea0ca2..5d96631 100644
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/src_main_java_Demo.fmk
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/src_main_java_Demo.fmk
@@ -30,31 +30,20 @@
 import javafx.collections.FXCollections;
 import static net.java.html.json.Models.applyBindings;
 
-public final class Demo implements FXBeanInfo.Provider {
-    private final StringProperty desc = new SimpleStringProperty(this, "desc", "Buy Milk");
-    private final ListProperty<String> todos = new SimpleListProperty<>(this, "todos", FXCollections.observableArrayList());
-    private final IntegerBinding numTodos = Bindings.createIntegerBinding(todos::size, todos);
+@FXBeanInfo.Generate
+public final class Demo extends DemoBeanInfo {
+    final StringProperty desc = new SimpleStringProperty(this, "desc", "");
+    final ListProperty<String> todos = new SimpleListProperty<>(this, "todos", FXCollections.observableArrayList());
+    final IntegerBinding numTodos = Bindings.createIntegerBinding(todos::size, todos);
 
     void addTodo() {
         todos.getValue().add(desc.getValue());
         desc.setValue("");
     }
 
-    private final FXBeanInfo info = FXBeanInfo.newBuilder(this)
-            .property(desc)
-            .property(todos)
-            .property("numTodos", numTodos)
-            .action("addTodo", this::addTodo)
-            .build();
-
-    @Override
-    public FXBeanInfo getFXBeanInfo() {
-        return info;
-    }
-
     public static void onPageLoad() {
         Demo model = new Demo();
+        model.desc.setValue("Try Java in browser @ " + System.currentTimeMillis());
         applyBindings(model);
     }
-
 }
\ No newline at end of file
diff --git a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/web_build.gradle.fmk b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/web_build.gradle.fmk
index e1dca2d..43ee3ab 100644
--- a/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/web_build.gradle.fmk
+++ b/java/gradle.htmlui/src/org/netbeans/modules/gradle/htmlui/resources/web_build.gradle.fmk
@@ -19,13 +19,11 @@
 
 -->
 buildscript {
-    ext.bck2brwsr_version = '0.31'
-
     repositories {
         mavenCentral()
     }
     dependencies {
-        classpath "org.apidesign.bck2brwsr:bck2brwsr-maven-plugin:$bck2brwsr_version"
+        classpath "org.apidesign.bck2brwsr:bck2brwsr-maven-plugin:0.51"
     }
 }
 
@@ -47,11 +45,6 @@
     runtimeOnly "com.dukescript.api:javafx.base:8.60.11"
 }
 
-configurations.bck2brwsr {
-    exclude group: 'org.jetbrains', module: 'annotations'
-    extendsFrom configurations.runtimeClasspath
-}
-
 bck2brwsrPages.from {
     fileTree("${commonProject.projectDir}/src/main/webapp/pages")
 }
diff --git a/java/gradle.htmlui/test/unit/src/org/netbeans/modules/gradle/htmlui/CreateArchetypeTest.java b/java/gradle.htmlui/test/unit/src/org/netbeans/modules/gradle/htmlui/CreateArchetypeTest.java
index 65a1c6d..87433d3 100644
--- a/java/gradle.htmlui/test/unit/src/org/netbeans/modules/gradle/htmlui/CreateArchetypeTest.java
+++ b/java/gradle.htmlui/test/unit/src/org/netbeans/modules/gradle/htmlui/CreateArchetypeTest.java
@@ -36,10 +36,13 @@
 import org.netbeans.junit.NbTestCase;
 import org.netbeans.modules.gradle.spi.actions.AfterBuildActionHook;
 import org.netbeans.modules.gradle.spi.newproject.TemplateOperation;
+import org.netbeans.spi.project.ActionProgress;
 import org.netbeans.spi.project.ActionProvider;
+import org.netbeans.spi.project.ui.LogicalViewProvider;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.FileUtil;
 import org.openide.filesystems.LocalFileSystem;
+import org.openide.nodes.Node;
 import org.openide.util.lookup.Lookups;
 
 public class CreateArchetypeTest extends NbTestCase {
@@ -118,6 +121,7 @@
         assertTrue(Arrays.asList(actions.getSupportedActions()).contains(ActionProvider.COMMAND_BUILD));
         actions.isActionEnabled(ActionProvider.COMMAND_BUILD, mainPrj.getLookup());
 
+        assertLogicalView(mainPrj);
 
         invokeCommand(actions, ActionProvider.COMMAND_BUILD, mainPrj);
         assertFile("JAR created", dest, "build", "libs", "dest-1.0-SNAPSHOT.jar");
@@ -139,13 +143,41 @@
         assertFile("Main script created", dest, "web", "build", "web", "bck2brwsr.js");
     }
 
+    private void assertLogicalView(Project mainPrj) {
+        LogicalViewProvider lvp = mainPrj.getLookup().lookup(LogicalViewProvider.class);
+        assertNotNull("Logical view found", lvp);
+        Node logicalView = lvp.createLogicalView();
+        final Node[] children = logicalView.getChildren().getNodes(true);
+        for (Node ch : children) {
+            if (ch.getName().equals("pages") && "Frontend UI Pages".equals(ch.getDisplayName())) {
+                FileObject pages = ch.getLookup().lookup(FileObject.class);
+                assertNotNull("Pages node provides FileObject: " + ch, pages);
+                assertNotNull("There is index.html", pages.getFileObject("index.html"));
+                return;
+            }
+        }
+        fail("Cannot find Frontend Pages in " + Arrays.toString(children));
+    }
+
     protected void invokeCommand(ActionProvider actions, String cmd, Project prj) throws IllegalArgumentException, InterruptedException {
         CountDownLatch waiter = new CountDownLatch(1);
-        AfterBuildActionHook notifier = (action, context, res, out) -> {
-            waiter.countDown();
+        boolean[] status = { false, false };
+        ActionProgress ap = new ActionProgress() {
+            @Override
+            protected void started() {
+                status[0] = true;
+            }
+
+            @Override
+            public void finished(boolean success) {
+                status[1] = success;
+                waiter.countDown();
+            }
         };
-        actions.invokeAction(cmd, Lookups.fixed(prj, notifier));
+        actions.invokeAction(cmd, Lookups.fixed(prj, ap));
+        assertTrue("ActionProgress was started", status[0]);
         waiter.await();
+        assertTrue("ActionProgress was successfully finished", status[1]);
     }
 
     private AssertContent assertFile(String msg, FileObject root, String... path) throws IOException {
diff --git a/java/gradle.java/nbproject/project.xml b/java/gradle.java/nbproject/project.xml
index 3ef753b..b8ba6b9 100644
--- a/java/gradle.java/nbproject/project.xml
+++ b/java/gradle.java/nbproject/project.xml
@@ -101,7 +101,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.41.1</specification-version>
+                        <specification-version>1.62</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java b/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java
index a54c8ec..f477056 100644
--- a/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java
+++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/execute/JavaExecTokenProvider.java
@@ -136,7 +136,7 @@
         List<String> extraArgs = new ArrayList<>();
         if (mode != null) {
             for (StartupExtender group : StartupExtender.getExtenders(new AbstractLookup(ic), mode)) {
-                extraArgs.addAll(group.getArguments());
+                extraArgs.addAll(group.getRawArguments());
             }
         }
         
diff --git a/java/java.disco/src/org/netbeans/modules/java/disco/BrowsePanel.java b/java/java.disco/src/org/netbeans/modules/java/disco/BrowsePanel.java
index 744dff4..fa69532 100644
--- a/java/java.disco/src/org/netbeans/modules/java/disco/BrowsePanel.java
+++ b/java/java.disco/src/org/netbeans/modules/java/disco/BrowsePanel.java
@@ -19,26 +19,28 @@
 package org.netbeans.modules.java.disco;
 
 import static org.netbeans.modules.java.disco.SwingWorker2.submit;
+
 import java.io.File;
 import javax.swing.JFileChooser;
 import org.checkerframework.checker.guieffect.qual.UIEffect;
 import org.checkerframework.checker.nullness.qual.NonNull;
+import org.openide.util.NbBundle;
 
+@NbBundle.Messages({
+    "DiscoBrowsePanel.searching=Searching...",
+    "DiscoBrowsePanel.error=Requested JDK not found."
+})
 public class BrowsePanel extends javax.swing.JPanel {
 
+    private final BrowseWizardPanel panel;
     private final WizardState state;
     private final Client discoClient;
 
-    @UIEffect
-    public static BrowsePanel create(WizardState state) {
-        BrowsePanel d = new BrowsePanel(state);
-        d.init();
-        return d;
-    }
-
-    public BrowsePanel(WizardState state) {
+    public BrowsePanel(BrowseWizardPanel panel, WizardState state) {
+        this.panel = panel;
         this.state = state;
         this.discoClient = Client.getInstance();
+        init();
     }
 
     @UIEffect
@@ -53,21 +55,26 @@
     public void addNotify() {
         super.addNotify();
         //we do this every time
-        jdkDescription.setText(state.selection.getFileName());
         if (state.selection.get(null) == null) {
+            jdkDescription.setText(Bundle.DiscoBrowsePanel_searching());
             //OK, we have a quick selection so the file name was not the best, let's try to load it
             submit(() -> {
                 return state.selection.get(discoClient);
             }).then(pkg -> {
                 //re-set the name
                 jdkDescription.setText(state.selection.getFileName());
-
-            }).execute(); //NOTE: ignoring errors on purpose...
+                panel.fireChangeListeners();
+            }).handle(ex -> {
+                jdkDescription.setText(Bundle.DiscoBrowsePanel_error());
+            }).execute();
+        } else {
+            jdkDescription.setText(state.selection.getFileName());
         }
     }
 
     public boolean isOK() {
-        return !downloadPathText.getText().isEmpty();
+        return state.selection.get(null) != null
+                && !downloadPathText.getText().isEmpty();
     }
 
     @NonNull
diff --git a/java/java.disco/src/org/netbeans/modules/java/disco/BrowseWizardPanel.java b/java/java.disco/src/org/netbeans/modules/java/disco/BrowseWizardPanel.java
index 2a080e0..cf4abb4 100644
--- a/java/java.disco/src/org/netbeans/modules/java/disco/BrowseWizardPanel.java
+++ b/java/java.disco/src/org/netbeans/modules/java/disco/BrowseWizardPanel.java
@@ -30,7 +30,7 @@
 
     @Override
     protected BrowsePanel createComponent() {
-        return BrowsePanel.create(state);
+        return new BrowsePanel(this, state);
     }
 
     @Override
diff --git a/java/java.disco/src/org/netbeans/modules/java/disco/FoojayPlatformInstall.java b/java/java.disco/src/org/netbeans/modules/java/disco/FoojayPlatformInstall.java
index 070caf9..8270c9f 100644
--- a/java/java.disco/src/org/netbeans/modules/java/disco/FoojayPlatformInstall.java
+++ b/java/java.disco/src/org/netbeans/modules/java/disco/FoojayPlatformInstall.java
@@ -24,7 +24,7 @@
 import org.openide.util.NbBundle;
 
 @NbBundle.Messages({
-    "DiscoPlatformInstall.displayName=Download OpenJDK (via foojay Disco API)"
+    "DiscoPlatformInstall.displayName=Download OpenJDK (via Foojay.io Disco API)"
 })
 public class FoojayPlatformInstall extends CustomPlatformInstall {
 
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java
index 5f14324..5f9581b 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java
@@ -28,6 +28,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 
@@ -88,6 +89,7 @@
 import org.netbeans.modules.nativeimage.api.debug.NIVariable;
 import org.netbeans.spi.debugger.ui.DebuggingView.DVFrame;
 import org.netbeans.spi.debugger.ui.DebuggingView.DVThread;
+import org.openide.util.RequestProcessor;
 
 /**
  *
@@ -101,6 +103,7 @@
     private final NbDisconnectRequestHandler disconnectRequestHandler = new NbDisconnectRequestHandler();
     private final NbBreakpointsRequestHandler breakpointsRequestHandler = new NbBreakpointsRequestHandler();
     private final NbVariablesRequestHandler variablesRequestHandler = new NbVariablesRequestHandler();
+    private final RequestProcessor evaluationRP = new RequestProcessor(NbProtocolServer.class.getName(), 3);
     private boolean initialized = false;
     private Future<Void> runningServer;
 
@@ -458,7 +461,7 @@
                 evaluateNative(niDebugger, expression, threadId, response);
             }
             return response;
-        });
+        }, evaluationRP);
     }
 
     private void evaluateJPDA(JPDADebugger debugger, String expression, int threadId, EvaluateResponse response) {
@@ -487,7 +490,7 @@
                 } catch (InvalidExpressionException ex) {
                     toString = variable.getValue();
                 }
-                response.setResult(toString);
+                response.setResult(Objects.toString(toString));
                 response.setVariablesReference(referenceId);
                 response.setType(variable.getType());
                 response.setIndexedVariables(Math.max(indexedVariables, 0));
diff --git a/java/java.source.ant/nbproject/project.xml b/java/java.source.ant/nbproject/project.xml
index ad46ca5..94db0b6 100644
--- a/java/java.source.ant/nbproject/project.xml
+++ b/java/java.source.ant/nbproject/project.xml
@@ -226,6 +226,11 @@
                         <test/>
                     </test-dependency>
                     <test-dependency>
+                        <code-name-base>org.netbeans.modules.java.source.base</code-name-base>
+                        <compile-dependency/>
+                        <test/>
+                    </test-dependency>
+                    <test-dependency>
                         <code-name-base>org.netbeans.modules.nbjunit</code-name-base>
                         <recursive/>
                         <compile-dependency/>
diff --git a/java/java.source.ant/test/unit/src/org/netbeans/modules/java/source/ant/ProjectRunnerImplTest.java b/java/java.source.ant/test/unit/src/org/netbeans/modules/java/source/ant/ProjectRunnerImplTest.java
index e80b70d..3810016 100644
--- a/java/java.source.ant/test/unit/src/org/netbeans/modules/java/source/ant/ProjectRunnerImplTest.java
+++ b/java/java.source.ant/test/unit/src/org/netbeans/modules/java/source/ant/ProjectRunnerImplTest.java
@@ -53,6 +53,8 @@
 import org.netbeans.api.java.project.runner.JavaRunner;
 import org.netbeans.junit.MockServices;
 import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.netbeans.spi.queries.FileEncodingQueryImplementation;
+import org.openide.util.lookup.ServiceProvider;
 
 /**
  *
@@ -259,4 +261,11 @@
         }
 
     }
+
+    @ServiceProvider(service=FileEncodingQueryImplementation.class)
+    public static final class FEQImpl extends FileEncodingQueryImplementation {
+        @Override public Charset getEncoding(FileObject file) {
+            return Charset.forName("UTF-8");
+        }
+    }
 }
diff --git a/java/java.testrunner.ui/src/org/netbeans/modules/java/testrunner/ui/ComputeTestMethodsImpl.java b/java/java.testrunner.ui/src/org/netbeans/modules/java/testrunner/ui/ComputeTestMethodsImpl.java
index 4b2bd43..6236049 100644
--- a/java/java.testrunner.ui/src/org/netbeans/modules/java/testrunner/ui/ComputeTestMethodsImpl.java
+++ b/java/java.testrunner.ui/src/org/netbeans/modules/java/testrunner/ui/ComputeTestMethodsImpl.java
@@ -36,6 +36,7 @@
 import org.netbeans.modules.parsing.spi.TaskIndexingMode;
 import org.openide.filesystems.FileObject;
 import org.openide.util.Lookup;
+import org.openide.util.RequestProcessor;
 import org.openide.util.lookup.ServiceProvider;
 
 /**
@@ -45,6 +46,8 @@
 @ServiceProvider(service=JavaSourceTaskFactory.class)
 public class ComputeTestMethodsImpl extends EditorAwareJavaSourceTaskFactory {
 
+    private static final RequestProcessor WORKER = new RequestProcessor(ComputeTestMethodsImpl.class.getName(), 1, false, false);
+
     public ComputeTestMethodsImpl() {
         super(Phase.ELEMENTS_RESOLVED, Priority.NORMAL, TaskIndexingMode.ALLOWED_DURING_SCAN);
     }
@@ -100,7 +103,7 @@
                 }
 
                 if (!cancel.get()) {
-                    TestMethodController.setTestMethods(doc, methods);
+                    WORKER.post(() -> TestMethodController.setTestMethods(doc, methods));
                 }
             }
         }
diff --git a/java/jshell.support/nbproject/project.xml b/java/jshell.support/nbproject/project.xml
index 0905707..41709af 100644
--- a/java/jshell.support/nbproject/project.xml
+++ b/java/jshell.support/nbproject/project.xml
@@ -245,7 +245,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.48</specification-version>
+                        <specification-version>1.62</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/jshell.support/src/org/netbeans/modules/jshell/j2se/JShellStartupExtender.java b/java/jshell.support/src/org/netbeans/modules/jshell/j2se/JShellStartupExtender.java
index 0117231..2589ba9 100644
--- a/java/jshell.support/src/org/netbeans/modules/jshell/j2se/JShellStartupExtender.java
+++ b/java/jshell.support/src/org/netbeans/modules/jshell/j2se/JShellStartupExtender.java
@@ -40,10 +40,8 @@
  *
  * @author sdedic
  */
-@StartupExtenderImplementation.Registration(displayName = "Java Shell", position = 10000, startMode = {
-    StartMode.DEBUG,
-    StartMode.NORMAL,
-})
+@StartupExtenderImplementation.Registration(displayName = "Java Shell", position = 10000, argumentsQuoted = false,
+    startMode = { StartMode.DEBUG, StartMode.NORMAL })
 public class JShellStartupExtender implements StartupExtenderImplementation {
     private static final Logger LOG = Logger.getLogger(JShellStartupExtender.class.getName());
     
@@ -77,9 +75,7 @@
         
         J2SEPropertyEvaluator  prjEval = p.getLookup().lookup(J2SEPropertyEvaluator.class);
         JavaPlatform platform = ShellProjectUtils.findPlatform(p);
-        List<String> args = ShellProjectUtils.quoteCmdArgs(
-                ShellLaunchManager.buildLocalJVMAgentArgs(platform, agent, prjEval.evaluator()::getProperty)
-        );
+        List<String> args = ShellLaunchManager.buildLocalJVMAgentArgs(platform, agent, prjEval.evaluator()::getProperty);
 
         args.addAll(ShellProjectUtils.launchVMOptions(p));
 
diff --git a/java/maven/nbproject/project.xml b/java/maven/nbproject/project.xml
index 0dfb9ca..9d105e2 100644
--- a/java/maven/nbproject/project.xml
+++ b/java/maven/nbproject/project.xml
@@ -131,7 +131,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.30</specification-version>
+                        <specification-version>1.62</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java
index 4f07e4c..d303f7f 100644
--- a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java
+++ b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java
@@ -118,7 +118,7 @@
  * The example will <b>append</b> <code>-DvmArg2=2</code> to VM arguments and <b>replaces</b> all user
  * program arguments with <code>"paramY"</code>. Append mode can be controlled using {@link ExplicitProcessParameters.Builder#appendArgs} or
  * {@link ExplicitProcessParameters.Builder#appendPriorityArgs}.
- * 
+ *
  * @author  Milos Kleint (mkleint@codehaus.org)
  * @author  Svata Dedic (svatopluk.dedic@gmail.com)
  */
@@ -128,18 +128,18 @@
     static final String ENV_JAVAHOME = "Env.JAVA_HOME"; //NOI18N
 
     private static final String KEY_UUID = "NB_EXEC_MAVEN_PROCESS_UUID"; //NOI18N
-    
+
     private static final String NETBEANS_MAVEN_COMMAND_LINE = "NETBEANS_MAVEN_COMMAND_LINE"; //NOI18N
-    
+
     private Process process;
     private String processUUID;
     private Process preProcess;
     private String preProcessUUID;
     private static final SpecificationVersion VER17 = new SpecificationVersion("1.7"); //NOI18N
     private static final Logger LOGGER = Logger.getLogger(MavenCommandLineExecutor.class.getName());
-    
+
     private static final RequestProcessor RP = new RequestProcessor(MavenCommandLineExecutor.class.getName(),1);
-    
+
     private final static RequestProcessor UPDATE_INDEX_RP = new RequestProcessor(RunUtils.class.getName(), 5);
     /**
      * Execute maven build in NetBeans execution engine.
@@ -158,7 +158,7 @@
         }
         return runner.execute(config, io, tc);
     }
-    
+
     /**
      * Hooks for tests to mock the Maven execution.
      */
@@ -192,12 +192,12 @@
             return task;
         }
     }
-    
+
     public MavenCommandLineExecutor(RunConfig conf, InputOutput io, TabContext tc) {
         super(conf, tc);
         this.io = io;
     }
-    
+
     /**
      * not to be called directly.. use execute();
      */
@@ -222,7 +222,7 @@
         }
         int executionresult = -10;
         final InputOutput ioput = getInputOutput();
-        
+
         final ProgressHandle handle = ProgressHandle.createHandle(clonedConfig.getTaskDisplayName(), this, new AbstractAction() {
             @Override
             public void actionPerformed(ActionEvent e) {
@@ -246,7 +246,7 @@
                 }
                 if (clonedConfig.getPreExecution() != null) {
                     if (!elem.checkRunConfig(clonedConfig.getPreExecution(), exCon)) {
-                         //#238360 when the check says don't execute, we still need to close the output and mark it for reuse
+                        //#238360 when the check says don't execute, we still need to close the output and mark it for reuse
                         ioput.getOut().close();
                         ioput.getErr().close();
                         actionStatesAtFinish(null, null);
@@ -257,9 +257,8 @@
                 }
             }
         }
-        
+
 //        final Properties originalProperties = clonedConfig.getProperties();
-        
         handle.start();
         processInitialMessage();
         boolean isMaven3 = !isMaven2();
@@ -272,7 +271,6 @@
             }
         }
 
-        
         CommandLineOutputHandler out = new CommandLineOutputHandler(ioput, clonedConfig.getProject(), handle, clonedConfig, isMaven3 && singlethreaded);
         try {
             BuildExecutionSupport.registerRunningItem(item);
@@ -352,7 +350,7 @@
             return true;
         }
     }
-    
+
     /**
      * Overridable by tests.
      */
@@ -371,7 +369,7 @@
         env.put(KEY_UUID, uuid);
         Processes.killTree(prcs, env);
     }
-    
+
     @Override
     public boolean cancel() {
         final Process pre = preProcess;
@@ -391,7 +389,7 @@
         });
         return true;
     }
-        
+
     private static List<String> createMavenExecutionCommand(RunConfig config, Constructor base) {
         List<String> toRet = new ArrayList<String>(base.construct());
 
@@ -415,8 +413,7 @@
         String quote = "\"";
         // the command line parameters with space in them need to be quoted and escaped to arrive
         // correctly to the java runtime on windows
-        String escaped = "\\" + quote;        
-        for (Map.Entry<? extends String,? extends String> entry : config.getProperties().entrySet()) {
+        for (Map.Entry<? extends String, ? extends String> entry : config.getProperties().entrySet()) {
             String k = entry.getKey();
             // filter out env vars AND internal properties.
             if (k.startsWith(ENV_PREFIX) || k.startsWith(INTERNAL_PREFIX)) {
@@ -424,15 +421,12 @@
             }
             //skip envs, these get filled in later.
             //#228901 since u21 we need to use cmd /c to execute on windows, quotes get escaped and when there is space in value, the value gets wrapped in quotes.
-            String value = (Utilities.isWindows() ? entry.getValue().replace(quote, escaped) : entry.getValue().replace(quote, "'"));
-            if (Utilities.isWindows() && value.endsWith("\"")) {
-                //#201132 property cannot end with 2 double quotes, add a space to the end after our quote to prevent the state
-                value = value + " ";
-            }
-            String s = "-D" + entry.getKey() + "=" + (Utilities.isWindows() && value.contains(" ") ? quote + value + quote : value);            
+            String value = quote2apos(entry.getValue());
+            String p = "-D" + entry.getKey() + "=" + value;
+            String s = (Utilities.isWindows() && value.contains(" ") ? quote + p + quote : p);
             toRet.add(s);
         }
-        
+
         //TODO based on a property? or UI option? can this backfire?
         //#224526 
         //execute in encoding that is based on project.build.sourceEncoding to have the output of exec:exec, surefire:test and others properly encoded.
@@ -449,7 +443,7 @@
         if (!config.isInteractive()) {
             toRet.add("--batch-mode"); //NOI18N
         }
-        
+
         if (!config.isRecursive()) {
             toRet.add("--non-recursive");//NOI18N
         }
@@ -509,7 +503,7 @@
         }
 
         String profiles = "";//NOI18N
-        
+
         for (Object profile : config.getActivatedProfiles()) {
             profiles = profiles + "," + profile;//NOI18N
         }
@@ -517,13 +511,59 @@
             profiles = profiles.substring(1);
             toRet.add("-P" + profiles);//NOI18N
         }
-        
+
         for (String goal : config.getGoals()) {
             toRet.add(goal);
         }
-        
+
         return toRet;
     }
+   
+    /**
+     * Quotes the parameter string using apostrohphes. As Maven does not understand \' escape in quoted string, work around by terminating single-quote
+     * and pass the apostrophe as double-quoted single-character string, then open single-quote again.
+     * @param s
+     * @return quoted string
+     */
+    private static String quote2apos(String s) {
+        boolean inQuote = false;
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c == '\\') {
+                i++;
+                if (i < s.length()) {
+                    char c2 = s.charAt(i);
+                    if (inQuote) {
+                        if (c2 == '\'') {
+                            sb.append("'\"'\"'");
+                            continue;
+                        } else if (c2 == '\"') {
+                            sb.append(c2);
+                            continue;
+                        }
+                    }
+                    sb.append(c);
+                    sb.append(c2);
+                } else {
+                    sb.append(c);
+                }
+            } else if (c == '\'') {
+                if (inQuote) {
+                    sb.append("'\"'\"'");
+                } else {
+                    inQuote = !inQuote;
+                    sb.append('\'');
+                }
+            } else if (c == '"') {
+                inQuote = !inQuote;
+                sb.append('\'');
+            } else {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
 
     private ProcessBuilder constructBuilder(final RunConfig clonedConfig, InputOutput ioput) {
         File javaHome = null;
@@ -588,12 +628,12 @@
         Constructor constructeur = new ShellConstructor(mavenHome);
 
         List<String> cmdLine = createMavenExecutionCommand(clonedConfig, constructeur);
-        
+
         //#228901 on windows, since u21 we must use cmd /c
         // the working format is ""C:\Users\mkleint\space in path\apache-maven-3.0.4\bin\mvn.bat"
-                           //-Dexec.executable=java -Dexec.args="-jar
-                           //C:\Users\mkleint\Documents\NetBeansProjects\JavaApplication13\dist\JavaApplication13.jar
-                           //-Dxx=\"space path\" -Dfoo=bar" exec:exec""
+        //-Dexec.executable=java -Dexec.args="-jar
+        //C:\Users\mkleint\Documents\NetBeansProjects\JavaApplication13\dist\JavaApplication13.jar
+        //-Dxx=\"space path\" -Dfoo=bar" exec:exec""
         if (cmdLine.get(0).equals("cmd")) {
             //merge all items after cmd /c into one string and quote it.
             StringBuilder sb = new StringBuilder();
@@ -602,12 +642,12 @@
             it.next(); //cmd
             it.next(); //c
             String m = it.next();
-            
+
             sb.append(m);
             while (it.hasNext()) {
                 sb.append(" ").append(it.next());
             }
-            
+
             // NETBEANS-3251, NETBEANS-3254: 
             // JDK-8221858 (non public) / CVE-2019-2958 changed the way cmd 
             // command lines are verified and made it "difficult" to have embedded 
@@ -636,11 +676,11 @@
             // TODO: do we really put *all* the env vars there? maybe filter, M2_HOME and JDK_HOME?
             builder.environment().put(env, val);
             if (!env.equals(CosChecker.NETBEANS_PROJECT_MAPPINGS)
-                && !env.equals(NETBEANS_MAVEN_COMMAND_LINE)) { //don't show to user
+                    && !env.equals(NETBEANS_MAVEN_COMMAND_LINE)) { //don't show to user
                 display.append(Utilities.escapeParameters(new String[] {env + "=" + val})).append(' '); // NOI18N
             }
         }
-       
+
         if (mavenHome != null) {
             //#195039
             builder.environment().put("M2_HOME", mavenHome.getAbsolutePath());
@@ -665,9 +705,9 @@
             }
             display.append(Utilities.escapeParameters(command.toArray(new String[command.size()])));
         }
-        
+
         printGray(ioput, display.toString());
-        
+
         return builder;
     }
 
@@ -727,14 +767,14 @@
             if (isMaven2()) {
                 printGray(ioput, "WARNING: Using Maven 2.x for execution, NetBeans cannot establish links between current project and output directories of dependency projects with Compile on Save turned on. Only works with Maven 3.0+.");
             }
-            
+
         }
         if (clonedConfig.getProperties().containsKey(ModelRunConfig.EXEC_MERGED)) {
             printGray(ioput, "\nDefault '" + clonedConfig.getActionName() + "' action exec.args merged with maven-exec-plugin arguments declared in pom.xml.");
         }
-        
+
     }
-    
+
     boolean isMaven2() {
         File mvnHome = EmbedderFactory.getEffectiveMavenHome();
         String version = MavenSettings.getCommandLineMavenVersion(mvnHome);
@@ -774,7 +814,7 @@
             }
         }
         return list.contains("-T") || list.contains("--threads");
-    } 
+    }
 
     private File guessBestMaven(RunConfig clonedConfig, InputOutput ioput) {
         MavenProject mp = clonedConfig.getMavenProject();
@@ -861,8 +901,8 @@
         if (ver == null) {
             return null;
         }
-        
-        File f = getAltMavenLocation(); 
+
+        File f = getAltMavenLocation();
         File child = FileUtil.normalizeFile(new File(f, "apache-maven-" + ver));
         if (child.exists()) {
             return child;
@@ -874,7 +914,7 @@
                 //this url pattern works for all versions except the last one 3.2.3
                 //which is only under <mirror>/apache/maven/maven-3/3.2.3/binaries/
                 URL[] urls = new URL[] {new URL("http://archive.apache.org/dist/maven/binaries/apache-maven-" + ver + "-bin.zip"),
-                                        new URL("http://archive.apache.org/dist/maven/maven-3/" + ver + "/binaries/apache-maven-" + ver + "-bin.zip")};
+                    new URL("http://archive.apache.org/dist/maven/maven-3/" + ver + "/binaries/apache-maven-" + ver + "-bin.zip")};
                 InputStream is = null;
                 for (URL u : urls) {
                     try {
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java b/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java
index 1c2924c..a5438e0 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/LaunchArgPrereqsChecker.java
@@ -86,7 +86,7 @@
                 }
             }
             for (StartupExtender group : StartupExtender.getExtenders(new AbstractLookup(ic), mode)) {
-                fixedArgs.addAll(group.getArguments());
+                fixedArgs.addAll(group.getRawArguments());
             }
         }
 
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java b/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java
index 614f4e8..4367fa4 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/MavenExecuteUtils.java
@@ -550,34 +550,33 @@
         return c == '\'' || c == '"';
     }
     
-    public static String joinParameters(List<String> params) {
-        StringBuilder sb = new StringBuilder();
+    public static List<String> escapeParameters(List<String> params) {
+        List<String> ret = new ArrayList<>();
         for (String s : params) {
             if (s == null) {
                 continue;
             }
-            if (sb.length() > 0) {
-                sb.append(" ");
-            }
             if (s.length() > 1) {
                 char c = s.charAt(0);
                 if (isQuoteChar(c) && s.charAt(s.length() - 1) == c) {
-                    sb.append(s);
+                    ret.add(s);
                     continue;
                 }
             }
             // note: does not care about escaped spaces.
             if (!s.contains(" ")) {
-                sb.append(s.replace("'", "\\'").replace("\"", "\\\""));
+                ret.add(s.replace("'", "\\'").replace("\"", "\\\""));
             } else {
-                sb.append("\"").append(
-                        s.replace("\"", "\\\"")
-                ).append("\"");
+                ret.add("\"" + s.replace("\"", "\\\"") + "\"");
             }
         }
-        return sb.toString();
+        return ret;
     }
     
+    public static String joinParameters(List<String> params) {
+        return String.join(" ", escapeParameters(params));
+    }
+
     public static List<String> extractDebugJVMOptions(String argLine) {
         Iterable<String> split = propertySplitter(argLine, true);
         List<String> toRet = new ArrayList<String>();
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/PropertySplitter.java b/java/maven/src/org/netbeans/modules/maven/runjar/PropertySplitter.java
index 8880da1..bfae302 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/PropertySplitter.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/PropertySplitter.java
@@ -85,7 +85,7 @@
                         buffer.append(escape).append(c);
                     }
                     escapeNext = false;
-                } else if (!inQuote && c == escape) {
+                } else if (c == escape) {
                     escapeNext = true;
                 } else if (inQuote) {
                     if (c == quoteChar) {
diff --git a/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java b/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java
index 3f32b56..811c713 100644
--- a/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java
+++ b/java/maven/src/org/netbeans/modules/maven/runjar/RunJarStartupArgs.java
@@ -39,7 +39,6 @@
 import org.netbeans.modules.maven.execute.ModelRunConfig;
 import org.netbeans.spi.project.ActionProvider;
 import org.netbeans.spi.project.ProjectServiceProvider;
-import org.openide.util.Lookup;
 import org.openide.util.lookup.AbstractLookup;
 import org.openide.util.lookup.InstanceContent;
 
@@ -108,7 +107,7 @@
                 }
             }
             for (StartupExtender group : StartupExtender.getExtenders(new AbstractLookup(ic), mode)) {
-                fixedArgs.addAll(group.getArguments());
+                fixedArgs.addAll(group.getRawArguments());
             }
         }
         
diff --git a/java/maven/test/unit/data/exec/PrintCommandLine.java b/java/maven/test/unit/data/exec/PrintCommandLine.java
new file mode 100644
index 0000000..fec95bd
--- /dev/null
+++ b/java/maven/test/unit/data/exec/PrintCommandLine.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package test;
+
+public class PrintCommandLine {
+    public static void main(String[] args) {
+        System.err.println("::PrintCommandLineStart");
+        System.err.print("argCount="); System.err.println(args.length);
+        
+        int index = 1;
+        for (String s : args) {
+            System.err.print("appArg." + index + "="); System.err.println(s);
+            index++;
+        }
+        
+        for (String k : System.getProperties().stringPropertyNames()) {
+            if (k.startsWith("test.")) {
+                System.err.print(k + "="); System.err.println(System.getProperty(k));
+            }
+        }
+        System.err.println("::PrintCommandLineEnd");
+    }
+}
diff --git a/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/RunJarStartupArgsTest.java b/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/RunJarStartupArgsTest.java
new file mode 100644
index 0000000..5d8914f
--- /dev/null
+++ b/java/maven/test/unit/src/org/netbeans/modules/maven/runjar/RunJarStartupArgsTest.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.maven.runjar;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import org.junit.Assume;
+import org.netbeans.api.extexecution.startup.StartupExtender;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.maven.NbMavenProjectImpl;
+import org.netbeans.modules.maven.api.execute.RunConfig;
+import org.netbeans.modules.maven.execute.ActionToGoalUtils;
+import org.netbeans.modules.maven.execute.BeanRunConfig;
+import org.netbeans.modules.maven.execute.MavenCommandLineExecutor;
+import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.netbeans.spi.project.ActionProvider;
+import org.openide.execution.ExecutorTask;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.test.TestFileUtils;
+import org.openide.util.BaseUtilities;
+import org.openide.util.Lookup;
+import org.openide.windows.InputOutput;
+import org.openide.windows.OutputListener;
+import org.openide.windows.OutputWriter;
+
+/**
+ *
+ * @author sdedic
+ */
+public class RunJarStartupArgsTest extends NbTestCase {
+
+    public RunJarStartupArgsTest(String name) {
+        super(name);
+    }
+    
+    private FileObject d;
+
+    protected @Override void setUp() throws Exception {
+        clearWorkDir();
+        d = FileUtil.toFileObject(getWorkDir());
+    }
+
+    
+    @Override
+    protected void tearDown() throws Exception {
+        LegacyStartupExtenderImpl.enabled = false;
+        CurrentStartupExtenderImpl.enabled = false;
+        super.tearDown();
+    }
+
+    /**
+     * Legacy Extenders give quoted arguments to preserve spaces. The quoted strings
+     * must be properly incorporated in exec.vmArgs property.
+     * @throws Exception 
+     */
+    public void testLegacyExtenderWithSpaces() throws Exception {
+        Assume.assumeFalse(BaseUtilities.isWindows());
+        LegacyStartupExtenderImpl.enabled = true;
+        Properties p = doTestSpacedExtender();
+        assertEquals("/tmp/spaced folder/", p.getProperty("test.spaced.folder.path"));
+        assertEquals("/tmp/another space/", p.getProperty("test.another.space"));
+    }
+    
+    /**
+     * Checks that 'modern' StartupExtender that declares it does NOT escape arguments work.
+     * @throws Exception 
+     */
+    public void testNetBeans12_4Extender() throws Exception {
+        Assume.assumeFalse(BaseUtilities.isWindows());
+        CurrentStartupExtenderImpl.enabled = true;
+        Properties p = doTestSpacedExtender();
+        assertEquals("/tmp/spaced folder/", p.getProperty("test.spaced.folder.path"));
+        assertEquals("/tmp/another space/", p.getProperty("test.another.space"));
+    }
+    
+    private Properties doTestSpacedExtender() throws Exception {
+        System.setProperty("netbeans.dirs", System.getProperty("cluster.path.final", ""));
+        TestFileUtils.writeFile(d, "pom.xml",
+                "<project>\n" +
+                "    <modelVersion>4.0.0</modelVersion>\n" +
+                "    <groupId>testgrp</groupId>\n" +
+                "    <artifactId>testart</artifactId>\n" +
+                "    <version>1.0</version>\n" +
+                "    <properties>\n" +
+                "        <project.mainclass>test.PrintCommandLine</project.mainclass>\n" +
+                "        <exec.java.bin>${java.home}/bin/java</exec.java.bin>\n" +
+                "    </properties>\n" +
+                "</project>\n");
+        
+        FileObject f = FileUtil.createFolder(d, "src/main/java/test");
+        FileObject source = FileUtil.toFileObject(getDataDir()).getFileObject("exec/PrintCommandLine.java");
+        FileObject result = FileUtil.copyFile(source, f, source.getName());
+        System.err.println("Testing application: " + result.getPath());
+
+        Project proj = ProjectManager.getDefault().findProject(d);
+        RunConfig rc = ActionToGoalUtils.createRunConfig(ActionProvider.COMMAND_RUN, proj.getLookup().lookup(NbMavenProjectImpl.class), proj.getLookup());
+        rc.setProperty("packageClassName", "test.PrintCommandLine");
+        ((BeanRunConfig)rc).setShowDebug(true);
+        ((BeanRunConfig)rc).setShowError(true);
+        
+        class CaptureOutput implements InputOutput {
+            
+            class OW extends OutputWriter {
+                public OW(Writer w) {
+                    super(w);
+                }
+
+                @Override
+                public void println(String s, OutputListener l) throws IOException {
+                    super.println(s);
+                }
+
+                @Override
+                public void reset() throws IOException {
+                }
+            }
+            
+            StringWriter sw = new StringWriter();
+
+            OW writer = new OW(sw);
+            
+            @Override
+            public OutputWriter getOut() {
+                return writer;
+            }
+
+            @Override
+            public Reader getIn() {
+                return InputOutput.NULL.getIn();
+            }
+
+            @Override
+            public OutputWriter getErr() {
+                return InputOutput.NULL.getErr();
+            }
+
+            @Override
+            public void closeInputOutput() {
+            }
+
+            @Override
+            public boolean isClosed() {
+                return false;
+            }
+
+            @Override
+            public void setOutputVisible(boolean value) {
+            }
+
+            @Override
+            public void setErrVisible(boolean value) {
+            }
+
+            @Override
+            public void setInputVisible(boolean value) {
+            }
+
+            @Override
+            public void select() {
+            }
+
+            @Override
+            public boolean isErrSeparated() {
+                return true;
+            }
+
+            @Override
+            public void setErrSeparated(boolean value) {
+            }
+
+            @Override
+            public boolean isFocusTaken() {
+                return false;
+            }
+
+            @Override
+            public void setFocusTaken(boolean value) {
+            }
+
+            @Override
+            public Reader flushReader() {
+                return getIn();
+            }
+            
+        }
+        
+        final CaptureOutput out = new CaptureOutput();
+        
+        ExecutorTask t = new ExecutorTask(() -> {
+        }) {
+            @Override
+            public void stop() {
+            }
+
+            @Override
+            public int result() {
+                return 0;
+            }
+
+            @Override
+            public InputOutput getInputOutput() {
+                return out;
+            }
+        };
+
+        MavenCommandLineExecutor cme = new MavenCommandLineExecutor(rc, out, null);
+        
+        cme.setTask(t);
+        cme.run();
+        
+        out.writer.flush();
+        
+        String[] lines = out.sw.toString().split("\n");
+        int from = 0;
+        for (; from < lines.length; from++) {
+            if (lines[from].startsWith("::PrintCommandLineStart"))  {
+                from++;
+                break;
+            }
+        }
+        assertTrue(from > 0);
+        
+        Properties p = new Properties();
+        while (from < lines.length) {
+            String s = lines[from];
+            if (s.startsWith("::PrintCommandLineEnd")) {
+                break;
+            }
+            int eq = s.indexOf('=');
+            p.put(s.substring(0, eq), s.substring(eq + 1));
+            from++;
+        }
+        
+        return p;
+    }
+
+    /**
+     * Returns a quoted string with spaces, to observe that a legacy behaviour works. This is used by e.g. profiler, which
+     * quotes its data path or path to JNI libraries (NB may be installed with a folder-with-spaces).
+     */
+    @StartupExtenderImplementation.Registration(displayName = "Test legacy", startMode = { StartupExtender.StartMode.NORMAL })
+    public static class LegacyStartupExtenderImpl implements StartupExtenderImplementation {
+        static boolean enabled = false;
+        
+        @Override
+        public List<String> getArguments(Lookup context, StartupExtender.StartMode mode) {
+            if (!enabled) {
+                return Collections.emptyList();
+            }
+            return Arrays.asList(
+                "-Dtest.spaced.folder.path=\"/tmp/spaced folder/\"",
+                "-Dtest.another.space=\"/tmp/another space/\""    
+            );
+        }
+        
+    }
+
+    /**
+     * Returns a quoted string with spaces, to observe that a legacy behaviour works. This is used by e.g. profiler, which
+     * quotes its data path or path to JNI libraries (NB may be installed with a folder-with-spaces).
+     */
+    @StartupExtenderImplementation.Registration(displayName = "Test 12.4", startMode = { StartupExtender.StartMode.NORMAL }, argumentsQuoted = false)
+    public static class CurrentStartupExtenderImpl implements StartupExtenderImplementation {
+        static boolean enabled = false;
+        
+        @Override
+        public List<String> getArguments(Lookup context, StartupExtender.StartMode mode) {
+            if (!enabled) {
+                return Collections.emptyList();
+            }
+            return Arrays.asList(
+                "-Dtest.spaced.folder.path=/tmp/spaced folder/",
+                "-Dtest.another.space=/tmp/another space/"
+            );
+        }
+        
+    }
+}
diff --git a/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/ApiGenAnnotationsProvider.java b/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/ApiGenAnnotationsProvider.java
index 71c22ee..1992134 100644
--- a/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/ApiGenAnnotationsProvider.java
+++ b/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/ApiGenAnnotationsProvider.java
@@ -86,6 +86,7 @@
                 new FilesourceTag(),
                 new GlobalTag(),
                 new IgnoreTag(),
+                new InheritDocTag(),
                 new InternalTag(),
                 new LicenseTag(),
                 new LinkTag(),
@@ -116,6 +117,7 @@
                 new ExampleTag(),
                 new GlobalTag(),
                 new IgnoreTag(),
+                new InheritDocTag(),
                 new InternalTag(),
                 new LicenseTag(),
                 new LinkTag(),
@@ -143,6 +145,7 @@
                 new FinalTag(),
                 new GlobalTag(),
                 new IgnoreTag(),
+                new InheritDocTag(),
                 new InternalTag(),
                 new LicenseTag(),
                 new LinkTag(),
diff --git a/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/Bundle.properties b/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/Bundle.properties
index a0dcc36..37cae40 100644
--- a/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/Bundle.properties
+++ b/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/Bundle.properties
@@ -500,6 +500,32 @@
 }\n\
 </code></pre>
 
+InheritDocTag.documentation=<p style="font-weight: bold; font-size: 1.2em">inline {@inheritDoc}</p>\
+<p>\
+This inline tag is used to inherit a description from a parent class into child classes.\
+</p>\
+<p>\
+<code>{@inheritDoc}</code>\
+</p>\
+<p style="font-weight: bold; font-size: 1.1em">Example</p>\
+<pre><code>\n\
+/**\n\
+\ * Parent title.\n\
+\ *\n\
+\ * Parent Description.\n\
+\ */\n\
+class ParentClass\n\
+{\n\
+}\n\
+\n\
+/**\n\
+\ * {@inheritdoc}\n\
+\ */\n\
+class ChildClass extends ParentClass\n\
+{\n\
+}\n\
+</code></pre>
+
 InternalTag.documentation=<p style="font-weight: bold; font-size: 1.2em">@internal</p>\
 <p>\
 Mark documentation as private, internal to the software project.\
@@ -1443,4 +1469,4 @@
 class Blah {\n\
 \    ...\n\
 }\n\
-</code></pre>
\ No newline at end of file
+</code></pre>
diff --git a/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/InheritDocTag.java b/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/InheritDocTag.java
new file mode 100644
index 0000000..f982bab
--- /dev/null
+++ b/php/php.apigen/src/org/netbeans/modules/php/apigen/annotations/InheritDocTag.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.php.apigen.annotations;
+
+import org.netbeans.modules.php.spi.annotation.AnnotationCompletionTag;
+import org.openide.util.NbBundle;
+
+/**
+ * inline {@code {@inheritDoc}} tag.
+ */
+public class InheritDocTag extends AnnotationCompletionTag {
+
+    public InheritDocTag() {
+        super("inheritDoc", "@inheritDoc", // NOI18N
+                NbBundle.getMessage(UsesTag.class, "InheritDocTag.documentation")); // NOI18N
+    }
+
+}
diff --git a/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.form b/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.form
index 6584c64..9f64be0 100644
--- a/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.form
+++ b/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.form
@@ -51,7 +51,6 @@
                       <EmptySpace max="-2" attributes="0"/>
                       <Group type="103" groupAlignment="0" attributes="0">
                           <Component id="noteLabel" alignment="0" min="-2" max="-2" attributes="0"/>
-                          <Component id="informationLabel" alignment="0" min="-2" max="-2" attributes="0"/>
                           <Component id="learnMoreLabel" alignment="0" min="-2" max="-2" attributes="0"/>
                           <Component id="emptyLabel" alignment="0" min="-2" max="-2" attributes="0"/>
                           <Group type="102" attributes="0">
@@ -94,9 +93,7 @@
               <Component id="debuggerPortOptionLabel" min="-2" max="-2" attributes="0"/>
               <EmptySpace max="-2" attributes="0"/>
               <Component id="optionsLabel" min="-2" max="-2" attributes="0"/>
-              <EmptySpace type="unrelated" min="-2" max="-2" attributes="0"/>
-              <Component id="informationLabel" min="-2" max="-2" attributes="0"/>
-              <EmptySpace max="-2" attributes="0"/>
+              <EmptySpace type="separate" min="-2" max="-2" attributes="0"/>
               <Component id="learnMoreLabel" min="-2" max="-2" attributes="0"/>
               <EmptySpace type="separate" max="-2" attributes="0"/>
               <Component id="emptyLabel" min="-2" max="-2" attributes="0"/>
@@ -135,15 +132,6 @@
         <EventHandler event="mouseEntered" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="optionsLabelMouseEntered"/>
       </Events>
     </Component>
-    <Component class="javax.swing.JLabel" name="informationLabel">
-      <Properties>
-        <Property name="text" type="java.lang.String" value="&lt;html&gt;&lt;a href=&quot;#&quot;&gt;More information about Xdebug2 installation/configuration&lt;/a&gt;"/>
-      </Properties>
-      <Events>
-        <EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="informationLabelMousePressed"/>
-        <EventHandler event="mouseEntered" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="informationLabelMouseEntered"/>
-      </Events>
-    </Component>
     <Component class="javax.swing.JLabel" name="learnMoreLabel">
       <Properties>
         <Property name="text" type="java.lang.String" value="&lt;html&gt;&lt;a href=&quot;#&quot;&gt;Learn more about Xdebug&lt;/a&gt;"/>
diff --git a/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.java b/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.java
index b10630c..204bc8c 100644
--- a/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.java
+++ b/php/php.dbgp/src/org/netbeans/modules/php/dbgp/ConnectionErrMessage.java
@@ -113,7 +113,6 @@
         noteLabel = new JLabel();
         debuggerPortOptionLabel = new JLabel();
         optionsLabel = new JLabel();
-        informationLabel = new JLabel();
         learnMoreLabel = new JLabel();
         emptyLabel = new JLabel();
         copySettingsLabel = new JLabel();
@@ -136,16 +135,6 @@
             }
         });
 
-        informationLabel.setText("<html><a href=\"#\">More information about Xdebug2 installation/configuration</a>");
-        informationLabel.addMouseListener(new MouseAdapter() {
-            public void mousePressed(MouseEvent evt) {
-                informationLabelMousePressed(evt);
-            }
-            public void mouseEntered(MouseEvent evt) {
-                informationLabelMouseEntered(evt);
-            }
-        });
-
         learnMoreLabel.setText("<html><a href=\"#\">Learn more about Xdebug</a>");
         learnMoreLabel.addMouseListener(new MouseAdapter() {
             public void mousePressed(MouseEvent evt) {
@@ -186,7 +175,6 @@
                         .addContainerGap()
                         .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
                             .addComponent(noteLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
-                            .addComponent(informationLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                             .addComponent(learnMoreLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                             .addComponent(emptyLabel)
                             .addGroup(layout.createSequentialGroup()
@@ -219,9 +207,7 @@
                 .addComponent(debuggerPortOptionLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                 .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
                 .addComponent(optionsLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
-                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
-                .addComponent(informationLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
-                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+                .addGap(18, 18, 18)
                 .addComponent(learnMoreLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                 .addGap(18, 18, 18)
                 .addComponent(emptyLabel)
@@ -242,14 +228,6 @@
         showUrl("https://xdebug.org/docs"); // NOI18N
     }//GEN-LAST:event_learnMoreLabelMousePressed
 
-    private void informationLabelMouseEntered(MouseEvent evt) {//GEN-FIRST:event_informationLabelMouseEntered
-        setHandCursor(evt);
-    }//GEN-LAST:event_informationLabelMouseEntered
-
-    private void informationLabelMousePressed(MouseEvent evt) {//GEN-FIRST:event_informationLabelMousePressed
-        showUrl("http://wiki.netbeans.org/HowToConfigureXDebug"); // NOI18N
-    }//GEN-LAST:event_informationLabelMousePressed
-
     private void optionsLabelMouseEntered(MouseEvent evt) {//GEN-FIRST:event_optionsLabelMouseEntered
         setHandCursor(evt);
     }//GEN-LAST:event_optionsLabelMouseEntered
@@ -322,7 +300,6 @@
     private JButton copySettingsXdebug3Button;
     private JLabel debuggerPortOptionLabel;
     private JLabel emptyLabel;
-    private JLabel informationLabel;
     private JLabel learnMoreLabel;
     private JLabel messageTextLabel;
     private JLabel noteLabel;
diff --git a/php/php.editor/src/org/netbeans/modules/php/editor/completion/PHPCodeCompletion.java b/php/php.editor/src/org/netbeans/modules/php/editor/completion/PHPCodeCompletion.java
index ec0f079..1c24d79 100644
--- a/php/php.editor/src/org/netbeans/modules/php/editor/completion/PHPCodeCompletion.java
+++ b/php/php.editor/src/org/netbeans/modules/php/editor/completion/PHPCodeCompletion.java
@@ -2166,7 +2166,7 @@
             Token t = ts.token();
             if (t != null) {
                 if (t.id() == PHPTokenId.T_INLINE_HTML) {
-                    return QueryType.ALL_COMPLETION;
+                    return QueryType.NONE;
                 } else {
                     if (AUTOPOPUP_STOP_CHARS.contains(Character.valueOf(lastChar))) {
                         return QueryType.STOP;
diff --git a/platform/libs.flatlaf/external/binaries-list b/platform/libs.flatlaf/external/binaries-list
index dbbd79a..22a0c39 100644
--- a/platform/libs.flatlaf/external/binaries-list
+++ b/platform/libs.flatlaf/external/binaries-list
@@ -15,4 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-1E93E3D19E4FFFAB2BB9AF96A6D47BBB7B3AB1CE com.formdev:flatlaf:1.4
+D5AD617D7C328784145B05F7C8F030DB2DE5234E com.formdev:flatlaf:1.5
diff --git a/platform/libs.flatlaf/external/flatlaf-1.4-license.txt b/platform/libs.flatlaf/external/flatlaf-1.5-license.txt
similarity index 99%
rename from platform/libs.flatlaf/external/flatlaf-1.4-license.txt
rename to platform/libs.flatlaf/external/flatlaf-1.5-license.txt
index 93beceb..271cdda 100644
--- a/platform/libs.flatlaf/external/flatlaf-1.4-license.txt
+++ b/platform/libs.flatlaf/external/flatlaf-1.5-license.txt
@@ -1,7 +1,7 @@
 Name: FlatLaf Look and Feel
 Description: FlatLaf Look and Feel
-Version: 1.4
-Files: flatlaf-1.4.jar
+Version: 1.5
+Files: flatlaf-1.5.jar
 License: Apache-2.0
 Origin: FormDev Software GmbH.
 URL: https://www.formdev.com/flatlaf/
diff --git a/platform/libs.flatlaf/nbproject/project.properties b/platform/libs.flatlaf/nbproject/project.properties
index 2eced85..afe9131 100644
--- a/platform/libs.flatlaf/nbproject/project.properties
+++ b/platform/libs.flatlaf/nbproject/project.properties
@@ -20,4 +20,4 @@
 javac.source=1.8
 nbm.target.cluster=platform
 
-release.external/flatlaf-1.4.jar=modules/ext/flatlaf-1.4.jar
+release.external/flatlaf-1.5.jar=modules/ext/flatlaf-1.5.jar
diff --git a/platform/libs.flatlaf/nbproject/project.xml b/platform/libs.flatlaf/nbproject/project.xml
index ac57929..4469760 100644
--- a/platform/libs.flatlaf/nbproject/project.xml
+++ b/platform/libs.flatlaf/nbproject/project.xml
@@ -30,8 +30,8 @@
                 <package>com.formdev.flatlaf.util</package>
             </public-packages>
             <class-path-extension>
-                <runtime-relative-path>ext/flatlaf-1.4.jar</runtime-relative-path>
-                <binary-origin>external/flatlaf-1.4.jar</binary-origin>
+                <runtime-relative-path>ext/flatlaf-1.5.jar</runtime-relative-path>
+                <binary-origin>external/flatlaf-1.5.jar</binary-origin>
             </class-path-extension>
         </data>
     </configuration>
diff --git a/platform/openide.actions/src/org/openide/actions/HeapView.java b/platform/openide.actions/src/org/openide/actions/HeapView.java
index a3bd5fb..0f4b13f 100644
--- a/platform/openide.actions/src/org/openide/actions/HeapView.java
+++ b/platform/openide.actions/src/org/openide/actions/HeapView.java
@@ -54,7 +54,7 @@
  * <li> nb.heapview.background - Color of widget background
  * <li> nb.heapview.foreground - Color of text
  * <li> nb.heapview.chart - Color of area chart
- * <li> nb.heapview.background - Color of outline around the text, to provide a contrast against
+ * <li> nb.heapview.highlight - Color of outline around the text, to provide a contrast against
  *                               the chart (may have a non-opaque alpha value)
  * </ul>
  * @author sky, radim, peter
@@ -112,17 +112,20 @@
         }
         TEXT_COLOR = c;
 
-        c = UIManager.getColor("nb.heapview.highlight"); //NOI18N
-        if (null == c) {
-            c = new Color(255, 255, 255, 192);
-        }
-        OUTLINE_COLOR = c;
-
         c = UIManager.getColor("nb.heapview.background"); //NOI18N
         if (null == c) {
             c = new Color(0xCEDBE6);
         }
         BACKGROUND_COLOR = c;
+
+        c = UIManager.getColor("nb.heapview.highlight"); //NOI18N
+        if (null == c) {
+            c = new Color(BACKGROUND_COLOR.getRed(),
+                    BACKGROUND_COLOR.getGreen(),
+                    BACKGROUND_COLOR.getBlue(),
+                    192);
+        }
+        OUTLINE_COLOR = c;
     }
 
     /**
diff --git a/profiler/profiler.nbimpl/nbproject/project.xml b/profiler/profiler.nbimpl/nbproject/project.xml
index e9e8579..7cfecd1 100644
--- a/profiler/profiler.nbimpl/nbproject/project.xml
+++ b/profiler/profiler.nbimpl/nbproject/project.xml
@@ -129,7 +129,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.30</specification-version>
+                        <specification-version>1.62</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/profiler/profiler.nbimpl/src/org/netbeans/modules/profiler/nbimpl/providers/DefaultProfilerArgsProvider.java b/profiler/profiler.nbimpl/src/org/netbeans/modules/profiler/nbimpl/providers/DefaultProfilerArgsProvider.java
index 65e045b..9bdbad7 100644
--- a/profiler/profiler.nbimpl/src/org/netbeans/modules/profiler/nbimpl/providers/DefaultProfilerArgsProvider.java
+++ b/profiler/profiler.nbimpl/src/org/netbeans/modules/profiler/nbimpl/providers/DefaultProfilerArgsProvider.java
@@ -24,6 +24,7 @@
 import org.netbeans.api.project.Project;
 import org.netbeans.modules.profiler.nbimpl.actions.ProfilerLauncher;
 import org.netbeans.spi.extexecution.startup.StartupExtenderImplementation;
+import org.openide.util.BaseUtilities;
 import org.openide.util.Lookup;
 import org.openide.util.NbBundle;
 
@@ -34,7 +35,7 @@
 @NbBundle.Messages({
     "DESC_NBProfiler=NetBeans Profiler"
 })
-@StartupExtenderImplementation.Registration(displayName="#DESC_NBProfiler", position=1000, startMode={
+@StartupExtenderImplementation.Registration(displayName="#DESC_NBProfiler", position=1000, argumentsQuoted = false, startMode={
     StartupExtender.StartMode.PROFILE,
     StartupExtender.StartMode.TEST_PROFILE
 })
@@ -50,7 +51,8 @@
                     List<String> args = new ArrayList<String>();
                     
                     String agentArgs = m.get("agent.jvmargs"); // NOI18N // Always set
-                    args.add(agentArgs);
+                    // remove quoting, expand params to array
+                    args.addAll(Arrays.asList(BaseUtilities.parseParameters(agentArgs)[0]));
                     
                     String jvmargs = m.get("profiler.info.jvmargs"); // NOI18N // May not be set
                     if (jvmargs != null) {
@@ -59,11 +61,13 @@
                         while (st.hasMoreTokens()) {
                             String arg = st.nextToken();
                             if (!arg.isEmpty()) {
-                                args.add((arg.startsWith("-") ? "" : "-") + arg); // NOI18N
+                                // remove any quoting etc, there should be just a single parameter.
+                                for (String a : BaseUtilities.parseParameters(arg)) {
+                                    args.add((arg.startsWith("-") ? "" : "-") + a); // NOI18N
+                                }
                             }
                         }
                     }
-                    
                     return args;
                 }
             }
diff --git a/webcommon/javascript.nodejs/src/org/netbeans/modules/javascript/nodejs/exec/NodeExecutable.java b/webcommon/javascript.nodejs/src/org/netbeans/modules/javascript/nodejs/exec/NodeExecutable.java
index 227124c..f392b21 100644
--- a/webcommon/javascript.nodejs/src/org/netbeans/modules/javascript/nodejs/exec/NodeExecutable.java
+++ b/webcommon/javascript.nodejs/src/org/netbeans/modules/javascript/nodejs/exec/NodeExecutable.java
@@ -359,7 +359,7 @@
         List<String> params = new ArrayList<>();
         List<StartupExtender> extenders = StartupExtender.getExtenders(project.getLookup(), StartupExtender.StartMode.DEBUG);
         for (StartupExtender e : extenders) {
-            params.addAll(e.getArguments());
+            params.addAll(e.getRawArguments());
         }
         if (params.isEmpty()) {
             params.add(String.format(getDebugCommand(), port));