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