1 /*
2  * TextEditingTargetRMarkdownHelper.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.workbench.views.source.editors.text;
16 
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.HashMap;
20 import java.util.List;
21 
22 import com.google.gwt.core.client.JsArray;
23 import com.google.gwt.core.client.JsArrayString;
24 import com.google.gwt.event.shared.HandlerRegistration;
25 import com.google.gwt.user.client.Command;
26 import com.google.inject.Inject;
27 
28 import org.rstudio.core.client.CommandWithArg;
29 import org.rstudio.core.client.Debug;
30 import org.rstudio.core.client.JsArrayUtil;
31 import org.rstudio.core.client.MessageDisplay;
32 import org.rstudio.core.client.StringUtil;
33 import org.rstudio.core.client.files.FileSystemItem;
34 import org.rstudio.core.client.widget.Operation;
35 import org.rstudio.core.client.widget.OperationWithInput;
36 import org.rstudio.core.client.widget.ProgressIndicator;
37 import org.rstudio.studio.client.RStudioGinjector;
38 import org.rstudio.studio.client.application.events.EventBus;
39 import org.rstudio.studio.client.common.ConsoleDispatcher;
40 import org.rstudio.studio.client.common.GlobalDisplay;
41 import org.rstudio.studio.client.common.GlobalProgressDelayer;
42 import org.rstudio.studio.client.common.SimpleRequestCallback;
43 import org.rstudio.studio.client.common.dependencies.DependencyManager;
44 import org.rstudio.studio.client.common.filetypes.FileTypeCommands;
45 import org.rstudio.studio.client.common.filetypes.TextFileType;
46 import org.rstudio.studio.client.notebookv2.CompileNotebookv2Options;
47 import org.rstudio.studio.client.notebookv2.CompileNotebookv2OptionsDialog;
48 import org.rstudio.studio.client.notebookv2.CompileNotebookv2Prefs;
49 import org.rstudio.studio.client.rmarkdown.RmdOutput;
50 import org.rstudio.studio.client.rmarkdown.events.RenderRmdEvent;
51 import org.rstudio.studio.client.rmarkdown.events.RenderRmdSourceEvent;
52 import org.rstudio.studio.client.rmarkdown.events.RmdParamsReadyEvent;
53 import org.rstudio.studio.client.rmarkdown.model.RMarkdownContext;
54 import org.rstudio.studio.client.rmarkdown.model.RMarkdownServerOperations;
55 import org.rstudio.studio.client.rmarkdown.model.RmdChosenTemplate;
56 import org.rstudio.studio.client.rmarkdown.model.RmdCreatedTemplate;
57 import org.rstudio.studio.client.rmarkdown.model.RmdExecutionState;
58 import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatter;
59 import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatterOutputOptions;
60 import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat;
61 import org.rstudio.studio.client.rmarkdown.model.RmdTemplate;
62 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateContent;
63 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateData;
64 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateFormat;
65 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateFormatOption;
66 import org.rstudio.studio.client.rmarkdown.model.RmdYamlData;
67 import org.rstudio.studio.client.rmarkdown.model.RmdYamlResult;
68 import org.rstudio.studio.client.rmarkdown.model.YamlTree;
69 import org.rstudio.studio.client.server.ServerError;
70 import org.rstudio.studio.client.server.ServerRequestCallback;
71 import org.rstudio.studio.client.server.Void;
72 import org.rstudio.studio.client.workbench.WorkbenchContext;
73 import org.rstudio.studio.client.workbench.model.Session;
74 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
75 import org.rstudio.studio.client.workbench.prefs.model.UserState;
76 import org.rstudio.studio.client.workbench.views.files.model.FilesServerOperations;
77 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
78 import org.rstudio.studio.client.workbench.views.source.editors.text.ui.NewRMarkdownDialog;
79 import org.rstudio.studio.client.workbench.views.source.events.FileEditEvent;
80 import org.rstudio.studio.client.workbench.views.source.model.DocUpdateSentinel;
81 import org.rstudio.studio.client.workbench.views.source.model.SourceDocument;
82 
83 public class TextEditingTargetRMarkdownHelper
84 {
85    public class RmdSelectedTemplate
86    {
RmdSelectedTemplate(RmdTemplate template, String format, boolean isShiny)87       public RmdSelectedTemplate (RmdTemplate template, String format,
88                                   boolean isShiny)
89       {
90          this.template = template;
91          this.format = format;
92          this.isShiny = isShiny;
93       }
94 
95       RmdTemplate template;
96       String format;
97       boolean isShiny;
98    }
99 
TextEditingTargetRMarkdownHelper()100    public TextEditingTargetRMarkdownHelper()
101    {
102       RStudioGinjector.INSTANCE.injectMembers(this);
103    }
104 
105    @Inject
initialize(Session session, GlobalDisplay globalDisplay, EventBus eventBus, UserPrefs prefs, UserState state, ConsoleDispatcher consoleDispatcher, WorkbenchContext workbenchContext, FileTypeCommands fileTypeCommands, DependencyManager dependencyManager, RMarkdownServerOperations server, FilesServerOperations fileServer)106    public void initialize(Session session,
107                           GlobalDisplay globalDisplay,
108                           EventBus eventBus,
109                           UserPrefs prefs,
110                           UserState state,
111                           ConsoleDispatcher consoleDispatcher,
112                           WorkbenchContext workbenchContext,
113                           FileTypeCommands fileTypeCommands,
114                           DependencyManager dependencyManager,
115                           RMarkdownServerOperations server,
116                           FilesServerOperations fileServer)
117    {
118       session_ = session;
119       fileTypeCommands_ = fileTypeCommands;
120       globalDisplay_ = globalDisplay;
121       eventBus_ = eventBus;
122       prefs_ = prefs;
123       state_ = state;
124       consoleDispatcher_ = consoleDispatcher;
125       workbenchContext_ = workbenchContext;
126       dependencyManager_ = dependencyManager;
127       server_ = server;
128       fileServer_ = fileServer;
129    }
130 
detectExtendedType(String contents, String extendedType, TextFileType fileType)131    public String detectExtendedType(String contents,
132                                     String extendedType,
133                                     TextFileType fileType)
134    {
135       if (extendedType.length() == 0 &&
136           fileType.isMarkdown() &&
137           useRMarkdownV2(contents))
138       {
139          return SourceDocument.XT_RMARKDOWN_DOCUMENT;
140       }
141       else
142       {
143          return extendedType;
144       }
145    }
146 
withRMarkdownPackage( final String userAction, final boolean isShinyDoc, final CommandWithArg<RMarkdownContext> onReady)147    public void withRMarkdownPackage(
148           final String userAction,
149           final boolean isShinyDoc,
150           final CommandWithArg<RMarkdownContext> onReady)
151    {
152       withRMarkdownPackage("R Markdown", userAction, isShinyDoc, onReady);
153    }
154 
withRMarkdownPackage( String progressCaption, final String userAction, final boolean isShinyDoc, final CommandWithArg<RMarkdownContext> onReady)155    public void withRMarkdownPackage(
156           String progressCaption,
157           final String userAction,
158           final boolean isShinyDoc,
159           final CommandWithArg<RMarkdownContext> onReady)
160    {
161       dependencyManager_.withRMarkdown(
162          progressCaption,
163          userAction,
164          new Command() {
165 
166             @Override
167             public void execute()
168             {
169                // command to execute when we are ready
170                Command callReadyCommand = new Command() {
171                   @Override
172                   public void execute()
173                   {
174                      server_.getRMarkdownContext(
175                         new SimpleRequestCallback<RMarkdownContext>() {
176 
177                            @Override
178                            public void onResponseReceived(RMarkdownContext ctx)
179                            {
180                               if (onReady != null)
181                                  onReady.execute(ctx);
182                            }
183                         });
184                   }
185                };
186 
187                // check if this is a Shiny Doc
188                if (isShinyDoc)
189                {
190                   dependencyManager_.withShiny("Running shiny documents",
191                                                callReadyCommand);
192                }
193                else
194                {
195                   callReadyCommand.execute();
196                }
197             }
198          });
199    }
200 
renderNotebookv2(final DocUpdateSentinel sourceDoc, final String viewerType)201    public void renderNotebookv2(final DocUpdateSentinel sourceDoc,
202          final String viewerType)
203    {
204       withRMarkdownPackage("Compiling notebooks from R scripts",
205                            false,
206          new CommandWithArg<RMarkdownContext>() {
207             @Override
208             public void execute(RMarkdownContext arg)
209             {
210                // see if we already have a format defined
211                server_.rmdOutputFormat(sourceDoc.getPath(),
212                                        sourceDoc.getEncoding(),
213                                        new SimpleRequestCallback<String>() {
214                      @Override
215                      public void onResponseReceived(String format)
216                      {
217                         if (format == null)
218                            renderNotebookv2WithDialog(sourceDoc);
219                         else
220                            renderNotebookv2(sourceDoc, format, viewerType);
221                      }
222                });
223             }
224           });
225    }
226 
227    final String NOTEBOOK_FORMAT = "notebook_format";
228 
renderNotebookv2WithDialog(final DocUpdateSentinel sourceDoc)229    private void renderNotebookv2WithDialog(final DocUpdateSentinel sourceDoc)
230    {
231       // default format
232       String format = sourceDoc.getProperty(NOTEBOOK_FORMAT);
233       if (StringUtil.isNullOrEmpty(format))
234       {
235          format = state_.compileRMarkdownNotebookPrefs()
236                                              .getValue().getFormat();
237          if (StringUtil.isNullOrEmpty(format))
238             format = CompileNotebookv2Options.FORMAT_DEFAULT;
239       }
240 
241       CompileNotebookv2OptionsDialog dialog =
242             new CompileNotebookv2OptionsDialog(
243                   format,
244                   new OperationWithInput<CompileNotebookv2Options>()
245       {
246          @Override
247          public void execute(CompileNotebookv2Options input)
248          {
249             renderNotebookv2(sourceDoc, input.getFormat(), null);
250 
251             // save options for this document
252             HashMap<String, String> changedProperties = new HashMap<>();
253             changedProperties.put(NOTEBOOK_FORMAT, input.getFormat());
254             sourceDoc.modifyProperties(changedProperties, null);
255 
256             // save global prefs
257             CompileNotebookv2Prefs prefs =
258                   CompileNotebookv2Prefs.create(input.getFormat());
259             if (!CompileNotebookv2Prefs.areEqual(
260                   prefs,
261                   state_.compileRMarkdownNotebookPrefs().getValue().cast()))
262             {
263                state_.compileRMarkdownNotebookPrefs().setGlobalValue(prefs.cast());
264                state_.writeState();
265             }
266          }
267       }
268       );
269       dialog.showModal();
270    }
271 
renderNotebookv2(final DocUpdateSentinel sourceDoc, String format, String viewerType)272    private void renderNotebookv2(final DocUpdateSentinel sourceDoc,
273                                  String format, String viewerType)
274    {
275       eventBus_.fireEvent(new RenderRmdEvent(sourceDoc.getPath(),
276                                              1,
277                                              format,
278                                              sourceDoc.getEncoding(),
279                                              null,
280                                              false,
281                                              RmdOutput.TYPE_STATIC,
282                                              null,
283                                              getKnitWorkingDir(sourceDoc),
284                                              viewerType));
285    }
286 
getKnitWorkingDir(DocUpdateSentinel sourceDoc)287    public String getKnitWorkingDir(DocUpdateSentinel sourceDoc)
288    {
289       // shortcut if we don't support manually specified working directories
290       if (!session_.getSessionInfo().getKnitWorkingDirAvailable())
291          return null;
292 
293       // compute desired working directory type
294       String workingDirType = sourceDoc.getProperty(
295             RenderRmdEvent.WORKING_DIR_PROP,
296             prefs_.knitWorkingDir().getValue());
297 
298       String workingDir = null;
299       if (workingDirType == UserPrefs.KNIT_WORKING_DIR_PROJECT)
300       {
301          // get the project directory, but if we don't have one (e.g. no
302          // project) use the default working directory for the session
303          FileSystemItem projectDir =
304                session_.getSessionInfo().getActiveProjectDir();
305          if (projectDir != null)
306             workingDir = projectDir.getPath();
307          if (StringUtil.isNullOrEmpty(workingDir))
308             workingDir = session_.getSessionInfo().getDefaultWorkingDir();
309       }
310       else if (workingDirType == UserPrefs.KNIT_WORKING_DIR_CURRENT)
311       {
312          workingDir = workbenchContext_.getCurrentWorkingDir().getPath();
313       }
314       return workingDir;
315    }
316 
renderRMarkdown(final String sourceFile, final int sourceLine, final String format, final String encoding, final String paramsFile, final boolean asTempfile, final int type, final boolean asShiny, final String workingDir, final String viewerType)317    public void renderRMarkdown(final String sourceFile,
318                                final int sourceLine,
319                                final String format,
320                                final String encoding,
321                                final String paramsFile,
322                                final boolean asTempfile,
323                                final int type,
324                                final boolean asShiny,
325                                final String workingDir,
326                                final String viewerType)
327    {
328       withRMarkdownPackage(type == RmdOutput.TYPE_NOTEBOOK ?
329                               "R Notebook" :
330                               "R Markdown",
331                            "Rendering R Markdown documents",
332                            type == RmdOutput.TYPE_SHINY,
333                            new CommandWithArg<RMarkdownContext>() {
334          @Override
335          public void execute(RMarkdownContext arg)
336          {
337             eventBus_.fireEvent(new RenderRmdEvent(sourceFile,
338                                                    sourceLine,
339                                                    format,
340                                                    encoding,
341                                                    paramsFile,
342                                                    asTempfile,
343                                                    type,
344                                                    null,
345                                                    workingDir,
346                                                    viewerType));
347          }
348       });
349    }
350 
renderRMarkdownSource(final String source, final boolean isShinyDoc)351    public void renderRMarkdownSource(final String source,
352                                      final boolean isShinyDoc)
353    {
354       withRMarkdownPackage("Rendering R Markdown documents",
355                            isShinyDoc,
356             new CommandWithArg<RMarkdownContext>() {
357          @Override
358          public void execute(RMarkdownContext arg)
359          {
360             eventBus_.fireEvent(new RenderRmdSourceEvent(source));
361          }
362       });
363    }
364 
365 
prepareForRmdChunkExecution(String id, String contents, final Command onExecuteChunk)366    public void prepareForRmdChunkExecution(String id,
367                                         String contents,
368                                         final Command onExecuteChunk)
369    {
370       // if this is R Markdown v2 then look for params
371       if (useRMarkdownV2(contents))
372       {
373          server_.prepareForRmdChunkExecution(id,
374             new ServerRequestCallback<RmdExecutionState>()
375          {
376             @Override
377             public void onResponseReceived(RmdExecutionState state)
378             {
379                onExecuteChunk.execute();
380             }
381 
382             @Override
383             public void onError(ServerError error)
384             {
385                Debug.logError(error);
386             }
387          });
388       }
389       else
390       {
391          onExecuteChunk.execute();
392       }
393    }
394 
395 
verifyPrerequisites(WarningBarDisplay display, TextFileType fileType)396    public boolean verifyPrerequisites(WarningBarDisplay display,
397                                       TextFileType fileType)
398    {
399       return verifyPrerequisites(null, display, fileType);
400    }
401 
verifyPrerequisites(String feature, WarningBarDisplay display, TextFileType fileType)402    public boolean verifyPrerequisites(String feature,
403                                       WarningBarDisplay display,
404                                       TextFileType fileType)
405    {
406       if (feature == null)
407          feature = fileType.getLabel();
408 
409       // if this file requires knitr then validate pre-reqs
410       boolean haveRMarkdown =
411          fileTypeCommands_.getHTMLCapabiliites().isRMarkdownSupported();
412       if (!haveRMarkdown)
413       {
414          if (fileType.isRpres())
415          {
416             showKnitrPreviewWarning(display, "R Presentations", "1.2");
417             return false;
418          }
419          else if (fileType.requiresKnit() &&
420                   !session_.getSessionInfo().getRMarkdownPackageAvailable())
421          {
422             showKnitrPreviewWarning(display, feature, "1.2");
423             return false;
424          }
425       }
426 
427       return true;
428    }
429 
frontMatterToYAML(RmdFrontMatter input, final String format, final CommandWithArg<String> onFinished)430    public void frontMatterToYAML(RmdFrontMatter input,
431                                  final String format,
432                                  final CommandWithArg<String> onFinished)
433    {
434       server_.convertToYAML(input, new ServerRequestCallback<RmdYamlResult>()
435       {
436          @Override
437          public void onResponseReceived(RmdYamlResult yamlResult)
438          {
439             YamlTree yamlTree = new YamlTree(yamlResult.getYaml());
440 
441             // quote fields
442             quoteField(yamlTree, "title");
443             quoteField(yamlTree, "author");
444             quoteField(yamlTree, "date");
445 
446             // Order the fields more semantically
447             yamlTree.reorder(
448                   Arrays.asList("title", "author", "date", "output"));
449 
450             // Bring the chosen format to the top
451             if (format != null)
452                yamlTree.reorder(Arrays.asList(format));
453             onFinished.execute(yamlTree.toString());
454          }
455          @Override
456          public void onError(ServerError error)
457          {
458             onFinished.execute("");
459          }
460          private void quoteField(YamlTree yamlTree, String field)
461          {
462             String value = yamlTree.getKeyValue(field);
463 
464             // The string should be quoted if it's a single line.
465             if (value.length() > 0 && !value.contains("\n"))
466             {
467                if (!((value.startsWith("\"") && value.endsWith("\"")) ||
468                      (value.startsWith("'") && value.endsWith("'"))))
469                   yamlTree.setKeyValue(field, "\"" + value + "\"");
470             }
471          }
472       });
473    }
474 
convertFromYaml(String yaml, final CommandWithArg<RmdYamlData> onFinished)475    public void convertFromYaml(String yaml,
476                                final CommandWithArg<RmdYamlData> onFinished)
477    {
478       server_.convertFromYAML(yaml, new ServerRequestCallback<RmdYamlData>()
479       {
480          @Override
481          public void onResponseReceived(RmdYamlData yamlData)
482          {
483             onFinished.execute(yamlData);
484          }
485          @Override
486          public void onError(ServerError error)
487          {
488             onFinished.execute(null);
489          }
490       });
491    }
492 
493    // Return the template appropriate to the given output format
getTemplateForFormat(String outFormat)494    public RmdTemplate getTemplateForFormat(String outFormat)
495    {
496       JsArray<RmdTemplate> templates = RmdTemplateData.getTemplates();
497       for (int i = 0; i < templates.length(); i++)
498       {
499          RmdTemplateFormat format = templates.get(i).getFormat(outFormat);
500          if (format != null)
501             return templates.get(i);
502       }
503       // No template found
504       return null;
505    }
506 
507    // Return the selected template and format given the YAML front matter
getTemplateFormat(String yaml)508    public RmdSelectedTemplate getTemplateFormat(String yaml)
509    {
510       // This is in the editor load path, so guard against exceptions and log
511       // any we find without bringing down the editor. Failing to find a
512       // template here just turns off the template-specific UI format editor.
513       try
514       {
515          YamlTree tree = new YamlTree(yaml);
516          boolean isShiny = false;
517 
518          if (tree.getKeyValue(RmdFrontMatter.KNIT_KEY).length() > 0)
519             return null;
520 
521          List<String> outFormats = getOutputFormats(tree);
522 
523          // Find the template appropriate to the first output format listed.
524          // If no output format is present, assume HTML document (as the
525          // renderer does).
526          String outFormat = outFormats == null ?
527                RmdOutputFormat.OUTPUT_HTML_DOCUMENT :
528                outFormats.get(0);
529 
530          RmdTemplate template = getTemplateForFormat(outFormat);
531          if (template == null)
532             return null;
533 
534          // If this format produces HTML and is marked as Shiny, treat it as
535          // a Shiny format
536          if (template.getFormat(outFormat).getExtension() == "html" &&
537              tree.getKeyValue(RmdFrontMatter.RUNTIME_KEY) ==
538                        RmdFrontMatter.SHINY_RUNTIME)
539          {
540             isShiny = true;
541          }
542 
543          return new RmdSelectedTemplate(template, outFormat, isShiny);
544       }
545       catch (Exception e)
546       {
547          Debug.log("Warning: Exception thrown while parsing YAML:\n" + yaml);
548       }
549       return null;
550    }
551 
isRuntimeShinyPrerendered(String yaml)552    public boolean isRuntimeShinyPrerendered(String yaml)
553    {
554       String runtime = getRuntime(yaml);
555       return runtime == RmdFrontMatter.SHINY_PRERENDERED_RUNTIME ||
556              runtime == RmdFrontMatter.SHINY_RMD_RUNTIME ||
557              getIsShinyServer(yaml);
558    }
559 
isRuntimeShiny(String yaml)560    public boolean isRuntimeShiny(String yaml)
561    {
562       return getRuntime(yaml).startsWith(RmdFrontMatter.SHINY_RUNTIME) ||
563              isRuntimeShinyPrerendered(yaml);
564    }
565 
getRuntime(String yaml)566    private String getRuntime(String yaml)
567    {
568       // This is in the editor load path, so guard against exceptions and log
569       // any we find without bringing down the editor.
570       try
571       {
572          YamlTree tree = new YamlTree(yaml);
573 
574          if (tree.getKeyValue(RmdFrontMatter.KNIT_KEY).length() > 0)
575             return "";
576 
577          return tree.getKeyValue(RmdFrontMatter.RUNTIME_KEY);
578       }
579       catch (Exception e)
580       {
581          Debug.log("Warning: Exception thrown while parsing YAML:\n" + yaml);
582       }
583       return "";
584    }
585 
getIsShinyServer(String yaml)586    private boolean getIsShinyServer(String yaml)
587    {
588       // This is in the editor load path, so guard against exceptions and log
589       // any we find without bringing down the editor.
590       try
591       {
592          YamlTree tree = new YamlTree(yaml);
593 
594          if (tree.getKeyValue(RmdFrontMatter.KNIT_KEY).length() > 0)
595             return false;
596 
597          if (tree.getChildValue(RmdFrontMatter.SERVER_KEY, "type") == "shiny")
598          {
599             return true;
600          }
601          else if (tree.getKeyValue(RmdFrontMatter.SERVER_KEY) == "shiny")
602          {
603             return true;
604          }
605          else
606          {
607             return false;
608          }
609       }
610       catch (Exception e)
611       {
612          Debug.log("Warning: Exception thrown while parsing YAML:\n" + yaml);
613       }
614       return false;
615    }
616 
getCustomKnit(String yaml)617    public String getCustomKnit(String yaml)
618    {
619       // This is in the editor load path, so guard against exceptions and log
620       // any we find without bringing down the editor.
621       try
622       {
623          YamlTree tree = new YamlTree(yaml);
624          String knit = tree.getKeyValue(RmdFrontMatter.KNIT_KEY);
625          return knit;
626       }
627       catch (Exception e)
628       {
629          Debug.log("Warning: Exception thrown while parsing YAML:\n" + yaml);
630       }
631       return "";
632    }
633 
634    // Parses YAML, adds the given format option with any transferable
635    // defaults, and returns the resulting YAML
setOutputFormat(String yaml, final String format, final CommandWithArg<String> onCompleted)636    public void setOutputFormat(String yaml, final String format,
637                                final CommandWithArg<String> onCompleted)
638    {
639       // first check to see if the format is already in the YAML; if it is,
640       // we can re-order the formats without roundtripping (this is desirable
641       // because roundtripping goes through the yaml R package which can
642       // introduce unwanted mutations)
643       YamlTree tree = new YamlTree(yaml);
644       List<String> formats = getOutputFormats(tree);
645       if (formats != null && formats.contains(format))
646       {
647          tree.reorder(Arrays.asList(format));
648          onCompleted.execute(tree.toString());
649          return;
650       }
651 
652       convertFromYaml(yaml, new CommandWithArg<RmdYamlData>()
653       {
654          @Override
655          public void execute(RmdYamlData arg)
656          {
657             if (!arg.parseSucceeded())
658                onCompleted.execute(null);
659             else
660                setOutputFormat(arg.getFrontMatter(), format, onCompleted);
661          }
662       });
663    }
664 
setOuartoOutputFormat(String yaml, String format)665    public String setOuartoOutputFormat(String yaml,  String format)
666    {
667       YamlTree tree = new YamlTree(yaml);
668       List<String> formats = getQuartoOutputFormats(tree);
669       if (formats != null && formats.contains(format))
670       {
671          tree.reorder(Arrays.asList(format));
672          return tree.toString();
673       }
674       return null;
675    }
676 
createDraftFromTemplate(final RmdChosenTemplate template)677    public void createDraftFromTemplate(final RmdChosenTemplate template)
678    {
679       final String target = template.getDirectory() + "/" +
680                             template.getFileName();
681       final String targetFile = target + (template.createDir() ? "" : ".Rmd");
682       fileServer_.stat(targetFile, new ServerRequestCallback<FileSystemItem>()
683       {
684          @Override
685          public void onResponseReceived(final FileSystemItem fsi)
686          {
687             // the file doesn't exist--proceed
688             if (!fsi.exists())
689             {
690                createDraftFromTemplate(template, target);
691                return;
692             }
693 
694             // the file exists--offer to clean it up and continue.
695             globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
696                   "Overwrite " + (template.createDir() ? "Directory" : "File"),
697                   targetFile + " exists. Overwrite it?", false,
698                   new Operation()
699                   {
700                      @Override
701                      public void execute()
702                      {
703                         cleanAndCreateTemplate(template, target, fsi);
704                      }
705                   }, null, null, "Overwrite", "Cancel", false);
706          }
707 
708          @Override
709          public void onError(ServerError error)
710          {
711             // presumably the file doesn't exist, which is what we want.
712             createDraftFromTemplate(template, target);
713          }
714       });
715    }
716 
convertYamlToShinyDoc(String yaml)717    public String convertYamlToShinyDoc(String yaml)
718    {
719       YamlTree yamlTree = new YamlTree(yaml);
720       yamlTree.addYamlValue(null, "runtime", "shiny");
721 
722       return yamlTree.toString();
723    }
724 
replaceOutputFormatOptions(final String yaml, final String format, final RmdFrontMatterOutputOptions options, final OperationWithInput<String> onCompleted)725    public void replaceOutputFormatOptions(final String yaml,
726          final String format, final RmdFrontMatterOutputOptions options,
727          final OperationWithInput<String> onCompleted)
728    {
729       server_.convertToYAML(options, new ServerRequestCallback<RmdYamlResult>()
730       {
731          @Override
732          public void onResponseReceived(RmdYamlResult result)
733          {
734             boolean isDefault = options.getOptionList().length() == 0;
735             YamlTree yamlTree = new YamlTree(yaml);
736             YamlTree optionTree = new YamlTree(result.getYaml());
737             // add the output key if needed
738             if (!yamlTree.containsKey(RmdFrontMatter.OUTPUT_KEY))
739             {
740                yamlTree.addYamlValue(null, RmdFrontMatter.OUTPUT_KEY,
741                      RmdOutputFormat.OUTPUT_HTML_DOCUMENT);
742             }
743             String treeFormat = yamlTree.getKeyValue(RmdFrontMatter.OUTPUT_KEY);
744 
745             if (treeFormat == format)
746             {
747                // case 1: the output format is a simple format and we're not
748                // changing to a different format
749 
750                if (isDefault)
751                {
752                   // 1-a: if all options are still at their defaults, leave
753                   // untouched
754                }
755                else
756                {
757                   // 1-b: not all options are at defaults; replace the simple
758                   // format with an option list
759                   yamlTree.setKeyValue(RmdFrontMatter.OUTPUT_KEY, "");
760                   yamlTree.addYamlValue(RmdFrontMatter.OUTPUT_KEY, format, "");
761                   yamlTree.setKeyValue(format, optionTree);
762                }
763             }
764             else if (treeFormat.length() > 0)
765             {
766                // case 2: the output format is a simple format and we are
767                // changing it
768                if (isDefault)
769                {
770                   // case 2-a: change one simple format to another
771                   yamlTree.setKeyValue(RmdFrontMatter.OUTPUT_KEY, format);
772                }
773 
774                else
775                {
776                   // case 2-b: change a simple format to a complex one
777                   yamlTree.setKeyValue(RmdFrontMatter.OUTPUT_KEY, "");
778                   yamlTree.addYamlValue(RmdFrontMatter.OUTPUT_KEY, format, "");
779                   yamlTree.setKeyValue(format, optionTree);
780                }
781             }
782             else
783             {
784                // case 3: the output format is already not simple
785                treeFormat = yamlTree.getKeyValue(format);
786 
787                if (treeFormat == RmdFrontMatter.DEFAULT_FORMAT)
788                {
789                   if (isDefault)
790                   {
791                      // case 3-a: still at default settings
792                   }
793                   else
794                   {
795                      // case 3-b: default to complex
796                      yamlTree.setKeyValue(format, optionTree);
797                   }
798                }
799                else
800                {
801                   if (isDefault)
802                   {
803                      // case 3-c: complex to default
804                      if (yamlTree.getChildKeys(
805                            RmdFrontMatter.OUTPUT_KEY).size() == 1)
806                      {
807                         // case 3-c-i: only one format, and has default settings
808                         yamlTree.clearChildren(RmdFrontMatter.OUTPUT_KEY);
809                         yamlTree.setKeyValue(RmdFrontMatter.OUTPUT_KEY, format);
810                      }
811                      else
812                      {
813                         // case 3-c-i: multiple formats, this one's becoming
814                         // the default
815                         yamlTree.clearChildren(format);
816                         yamlTree.setKeyValue(format, RmdFrontMatter.DEFAULT_FORMAT);
817                      }
818                   }
819                   else
820                   {
821                      // case 3-d: complex to complex
822                      if (!yamlTree.containsKey(format))
823                      {
824                         yamlTree.addYamlValue(RmdFrontMatter.OUTPUT_KEY,
825                               format, "");
826                      }
827                      yamlTree.setKeyValue(format, optionTree);
828                   }
829                }
830             }
831 
832             yamlTree.reorder(Arrays.asList(format));
833             onCompleted.execute(yamlTree.toString());
834          }
835          @Override
836          public void onError(ServerError error)
837          {
838             // if we fail, return the unmodified YAML
839             onCompleted.execute(yaml);
840          }
841       });
842    }
843 
getTemplateContent( final RmdChosenTemplate template, final OperationWithInput<String> onContentReceived)844    public void getTemplateContent(
845          final RmdChosenTemplate template,
846          final OperationWithInput<String> onContentReceived)
847    {
848       server_.getRmdTemplate(template.getTemplatePath(),
849          new ServerRequestCallback<RmdTemplateContent>()
850          {
851             @Override
852             public void onResponseReceived (RmdTemplateContent content)
853             {
854                onContentReceived.execute(content.getContent());
855             }
856             @Override
857             public void onError(ServerError error)
858             {
859                globalDisplay_.showErrorMessage("Template Creation Failed",
860                      "Failed to load content from the template at " +
861                      template.getTemplatePath() + ": " + error.getMessage());
862             }
863          });
864    }
865 
addAdditionalResourceFiles(String yaml, final ArrayList<String> files, final CommandWithArg<String> onCompleted)866    public void addAdditionalResourceFiles(String yaml,
867          final ArrayList<String> files,
868          final CommandWithArg<String> onCompleted)
869    {
870       convertFromYaml(yaml, new CommandWithArg<RmdYamlData>()
871       {
872          @Override
873          public void execute(RmdYamlData arg)
874          {
875             if (!arg.parseSucceeded())
876                onCompleted.execute(null);
877             else
878                addAdditionalResourceFiles(arg.getFrontMatter(), files,
879                      onCompleted);
880          }
881       });
882    }
883 
getRMarkdownParamsFile(final String file, final String encoding, final boolean contentKnownToBeAscii, final CommandWithArg<String> onReady)884    public void getRMarkdownParamsFile(final String file,
885                                       final String encoding,
886                                       final boolean contentKnownToBeAscii,
887                                       final CommandWithArg<String> onReady)
888    {
889       // can't do this if the server is already busy
890       if (workbenchContext_.isServerBusy())
891       {
892          globalDisplay_.showMessage(
893                MessageDisplay.MSG_WARNING,
894                "R Session Busy",
895                "Unable to edit parameters (the R session is currently busy).");
896          return;
897       }
898 
899       // meet all dependencies then ask for params
900       final String action = "Specifying Knit parameters";
901       dependencyManager_.withRMarkdown(
902          action,
903          new Command() {
904             @Override
905             public void execute()
906             {
907                dependencyManager_.withShiny(
908                   action,
909                   new Command() {
910 
911                      @Override
912                      public void execute()
913                      {
914                         // subscribe to notification of params ready
915                         // (ensure only one handler at a time is subscribed)
916                         rmdParamsReadyUnsubscribe();
917                         rmdParamsReadyRegistration_ = eventBus_.addHandler(
918                               RmdParamsReadyEvent.TYPE,
919                               new RmdParamsReadyEvent.Handler()
920                         {
921                            @Override
922                            public void onRmdParamsReady(RmdParamsReadyEvent e)
923                            {
924                               rmdParamsReadyUnsubscribe();
925                               onReady.execute(e.getParamsFile());
926                            }
927                         });
928 
929                         // execute knit_with_parameters in the console
930                         FileSystemItem targetFile =
931                                           FileSystemItem.createFile(file);
932                         consoleDispatcher_.executeCommandWithFileEncoding(
933                                              "knit_with_parameters",
934                                              targetFile.getPath(),
935                                              encoding,
936                                              contentKnownToBeAscii);
937                      }
938                   });
939             }
940       });
941    }
942 
943    /**
944     * For a chunk like:
945     *
946     * ```{r cars, echo=FALSE}
947     * ```
948     *
949     * returns the text "r cars, echo=FALSE".
950     *
951     * @param chunk Scope representing the chunk
952     * @return Range representing the contents of the chunk's {} options block
953     */
getRmdChunkOptionText(Scope chunk, DocDisplay display)954    public static String getRmdChunkOptionText(Scope chunk, DocDisplay display)
955    {
956       if (chunk == null)
957          return null;
958 
959       assert chunk.isChunk();
960 
961       Position start = Position.create(chunk.getPreamble().getRow(),
962             chunk.getPreamble().getColumn() + 4); // 4 = length of "```{"
963       Position end = Position.create(chunk.getPreamble().getRow(),
964             display.getLine(start.getRow()).length() - 1);
965       return display.getCode(start, end);
966    }
967 
968    // Private methods ---------------------------------------------------------
969 
970 
rmdParamsReadyUnsubscribe()971    private static void rmdParamsReadyUnsubscribe()
972    {
973       if (rmdParamsReadyRegistration_ != null)
974       {
975          rmdParamsReadyRegistration_.removeHandler();
976          rmdParamsReadyRegistration_ = null;
977       }
978    }
979 
cleanAndCreateTemplate(final RmdChosenTemplate template, final String target, final FileSystemItem oldFile)980    private void cleanAndCreateTemplate(final RmdChosenTemplate template,
981                                        final String target,
982                                        final FileSystemItem oldFile)
983    {
984       ArrayList<FileSystemItem> oldFiles = new ArrayList<>();
985       oldFiles.add(oldFile);
986       fileServer_.deleteFiles(oldFiles, new ServerRequestCallback<Void>()
987          {
988             @Override
989             public void onResponseReceived(Void v)
990             {
991                createDraftFromTemplate(template, target);
992             }
993 
994             @Override
995             public void onError(ServerError error)
996             {
997                globalDisplay_.showErrorMessage("File Remove Failed",
998                      "Couldn't remove " + oldFile.getPath());
999             }
1000          });
1001    }
1002 
createDraftFromTemplate(final RmdChosenTemplate template, final String target)1003    private void createDraftFromTemplate(final RmdChosenTemplate template,
1004                                         final String target)
1005    {
1006       final ProgressIndicator progress = new GlobalProgressDelayer(
1007             globalDisplay_,
1008             250,
1009             "Creating R Markdown Document...").getIndicator();
1010 
1011       server_.createRmdFromTemplate(target,
1012             template.getTemplatePath(), template.createDir(),
1013             new ServerRequestCallback<RmdCreatedTemplate>() {
1014                @Override
1015                public void onResponseReceived(RmdCreatedTemplate created)
1016                {
1017                   // write a pref indicating this is the preferred template--
1018                   // we'll default to it the next time we load the template list
1019                   prefs_.rmdPreferredTemplatePath().setGlobalValue(
1020                         template.getTemplatePath());
1021                   prefs_.writeUserPrefs();
1022                   FileSystemItem file =
1023                         FileSystemItem.createFile(created.getPath());
1024                   eventBus_.fireEvent(new FileEditEvent(file));
1025                   progress.onCompleted();
1026                }
1027 
1028                @Override
1029                public void onError(ServerError error)
1030                {
1031                   progress.onError(
1032                         "Couldn't create a template from " +
1033                         template.getTemplatePath() + " at " + target + ".\n\n" +
1034                         error.getMessage());
1035                }
1036             });
1037    }
1038 
setOutputFormat(RmdFrontMatter frontMatter, String format, final CommandWithArg<String> onCompleted)1039    private void setOutputFormat(RmdFrontMatter frontMatter, String format,
1040                                 final CommandWithArg<String> onCompleted)
1041    {
1042       // If the format list doesn't already contain the given format, add it
1043       // to the list and transfer any applicable options
1044       if (!JsArrayUtil.jsArrayStringContains(frontMatter.getFormatList(),
1045                                              format))
1046       {
1047          RmdTemplate template = getTemplateForFormat(format);
1048          RmdFrontMatterOutputOptions opts = RmdFrontMatterOutputOptions.create();
1049          if (template != null)
1050          {
1051             opts = transferOptions(frontMatter, template, format);
1052          }
1053          frontMatter.setOutputOption(format, opts);
1054       }
1055       frontMatterToYAML(frontMatter, format, onCompleted);
1056    }
1057 
transferOptions( RmdFrontMatter frontMatter, RmdTemplate template, String format)1058    private RmdFrontMatterOutputOptions transferOptions(
1059          RmdFrontMatter frontMatter,
1060          RmdTemplate template,
1061          String format)
1062    {
1063       RmdFrontMatterOutputOptions result = RmdFrontMatterOutputOptions.create();
1064 
1065       // loop over each option applicable to the new format; if it's
1066       // transferable, try to find it in one of the other formats. If
1067       // it should be added to the header, add it.
1068       JsArrayString options = template.getFormat(format).getOptions();
1069       for (int i = 0; i < options.length(); i++)
1070       {
1071          String optionName = options.get(i);
1072          RmdTemplateFormatOption option = template.getOption(optionName);
1073 
1074          if (option.isAddHeader()) {
1075             String val = option.getDefaultValue();
1076             result.setOptionValue(option, val);
1077          }
1078 
1079          if (!option.isTransferable())
1080             continue;
1081 
1082          // option is transferable, is it present in another front matter entry?
1083          JsArrayString formats = frontMatter.getFormatList();
1084          for (int j = 0; j < formats.length(); j++)
1085          {
1086             RmdFrontMatterOutputOptions outOptions =
1087                   frontMatter.getOutputOption(formats.get(j));
1088             if (outOptions == null)
1089                continue;
1090             String val = outOptions.getOptionValue(optionName);
1091             if (val != null)
1092                result.setOptionValue(option, val);
1093          }
1094       }
1095 
1096       return result;
1097    }
1098 
getOutputFormats(String yaml)1099    public static List<String> getOutputFormats(String yaml)
1100    {
1101       return getOutputFormats(yaml, RmdFrontMatter.OUTPUT_KEY);
1102    }
1103 
getQuartoOutputFormats(String yaml)1104    public static List<String> getQuartoOutputFormats(String yaml)
1105    {
1106       return getOutputFormats(yaml, RmdFrontMatter.FORMAT_KEY);
1107    }
1108 
1109 
getOutputFormats(String yaml, String outputKey)1110    public static List<String> getOutputFormats(String yaml, String outputKey)
1111    {
1112       try
1113       {
1114          YamlTree tree = new YamlTree(yaml);
1115          return getOutputFormats(tree, outputKey);
1116       }
1117       catch (Exception e)
1118       {
1119          Debug.log("Warning: Exception thrown while parsing YAML:\n" + yaml);
1120       }
1121       return null;
1122    }
1123 
1124 
getOutputFormats(YamlTree tree, String outputKey)1125    private static List<String> getOutputFormats(YamlTree tree, String outputKey)
1126    {
1127       List<String> outputs = tree.getChildKeys(outputKey);
1128 
1129       if (outputs == null)
1130          return null;
1131       if (outputs.isEmpty())
1132          outputs.add(tree.getKeyValue(outputKey));
1133 
1134       // filter commented out outputs
1135       outputs.removeIf(output -> output.startsWith("#"));
1136 
1137       return outputs;
1138    }
1139 
getOutputFormats(YamlTree tree)1140    private static List<String> getOutputFormats(YamlTree tree)
1141    {
1142       return getOutputFormats(tree, RmdFrontMatter.OUTPUT_KEY);
1143    }
1144 
getQuartoOutputFormats(YamlTree tree)1145    private static List<String> getQuartoOutputFormats(YamlTree tree)
1146    {
1147       return getOutputFormats(tree, RmdFrontMatter.FORMAT_KEY);
1148    }
1149 
showNewRMarkdownDialog( final OperationWithInput<NewRMarkdownDialog.Result> onComplete)1150    public void showNewRMarkdownDialog(
1151          final OperationWithInput<NewRMarkdownDialog.Result> onComplete)
1152    {
1153       withRMarkdownPackage(
1154          "Creating R Markdown documents",
1155          false,
1156          new CommandWithArg<RMarkdownContext>()
1157       {
1158          @Override
1159          public void execute(RMarkdownContext context)
1160          {
1161             new NewRMarkdownDialog(
1162                server_,
1163                context,
1164                workbenchContext_,
1165                prefs_.documentAuthor().getGlobalValue(),
1166                onComplete).showModal();
1167          }
1168       });
1169 }
1170 
showKnitrPreviewWarning(WarningBarDisplay display, String feature, String requiredVersion)1171    private void showKnitrPreviewWarning(WarningBarDisplay display,
1172                                         String feature,
1173                                         String requiredVersion)
1174    {
1175       display.showWarningBar(feature + " requires the " +
1176                              "knitr package (version " + requiredVersion +
1177                              " or higher)");
1178    }
1179 
addAdditionalResourceFiles(RmdFrontMatter frontMatter, ArrayList<String> additionalFiles, CommandWithArg<String> onCompleted)1180    private void addAdditionalResourceFiles(RmdFrontMatter frontMatter,
1181          ArrayList<String> additionalFiles,
1182          CommandWithArg<String> onCompleted)
1183    {
1184       for (String file: additionalFiles)
1185       {
1186          frontMatter.addResourceFile(file);
1187       }
1188 
1189       frontMatterToYAML(frontMatter, null, onCompleted);
1190    }
1191 
useRMarkdownV2(String contents)1192    private boolean useRMarkdownV2(String contents)
1193    {
1194       return !contents.contains("<!-- rmarkdown v1 -->") &&
1195               session_.getSessionInfo().getRMarkdownPackageAvailable();
1196    }
1197 
1198    private Session session_;
1199    private GlobalDisplay globalDisplay_;
1200    private EventBus eventBus_;
1201    private UserPrefs prefs_;
1202    private UserState state_;
1203    private ConsoleDispatcher consoleDispatcher_;
1204    private WorkbenchContext workbenchContext_;
1205    private FileTypeCommands fileTypeCommands_;
1206    private DependencyManager dependencyManager_;
1207    private RMarkdownServerOperations server_;
1208    private FilesServerOperations fileServer_;
1209 
1210    private static HandlerRegistration rmdParamsReadyRegistration_ = null;
1211 }
1212