Merge pull request #6637 from lahodaj/string-templates-write-model

Improving String templates support in the code gen.
diff --git a/java/java.source.base/apichanges.xml b/java/java.source.base/apichanges.xml
index 27f23bf..8b55514 100644
--- a/java/java.source.base/apichanges.xml
+++ b/java/java.source.base/apichanges.xml
@@ -25,6 +25,18 @@
     <apidef name="javasource_base">Java Source API</apidef>
 </apidefs>
 <changes>
+    <change id="TreeMaker.StringTemplate">
+        <api name="javasource_base" />
+        <summary>Adding TreeMaker.StringTemplate</summary>
+        <version major="1" minor="2.65.0"/>
+        <date day="3" month="11" year="2023"/>
+        <author login="jlahoda"/>
+        <compatibility addition="yes" binary="compatible" source="compatible"/>
+        <description>
+                Adding TreeMaker.StringTemplate
+        </description>
+        <class name="TreeMaker" package="org.netbeans.api.java.source"/>
+    </change>
     <change id="SourceUtils.getFile">
         <api name="javasource_base" />
         <summary>Source file name for Element</summary>
diff --git a/java/java.source.base/nbproject/project.properties b/java/java.source.base/nbproject/project.properties
index 5a122a5..59475c5 100644
--- a/java/java.source.base/nbproject/project.properties
+++ b/java/java.source.base/nbproject/project.properties
@@ -23,7 +23,7 @@
 javadoc.title=Java Source Base
 javadoc.arch=${basedir}/arch.xml
 javadoc.apichanges=${basedir}/apichanges.xml
-spec.version.base=2.64.0
+spec.version.base=2.65.0
 test.qa-functional.cp.extra=${refactoring.java.dir}/modules/ext/nb-javac-api.jar
 test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:\
     ${o.n.core.dir}/lib/boot.jar:\
diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java b/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java
index 74a4e8b..8eeca54 100644
--- a/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java
+++ b/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java
@@ -3916,4 +3916,17 @@
     public LambdaExpressionTree setLambdaBody(LambdaExpressionTree method, Tree newBody) {
         return delegate.setLambdaBody(method, newBody);
     }
+
+    /**Creates a new string template expression from the given parameters.
+     *
+     * @param processor the processor of the string template
+     * @param fragments the template fragments
+     * @param expressions the template expressions
+     * @return the string template instance
+     * @since 2.65
+     */
+    public StringTemplateTree StringTemplate(ExpressionTree processor, List<String> fragments, List<? extends ExpressionTree> expressions) {
+        return delegate.StringTemplate(processor, fragments, expressions);
+    }
+
 }
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java
index 9c94b6f..23851bb 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java
@@ -116,6 +116,7 @@
 import org.netbeans.modules.java.source.parsing.FileObjects;
 import org.netbeans.modules.java.source.parsing.JavacParser;
 import org.netbeans.modules.java.source.save.CasualDiff;
+import org.netbeans.modules.java.source.save.CasualDiff.StringTemplateFragmentTree;
 import org.netbeans.modules.java.source.save.DiffContext;
 import org.netbeans.modules.java.source.save.PositionEstimator;
 import org.netbeans.modules.java.source.save.Reformatter;
@@ -1865,7 +1866,7 @@
         if (   diffContext != null
             && diffContext.origUnit != null
             && (start = diffContext.trees.getSourcePositions().getStartPosition(diffContext.origUnit, tree)) >= 0 //#137564
-            && (end = diffContext.trees.getSourcePositions().getEndPosition(diffContext.origUnit, tree)) >= 0
+            && (end = diffContext.getEndPosition(diffContext.origUnit, tree)) >= 0
             && origText != null) {
             print(origText.substring((int) start, (int) end));
             return ;
@@ -1873,7 +1874,7 @@
         if (   diffContext != null
             && diffContext.mainUnit != null
             && (start = diffContext.trees.getSourcePositions().getStartPosition(diffContext.mainUnit, tree)) >= 0 //#137564
-            && (end = diffContext.trees.getSourcePositions().getEndPosition(diffContext.mainUnit, tree)) >= 0
+            && (end = diffContext.getEndPosition(diffContext.mainUnit, tree)) >= 0
             && diffContext.mainCode != null) {
             print(diffContext.mainCode.substring((int) start, (int) end));
             return ;
@@ -1899,7 +1900,22 @@
 	    break;
 	   case CLASS:
              if (tree.value instanceof String) {
-                 print("\"" + quote((String) tree.value, '\'') + "\"");
+                 String leading;
+                 String trailing;
+                 if (tree instanceof StringTemplateFragmentTree) {
+                     StringTemplateFragmentTree stf = (StringTemplateFragmentTree) tree;
+                     switch (stf.fragmentKind) {
+                         case START: leading = "\""; trailing = "\\{"; break;
+                         case MIDDLE: leading = "}"; trailing = "\\{"; break;
+                         case END: leading = "}"; trailing = "\""; break;
+                         default: throw new IllegalStateException(stf.fragmentKind.name());
+                     }
+                 } else {
+                     leading = trailing = "\"";
+                 }
+                 print(leading);
+                 print(quote((String) tree.value, '\''));
+                 print(trailing);
              } else if (tree.value instanceof String[]) {
                  int indent = out.col;
                  print("\"\"\"");
@@ -2077,6 +2093,31 @@
     }
 
     @Override
+    public void visitStringTemplate(JCStringTemplate tree) {
+        printExpr(tree.processor, TreeInfo.postfixPrec);
+        print('.');
+
+        Iterator<? extends String> fragmentIt = tree.fragments.iterator();
+        Iterator<? extends JCExpression> expressionIt = tree.expressions.iterator();
+        boolean start = true;
+
+        while (expressionIt.hasNext()) {
+            if (start) {
+                print("\"");
+            } else {
+                print("}");
+            }
+            print(quote(fragmentIt.next(), '\''));
+            print("\\{");
+            print(expressionIt.next());
+            start = false;
+        }
+        print("}");
+        print(quote(fragmentIt.next(), '\''));
+        print("\"");
+    }
+
+    @Override
     public void visitLetExpr(LetExpr tree) {
 	print("(let " + tree.defs + " in " + tree.expr + ")");
     }
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java
index a4df3d1..78846b4 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java
@@ -109,6 +109,7 @@
 import com.sun.tools.javac.tree.JCTree.JCRequires;
 import com.sun.tools.javac.tree.JCTree.JCReturn;
 import com.sun.tools.javac.tree.JCTree.JCStatement;
+import com.sun.tools.javac.tree.JCTree.JCStringTemplate;
 import com.sun.tools.javac.tree.JCTree.JCSwitch;
 import com.sun.tools.javac.tree.JCTree.JCSwitchExpression;
 import com.sun.tools.javac.tree.JCTree.JCSynchronized;
@@ -128,6 +129,7 @@
 import com.sun.tools.javac.tree.JCTree.TypeBoundKind;
 import com.sun.tools.javac.tree.Pretty;
 import com.sun.tools.javac.tree.TreeInfo;
+import com.sun.tools.javac.tree.TreeMaker;
 import com.sun.tools.javac.util.Context;
 import com.sun.tools.javac.util.ListBuffer;
 import com.sun.tools.javac.util.Name;
@@ -173,6 +175,7 @@
 import org.openide.util.NbBundle;
 import org.openide.util.NbCollections;
 import javax.lang.model.type.TypeKind;
+import org.netbeans.modules.java.source.save.CasualDiff.StringTemplateFragmentTree.FragmentKind;
 import org.netbeans.modules.java.source.transform.TreeHelpers;
 
 public class CasualDiff {
@@ -190,6 +193,7 @@
     private VeryPretty printer;
     private final Context context;
     private final Names names;
+    private final TreeMaker make;
     private static final Logger LOG = Logger.getLogger(CasualDiff.class.getName());
     public static final int GENERATED_MEMBER = 1<<24;
 
@@ -223,6 +227,7 @@
         this.origText = diffContext.origText;
         this.context = context;
         this.names = Names.instance(context);
+        this.make = TreeMaker.instance(context);
         this.tree2Tag = tree2Tag;
         this.tree2Doc = tree2Doc;
         this.tag2Span = (Map<Object, int[]>) tag2Span;//XXX
@@ -539,7 +544,7 @@
             VariableTree vt = fgt.getVariables().get(fgt.getVariables().size() - 1);
             return TreeInfo.getEndPos((JCTree)vt, oldTopLevel.endPositions);
         }
-        int endPos = TreeInfo.getEndPos(t, oldTopLevel.endPositions);
+        int endPos = diffContext.getEndPosition(oldTopLevel, t);
 
         if (endPos == Position.NOPOS) {
             if (t instanceof JCAssign) {
@@ -1987,6 +1992,61 @@
         return bounds[1];
     }
 
+    protected int diffStringTemplate(JCStringTemplate oldT, JCStringTemplate newT, int[] bounds) {
+        int localPointer = bounds[0];
+
+        // processor
+        int[] processorBounds = getBounds(oldT.processor);
+        copyTo(localPointer, processorBounds[0]);
+        localPointer = diffTree(oldT.processor, newT.processor, processorBounds);
+
+        tokenSequence.move(processorBounds[1]);
+        do { } while (tokenSequence.moveNext() && JavaTokenId.DOT != tokenSequence.token().id());
+        tokenSequence.moveNext();
+        copyTo(localPointer, localPointer = tokenSequence.offset());
+
+        // expressions
+        List<? extends JCExpression> oldFragmentsAndExpressions = zipFragmentsAndExpressions(oldT, localPointer);
+        List<? extends JCExpression> newFragmentsAndExpressions = zipFragmentsAndExpressions(newT, NOPOS);
+        PositionEstimator est = EstimatorFactory.stringTemplate(oldFragmentsAndExpressions, newFragmentsAndExpressions, diffContext);
+        localPointer = diffList(oldFragmentsAndExpressions, newFragmentsAndExpressions, localPointer, est, Measure.REAL_MEMBER, printer);
+        copyTo(localPointer, bounds[1]);
+        return bounds[1];
+    }
+
+    private List<JCExpression> zipFragmentsAndExpressions(JCStringTemplate template, int pos) {
+        ListBuffer<JCExpression> result = new ListBuffer<>();
+        Iterator<? extends String> fragmentIt = template.fragments.iterator();
+        Iterator<? extends JCExpression> expressionIt = template.expressions.iterator();
+
+        while (fragmentIt.hasNext()) {
+            JCExpression expression = expressionIt.hasNext() ? expressionIt.next() : null;
+            FragmentKind fragmentKind = result.isEmpty() ? FragmentKind.START
+                                                         : expression != null ? FragmentKind.MIDDLE
+                                                                              : FragmentKind.END;
+            JCLiteral literal = new StringTemplateFragmentTree(TypeTag.CLASS, fragmentIt.next(), fragmentKind);
+
+            if (pos != NOPOS) {
+                tokenSequence.move(pos);
+
+                if (tokenSequence.moveNext() && tokenSequence.token().id() == JavaTokenId.STRING_LITERAL) {
+                    literal.pos = tokenSequence.offset();
+                    diffContext.syntheticEndPositions.put(literal, literal.pos + tokenSequence.token().length());
+                }
+
+                pos = expression != null ? endPos(expression) : NOPOS;
+            }
+
+            result.append(literal);
+
+            if (expression != null) {
+                result.append(expression);
+            }
+        }
+
+        return result.toList();
+    }
+
     protected int diffConstantCaseLabel(JCConstantCaseLabel oldT, JCConstantCaseLabel newT, int[] bounds) {
         return diffTree((JCTree) oldT.expr, (JCTree) newT.expr, bounds);
     }
@@ -3372,7 +3432,7 @@
         assert oldList != null && newList != null;
         int lastOldPos = initialPos;
 
-        ListMatcher<JCTree> matcher = ListMatcher.<JCTree>instance(oldList, newList);
+        ListMatcher<JCTree> matcher = ListMatcher.<JCTree>instance(oldList, newList, null);
         if (!matcher.match()) {
             return initialPos;
         }
@@ -5753,6 +5813,9 @@
           case RECORDPATTERN:
               retVal = diffRecordPattern((JCRecordPattern) oldT, (JCRecordPattern) newT, elementBounds);
               break;
+          case STRING_TEMPLATE:
+              retVal = diffStringTemplate((JCStringTemplate) oldT, (JCStringTemplate) newT, elementBounds);
+              break;
           default:
               // handle special cases like field groups and enum constants
               if (oldT.getKind() == Kind.OTHER) {
@@ -6380,4 +6443,20 @@
         }
         return -1;
     }
+
+    public static final class StringTemplateFragmentTree extends JCLiteral {
+
+        public final FragmentKind fragmentKind;
+
+        public StringTemplateFragmentTree(TypeTag typetag, Object value, FragmentKind kind) {
+            super(typetag, value);
+            this.fragmentKind = kind;
+        }
+
+        public enum FragmentKind {
+            START,
+            MIDDLE,
+            END;
+        }
+    }
 }
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/DiffContext.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/DiffContext.java
index b6cc3bd..36b2ad6 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/save/DiffContext.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/DiffContext.java
@@ -22,8 +22,11 @@
 import com.sun.source.tree.CompilationUnitTree;
 import com.sun.source.tree.Tree;
 import com.sun.source.util.Trees;
+import com.sun.tools.javac.tree.JCTree;
 import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.tree.TreeInfo;
 import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Position;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -63,6 +66,7 @@
     public final int textLength;
     
     public final BlockSequences blockSequences;
+    public final Map<JCTree, Integer> syntheticEndPositions = new HashMap<>();
     
     /**
      * Special flag; when creating new CUs from template, always include their initial comments
@@ -136,4 +140,13 @@
         return CodeStyle.getDefault((Document)null);
     }
 
+    public int getEndPosition(JCCompilationUnit unit, JCTree t) {
+        int endPos = TreeInfo.getEndPos(t, unit.endPositions);
+
+        if (endPos == Position.NOPOS && unit == origUnit) {
+            endPos = syntheticEndPositions.getOrDefault(t, Position.NOPOS);
+        }
+
+        return endPos;
+    }
 }
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/EstimatorFactory.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/EstimatorFactory.java
index c7733a8..5a2de80 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/save/EstimatorFactory.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/EstimatorFactory.java
@@ -47,6 +47,13 @@
         return new PositionEstimator.CasePatternEstimator(oldL, newL, diffContext);
     }
     
+    static PositionEstimator stringTemplate(List<? extends Tree> oldL,
+                                          List<? extends Tree> newL,
+                                          DiffContext diffContext)
+    {
+        return new PositionEstimator.StringTemaplateEstimator(oldL, newL, diffContext);
+    }
+    
     static PositionEstimator exportsOpensTo(List<? extends ExpressionTree> oldL, 
                                     List<? extends ExpressionTree> newL,
                                     DiffContext diffContext)
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java
index 40a91a3..c6d73c3 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java
@@ -27,7 +27,9 @@
 import com.sun.source.tree.VariableTree;
 import com.sun.source.util.SourcePositions;
 import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.tree.JCTree;
 import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
+import com.sun.tools.javac.tree.TreeInfo;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashMap;
@@ -190,6 +192,37 @@
         
     }
     
+    static class StringTemaplateEstimator extends BaseEstimator {
+        StringTemaplateEstimator(List<? extends Tree> oldL, 
+                             List<? extends Tree> newL,
+                             DiffContext diffContext)
+        {
+            super(DOT, oldL, newL, diffContext);
+        }
+
+        @Override
+        public String head() {
+            return precToken.fixedText();
+        }
+
+        @Override
+        public int getInsertPos(int index) {
+            if (index == oldL.size()) {
+                return diffContext.getEndPosition(diffContext.origUnit, (JCTree) oldL.get(index - 1));
+            }
+            return (int) diffContext.trees.getSourcePositions().getStartPosition(diffContext.origUnit, oldL.get(index));
+        }
+
+        @Override
+        public int[] getPositions(int index) {
+            int start = (int) diffContext.trees.getSourcePositions().getStartPosition(diffContext.origUnit, oldL.get(index));
+            int end = diffContext.getEndPosition(diffContext.origUnit, (JCTree) oldL.get(index));
+
+            return new int[] {start, end};
+        }
+
+    }
+
     static class ExportsOpensToEstimator extends BaseEstimator {
         
         ExportsOpensToEstimator(List<? extends ExpressionTree> oldL,
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java
index f123913..86cb917 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java
@@ -3557,6 +3557,18 @@
         }
 
         @Override
+        public Boolean visitStringTemplate(StringTemplateTree node, Void p) {
+            scan(node.getProcessor(), p);
+            accept(DOT);
+            for (ExpressionTree expression : node.getExpressions()) {
+                accept(STRING_LITERAL);
+                scan(expression, p);
+            }
+            accept(STRING_LITERAL);
+            return true;
+        }
+
+        @Override
         public Boolean visitOther(Tree node, Void p) {
             do {
                 col += tokens.token().length();
diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/StringTemplateTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/StringTemplateTest.java
new file mode 100644
index 0000000..df7b4cf
--- /dev/null
+++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/StringTemplateTest.java
@@ -0,0 +1,349 @@
+/*
+ * 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.java.source.gen;
+
+import com.sun.source.tree.BlockTree;
+import com.sun.source.tree.CaseTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.StatementTree;
+import com.sun.source.tree.StringTemplateTree;
+import com.sun.source.tree.SwitchExpressionTree;
+import com.sun.source.tree.SwitchTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.Tree.Kind;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreeScanner;
+import com.sun.tools.javac.tree.JCTree;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Modifier;
+import javax.swing.event.ChangeListener;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import org.netbeans.api.java.source.JavaSource;
+import org.netbeans.api.java.source.Task;
+import org.netbeans.api.java.source.TestUtilities;
+import org.netbeans.api.java.source.TreeMaker;
+import org.netbeans.api.java.source.WorkingCopy;
+import org.netbeans.junit.NbTestSuite;
+import org.netbeans.modules.java.source.parsing.JavacParser;
+import org.netbeans.spi.java.queries.CompilerOptionsQueryImplementation;
+import org.openide.filesystems.FileObject;
+import org.openide.util.lookup.ServiceProvider;
+
+public class StringTemplateTest extends TreeRewriteTestBase {
+
+    private static final List<String> EXTRA_OPTIONS = new ArrayList<>();
+
+    public StringTemplateTest(String testName) {
+        super(testName);
+    }
+
+    public static NbTestSuite suite() {
+        NbTestSuite suite = new NbTestSuite();
+        suite.addTestSuite(StringTemplateTest.class);
+        return suite;
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        sourceLevel = "1.21";
+        JavacParser.DISABLE_SOURCE_LEVEL_DOWNGRADE = true;
+        EXTRA_OPTIONS.add("--enable-preview");
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        JavacParser.DISABLE_SOURCE_LEVEL_DOWNGRADE = false;
+
+    }
+
+    @ServiceProvider(service = CompilerOptionsQueryImplementation.class, position = 100)
+    public static class TestCompilerOptionsQueryImplementation implements CompilerOptionsQueryImplementation {
+
+        @Override
+        public CompilerOptionsQueryImplementation.Result getOptions(FileObject file) {
+            return new CompilerOptionsQueryImplementation.Result() {
+                @Override
+                public List<? extends String> getArguments() {
+                    return EXTRA_OPTIONS;
+                }
+
+                @Override
+                public void addChangeListener(ChangeListener listener) {
+                }
+
+                @Override
+                public void removeChangeListener(ChangeListener listener) {
+                }
+            };
+        }
+
+    }
+
+    public void testRenameInStringTemplate() throws Exception {
+        String code = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b) {\n"
+                + "         return STR.\"\\{a}\";\n"
+                + "     }\n"
+                + "}\n";
+        String golden = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b) {\n"
+                + "         return FMT.\"\\{b}\";\n"
+                + "     }\n"
+                + "}\n";
+
+        prepareTest("Test", code);
+
+        JavaSource js = getJavaSource();
+        assertNotNull(js);
+
+        Task<WorkingCopy> task = new Task<WorkingCopy>() {
+            public void run(WorkingCopy workingCopy) throws IOException {
+                workingCopy.toPhase(JavaSource.Phase.RESOLVED);
+                TreeMaker make = workingCopy.getTreeMaker();
+                CompilationUnitTree cut = workingCopy.getCompilationUnit();
+                new TreeScanner<Void, Void>() {
+                    @Override
+                    public Void visitIdentifier(IdentifierTree node, Void p) {
+                        if (node.getName().contentEquals("a")) {
+                            workingCopy.rewrite(node, make.Identifier("b"));
+                            return null;
+                        }
+                        if (node.getName().contentEquals("STR")) {
+                            workingCopy.rewrite(node, make.Identifier("FMT"));
+                            return null;
+                        }
+                        return super.visitIdentifier(node, p);
+                    }
+                }.scan(cut, null);
+            }
+
+        };
+
+        js.runModificationTask(task).commit();
+
+        String res = TestUtilities.copyFileToString(getTestFile());
+        //System.err.println(res);
+        assertEquals(golden, res);
+
+    }
+
+    public void testRemoveStringTemplateFragment() throws Exception {
+        String code = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b, int c) {\n"
+                + "         String s1 = STR.\"a\\{a}b\\{b}c\\{c}e\";\n"
+                + "         String s2 = STR.\"a\\{a}b\\{b}c\\{c}e\";\n"
+                + "         String s3 = STR.\"a\\{a}b\\{b}c\\{c}e\";\n"
+                + "     }\n"
+                + "}\n";
+        String golden = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b, int c) {\n"
+                + "         String s1 = STR.\"b\\{b}c\\{c}e\";\n"
+                + "         String s2 = STR.\"a\\{a}c\\{c}e\";\n"
+                + "         String s3 = STR.\"a\\{a}b\\{b}e\";\n"
+                + "     }\n"
+                + "}\n";
+
+        prepareTest("Test", code);
+
+        JavaSource js = getJavaSource();
+        assertNotNull(js);
+
+        Task<WorkingCopy> task = new Task<WorkingCopy>() {
+            public void run(WorkingCopy workingCopy) throws IOException {
+                workingCopy.toPhase(JavaSource.Phase.RESOLVED);
+                TreeMaker make = workingCopy.getTreeMaker();
+                CompilationUnitTree cut = workingCopy.getCompilationUnit();
+                new TreeScanner<Void, Void>() {
+                    @Override
+                    public Void visitVariable(VariableTree node, Void p) {
+                        CONT: if (node.getInitializer() != null) {
+                            int toDelete;
+                            switch (node.getName().toString()) {
+                                case "s1": toDelete = 0; break;
+                                case "s2": toDelete = 1; break;
+                                case "s3": toDelete = 2; break;
+                                default: break CONT;
+                            }
+
+                            StringTemplateTree template = (StringTemplateTree) node.getInitializer();
+                            List<String> fragments = new ArrayList<>(template.getFragments());
+                            List<? extends ExpressionTree> expressions = new ArrayList<>(template.getExpressions());
+
+                            fragments.remove(toDelete);
+                            expressions.remove(toDelete);
+
+                            workingCopy.rewrite(template, make.StringTemplate(template.getProcessor(), fragments, expressions));
+                        }
+                        return super.visitVariable(node, p);
+                    }
+                }.scan(cut, null);
+            }
+
+        };
+
+        js.runModificationTask(task).commit();
+
+        String res = TestUtilities.copyFileToString(getTestFile());
+        //System.err.println(res);
+        assertEquals(golden, res);
+
+    }
+
+    public void testAddStringTemplateFragment() throws Exception {
+        String code = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b, int c) {\n"
+                + "         String s1 = STR.\"0\\{0}1\\{1}e\";\n"
+                + "         String s2 = STR.\"0\\{0}1\\{1}e\";\n"
+                + "         String s3 = STR.\"0\\{0}1\\{1}e\";\n"
+                + "     }\n"
+                + "}\n";
+        String golden = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b, int c) {\n"
+                + "         String s1 = STR.\"a\\{a}0\\{0}1\\{1}e\";\n"
+                + "         String s2 = STR.\"0\\{0}a\\{a}1\\{1}e\";\n"
+                + "         String s3 = STR.\"0\\{0}1\\{1}a\\{a}e\";\n"
+                + "     }\n"
+                + "}\n";
+
+        prepareTest("Test", code);
+
+        JavaSource js = getJavaSource();
+        assertNotNull(js);
+
+        Task<WorkingCopy> task = new Task<WorkingCopy>() {
+            public void run(WorkingCopy workingCopy) throws IOException {
+                workingCopy.toPhase(JavaSource.Phase.RESOLVED);
+                TreeMaker make = workingCopy.getTreeMaker();
+                CompilationUnitTree cut = workingCopy.getCompilationUnit();
+                new TreeScanner<Void, Void>() {
+                    @Override
+                    public Void visitVariable(VariableTree node, Void p) {
+                        CONT: if (node.getInitializer() != null) {
+                            int insertPos;
+                            switch (node.getName().toString()) {
+                                case "s1": insertPos = 0; break;
+                                case "s2": insertPos = 1; break;
+                                case "s3": insertPos = 2; break;
+                                default: break CONT;
+                            }
+
+                            StringTemplateTree template = (StringTemplateTree) node.getInitializer();
+                            List<String> fragments = new ArrayList<>(template.getFragments());
+                            List<ExpressionTree> expressions = new ArrayList<>(template.getExpressions());
+
+                            fragments.add(insertPos, "a");
+                            expressions.add(insertPos, make.Identifier("a"));
+
+                            workingCopy.rewrite(template, make.StringTemplate(template.getProcessor(), fragments, expressions));
+                        }
+                        return super.visitVariable(node, p);
+                    }
+                }.scan(cut, null);
+            }
+
+        };
+
+        js.runModificationTask(task).commit();
+
+        String res = TestUtilities.copyFileToString(getTestFile());
+        //System.err.println(res);
+        assertEquals(golden, res);
+
+    }
+
+    public void testNewStringTemplate() throws Exception {
+        String code = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b, int c) {\n"
+                + "         String s;\n"
+                + "     }\n"
+                + "}\n";
+        String golden = "package test; \n"
+                + "public class Test {\n"
+                + "     private String test(int a, int b, int c) {\n"
+                + "         String s = STR.\"a\\{a}b\\{b}c\\{c}e\";\n"
+                + "     }\n"
+                + "}\n";
+
+        prepareTest("Test", code);
+
+        JavaSource js = getJavaSource();
+        assertNotNull(js);
+
+        Task<WorkingCopy> task = new Task<WorkingCopy>() {
+            public void run(WorkingCopy workingCopy) throws IOException {
+                workingCopy.toPhase(JavaSource.Phase.RESOLVED);
+                TreeMaker make = workingCopy.getTreeMaker();
+                CompilationUnitTree cut = workingCopy.getCompilationUnit();
+                new TreeScanner<Void, Void>() {
+                    @Override
+                    public Void visitVariable(VariableTree node, Void p) {
+                        if (node.getName().contentEquals("s")) {
+                            List<String> fragments = new ArrayList<>();
+                            List<ExpressionTree> expressions = new ArrayList<>();
+
+                            fragments.add("a");
+                            expressions.add(make.Identifier("a"));
+                            fragments.add("b");
+                            expressions.add(make.Identifier("b"));
+                            fragments.add("c");
+                            expressions.add(make.Identifier("c"));
+                            fragments.add("e");
+
+                            workingCopy.rewrite(node, make.setInitialValue(node, make.StringTemplate(make.Identifier("STR"), fragments, expressions)));
+                        }
+                        return super.visitVariable(node, p);
+                    }
+                }.scan(cut, null);
+            }
+
+        };
+
+        js.runModificationTask(task).commit();
+
+        String res = TestUtilities.copyFileToString(getTestFile());
+        //System.err.println(res);
+        assertEquals(golden, res);
+
+    }
+
+}
diff --git a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/FormatingTest.java b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/FormatingTest.java
index c11be84..fc0911d 100644
--- a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/FormatingTest.java
+++ b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/FormatingTest.java
@@ -6753,6 +6753,32 @@
         reformat(doc, content, golden);
     }
   
+    public void testStringTemplate() throws Exception {
+        testFile = new File(getWorkDir(), "Test.java");
+        TestUtilities.copyStringToFile(testFile, "");
+        FileObject testSourceFO = FileUtil.toFileObject(testFile);
+        DataObject testSourceDO = DataObject.find(testSourceFO);
+        EditorCookie ec = (EditorCookie) testSourceDO.getCookie(EditorCookie.class);
+        final Document doc = ec.openDocument();
+        doc.putProperty(Language.class, JavaTokenId.language());
+        String content
+                = "\n"
+                + "class Test {\n\n"
+                + "    private String t() {\n"
+                + "        return STR.\"a\\{1 + 2}b\";\n"
+                + "    }\n"
+                + "}\n";
+
+        String golden
+                = "\n"
+                + "class Test {\n\n"
+                + "    private String t() {\n"
+                + "        return STR.\"a\\{1 + 2}b\";\n"
+                + "    }\n"
+                + "}\n";
+        reformat(doc, content, golden);
+    }
+
     private void reformat(Document doc, String content, String golden) throws Exception {
         reformat(doc, content, golden, 0, content.length());
     }