1 /* 2 * TextEditingTarget.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 com.google.gwt.core.client.GWT; 18 import com.google.gwt.core.client.JavaScriptObject; 19 import com.google.gwt.core.client.JsArray; 20 import com.google.gwt.core.client.JsArrayString; 21 import com.google.gwt.core.client.Scheduler; 22 import com.google.gwt.core.client.Scheduler.RepeatingCommand; 23 import com.google.gwt.core.client.Scheduler.ScheduledCommand; 24 import com.google.gwt.dom.client.Element; 25 import com.google.gwt.dom.client.NativeEvent; 26 import com.google.gwt.dom.client.Style.FontWeight; 27 import com.google.gwt.event.dom.client.*; 28 import com.google.gwt.event.logical.shared.*; 29 import com.google.gwt.event.shared.GwtEvent; 30 import com.google.gwt.event.shared.HandlerManager; 31 import com.google.gwt.event.shared.HandlerRegistration; 32 import com.google.gwt.http.client.URL; 33 import com.google.gwt.safehtml.shared.SafeHtmlBuilder; 34 import com.google.gwt.user.client.Command; 35 import com.google.gwt.user.client.Event; 36 import com.google.gwt.user.client.Event.NativePreviewEvent; 37 import com.google.gwt.user.client.Timer; 38 import com.google.gwt.user.client.ui.HasValue; 39 import com.google.gwt.user.client.ui.MenuItem; 40 import com.google.gwt.user.client.ui.UIObject; 41 import com.google.gwt.user.client.ui.Widget; 42 import com.google.inject.Inject; 43 import com.google.inject.Provider; 44 45 import org.rstudio.core.client.*; 46 import org.rstudio.core.client.command.AppCommand; 47 import org.rstudio.core.client.command.CommandBinder; 48 import org.rstudio.core.client.command.Handler; 49 import org.rstudio.core.client.command.KeyboardShortcut; 50 import org.rstudio.core.client.events.EnsureHeightEvent; 51 import org.rstudio.core.client.events.EnsureVisibleEvent; 52 import org.rstudio.core.client.events.HasEnsureHeightHandlers; 53 import org.rstudio.core.client.events.HasEnsureVisibleHandlers; 54 import org.rstudio.core.client.files.FileSystemContext; 55 import org.rstudio.core.client.files.FileSystemItem; 56 import org.rstudio.core.client.js.JsMap; 57 import org.rstudio.core.client.js.JsUtil; 58 import org.rstudio.core.client.regex.Match; 59 import org.rstudio.core.client.regex.Pattern; 60 import org.rstudio.core.client.widget.*; 61 import org.rstudio.studio.client.RStudioGinjector; 62 import org.rstudio.studio.client.application.Desktop; 63 import org.rstudio.studio.client.application.events.ChangeFontSizeEvent; 64 import org.rstudio.studio.client.application.events.EventBus; 65 import org.rstudio.studio.client.application.events.ResetEditorCommandsEvent; 66 import org.rstudio.studio.client.application.events.SetEditorCommandBindingsEvent; 67 import org.rstudio.studio.client.common.*; 68 import org.rstudio.studio.client.common.console.ConsoleProcess; 69 import org.rstudio.studio.client.common.console.ProcessExitEvent; 70 import org.rstudio.studio.client.common.debugging.BreakpointManager; 71 import org.rstudio.studio.client.common.debugging.events.BreakpointsSavedEvent; 72 import org.rstudio.studio.client.common.debugging.model.Breakpoint; 73 import org.rstudio.studio.client.common.dependencies.DependencyManager; 74 import org.rstudio.studio.client.common.filetypes.DocumentMode; 75 import org.rstudio.studio.client.common.filetypes.FileIcon; 76 import org.rstudio.studio.client.common.filetypes.FileType; 77 import org.rstudio.studio.client.common.filetypes.FileTypeCommands; 78 import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; 79 import org.rstudio.studio.client.common.filetypes.SweaveFileType; 80 import org.rstudio.studio.client.common.filetypes.TexFileType; 81 import org.rstudio.studio.client.common.filetypes.TextFileType; 82 import org.rstudio.studio.client.common.filetypes.events.CopySourcePathEvent; 83 import org.rstudio.studio.client.common.filetypes.events.RenameSourceFileEvent; 84 import org.rstudio.studio.client.common.mathjax.MathJax; 85 import org.rstudio.studio.client.common.r.roxygen.RoxygenHelper; 86 import org.rstudio.studio.client.common.rnw.RnwWeave; 87 import org.rstudio.studio.client.common.synctex.Synctex; 88 import org.rstudio.studio.client.common.synctex.SynctexUtils; 89 import org.rstudio.studio.client.common.synctex.model.SourceLocation; 90 import org.rstudio.studio.client.events.GetEditorContextEvent; 91 import org.rstudio.studio.client.htmlpreview.events.ShowHTMLPreviewEvent; 92 import org.rstudio.studio.client.htmlpreview.model.HTMLPreviewParams; 93 import org.rstudio.studio.client.notebook.CompileNotebookOptions; 94 import org.rstudio.studio.client.notebook.CompileNotebookOptionsDialog; 95 import org.rstudio.studio.client.notebook.CompileNotebookPrefs; 96 import org.rstudio.studio.client.notebook.CompileNotebookResult; 97 import org.rstudio.studio.client.palette.model.CommandPaletteEntryProvider; 98 import org.rstudio.studio.client.plumber.events.LaunchPlumberAPIEvent; 99 import org.rstudio.studio.client.plumber.events.PlumberAPIStatusEvent; 100 import org.rstudio.studio.client.plumber.model.PlumberAPIParams; 101 import org.rstudio.studio.client.quarto.QuartoHelper; 102 import org.rstudio.studio.client.quarto.model.QuartoConfig; 103 import org.rstudio.studio.client.rmarkdown.RmdOutput; 104 import org.rstudio.studio.client.rmarkdown.events.ConvertToShinyDocEvent; 105 import org.rstudio.studio.client.rmarkdown.events.RmdOutputFormatChangedEvent; 106 import org.rstudio.studio.client.rmarkdown.events.RmdRenderPendingEvent; 107 import org.rstudio.studio.client.rmarkdown.model.NotebookQueueUnit; 108 import org.rstudio.studio.client.rmarkdown.model.RMarkdownContext; 109 import org.rstudio.studio.client.rmarkdown.model.RmdEditorOptions; 110 import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatter; 111 import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatterOutputOptions; 112 import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat; 113 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateFormat; 114 import org.rstudio.studio.client.rmarkdown.model.RmdYamlData; 115 import org.rstudio.studio.client.rmarkdown.model.YamlFrontMatter; 116 import org.rstudio.studio.client.rmarkdown.model.YamlTree; 117 import org.rstudio.studio.client.rmarkdown.ui.RmdTemplateOptionsDialog; 118 import org.rstudio.studio.client.rsconnect.events.RSConnectActionEvent; 119 import org.rstudio.studio.client.rsconnect.events.RSConnectDeployInitiatedEvent; 120 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSettings; 121 import org.rstudio.studio.client.server.ServerError; 122 import org.rstudio.studio.client.server.ServerRequestCallback; 123 import org.rstudio.studio.client.server.Void; 124 import org.rstudio.studio.client.server.VoidServerRequestCallback; 125 import org.rstudio.studio.client.shiny.ShinyApplication; 126 import org.rstudio.studio.client.shiny.events.LaunchShinyApplicationEvent; 127 import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent; 128 import org.rstudio.studio.client.shiny.model.ShinyApplicationParams; 129 import org.rstudio.studio.client.shiny.model.ShinyTestResults; 130 import org.rstudio.studio.client.workbench.WorkbenchContext; 131 import org.rstudio.studio.client.workbench.commands.Commands; 132 import org.rstudio.studio.client.workbench.model.Session; 133 import org.rstudio.studio.client.workbench.model.SessionInfo; 134 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 135 import org.rstudio.studio.client.workbench.prefs.model.UserState; 136 import org.rstudio.studio.client.workbench.ui.FontSizeManager; 137 import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; 138 import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorPosition; 139 import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection; 140 import org.rstudio.studio.client.workbench.views.files.events.FileChangeEvent; 141 import org.rstudio.studio.client.workbench.views.files.model.FileChange; 142 import org.rstudio.studio.client.workbench.views.help.events.ShowHelpEvent; 143 import org.rstudio.studio.client.workbench.views.jobs.events.JobRunScriptEvent; 144 import org.rstudio.studio.client.workbench.views.jobs.events.LauncherJobRunScriptEvent; 145 import org.rstudio.studio.client.workbench.views.output.compilepdf.events.CompilePdfEvent; 146 import org.rstudio.studio.client.workbench.views.output.lint.LintManager; 147 import org.rstudio.studio.client.workbench.views.presentation.events.SourceFileSaveCompletedEvent; 148 import org.rstudio.studio.client.workbench.views.presentation.model.PresentationState; 149 import org.rstudio.studio.client.workbench.views.source.Source; 150 import org.rstudio.studio.client.workbench.views.source.SourceColumn; 151 import org.rstudio.studio.client.workbench.views.source.SourceColumnManager; 152 import org.rstudio.studio.client.workbench.views.source.SourceWindowManager; 153 import org.rstudio.studio.client.workbench.views.source.editors.EditingTarget; 154 import org.rstudio.studio.client.workbench.views.source.editors.EditingTargetCodeExecution; 155 import org.rstudio.studio.client.workbench.views.source.editors.EditingTargetSource.EditingTargetNameProvider; 156 import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTargetRMarkdownHelper.RmdSelectedTemplate; 157 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceAfterCommandExecutedEvent; 158 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceFold; 159 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Mode.InsertChunkInfo; 160 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position; 161 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range; 162 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Token; 163 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.VimMarks; 164 import org.rstudio.studio.client.workbench.views.source.editors.text.cpp.CppCompletionContext; 165 import org.rstudio.studio.client.workbench.views.source.editors.text.cpp.CppCompletionOperation; 166 import org.rstudio.studio.client.workbench.views.source.editors.text.events.*; 167 import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.ChunkExecUnit; 168 import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.TextEditingTargetNotebook; 169 import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.events.InterruptChunkEvent; 170 import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBar; 171 import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBar.HideMessageHandler; 172 import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBarPopupMenu; 173 import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBarPopupRequest; 174 import org.rstudio.studio.client.workbench.views.source.editors.text.ui.ChooseEncodingDialog; 175 import org.rstudio.studio.client.workbench.views.source.editors.text.ui.RMarkdownNoParamsDialog; 176 import org.rstudio.studio.client.workbench.views.source.editors.text.visualmode.VisualMode; 177 import org.rstudio.studio.client.workbench.views.source.editors.text.visualmode.VisualMode.SyncType; 178 import org.rstudio.studio.client.workbench.views.source.editors.text.visualmode.VisualModeChunk; 179 import org.rstudio.studio.client.workbench.views.source.editors.text.visualmode.VisualModeUtil; 180 import org.rstudio.studio.client.workbench.views.source.events.CollabEditStartParams; 181 import org.rstudio.studio.client.workbench.views.source.events.CollabExternalEditEvent; 182 import org.rstudio.studio.client.workbench.views.source.events.DocFocusedEvent; 183 import org.rstudio.studio.client.workbench.views.source.events.DocTabDragStateChangedEvent; 184 import org.rstudio.studio.client.workbench.views.source.events.DocWindowChangedEvent; 185 import org.rstudio.studio.client.workbench.views.source.events.PopoutDocEvent; 186 import org.rstudio.studio.client.workbench.views.source.events.RecordNavigationPositionEvent; 187 import org.rstudio.studio.client.workbench.views.source.events.SourceFileSavedEvent; 188 import org.rstudio.studio.client.workbench.views.source.events.SourceNavigationEvent; 189 import org.rstudio.studio.client.workbench.views.source.model.*; 190 import org.rstudio.studio.client.workbench.views.vcs.common.ConsoleProgressDialog; 191 import org.rstudio.studio.client.workbench.views.vcs.common.events.ShowVcsDiffEvent; 192 import org.rstudio.studio.client.workbench.views.vcs.common.events.ShowVcsHistoryEvent; 193 import org.rstudio.studio.client.workbench.views.vcs.common.events.VcsRevertFileEvent; 194 import org.rstudio.studio.client.workbench.views.vcs.common.events.VcsViewOnGitHubEvent; 195 import org.rstudio.studio.client.workbench.views.vcs.common.model.GitHubViewRequest; 196 197 import java.util.ArrayList; 198 import java.util.Arrays; 199 import java.util.HashMap; 200 import java.util.HashSet; 201 import java.util.Iterator; 202 import java.util.List; 203 204 public class TextEditingTarget implements 205 EditingTarget, 206 EditingTargetCodeExecution.CodeExtractor 207 { 208 interface MyCommandBinder 209 extends CommandBinder<Commands, TextEditingTarget> 210 { 211 } 212 213 private static final String NOTEBOOK_TITLE = "notebook_title"; 214 private static final String NOTEBOOK_AUTHOR = "notebook_author"; 215 private static final String NOTEBOOK_TYPE = "notebook_type"; 216 217 public final static String DOC_OUTLINE_SIZE = "docOutlineSize"; 218 public final static String DOC_OUTLINE_VISIBLE = "docOutlineVisible"; 219 220 public static final String RMD_VISUAL_MODE = "rmdVisualMode"; 221 public static final String RMD_VISUAL_MODE_WRAP_CONFIGURED = "rmdVisualWrapConfigured"; 222 223 public static final String SOFT_WRAP_LINES = "softWrapLines"; 224 public static final String USE_RAINBOW_PARENS = "useRainbowParens"; 225 226 private static final MyCommandBinder commandBinder = 227 GWT.create(MyCommandBinder.class); 228 229 public interface Display extends TextDisplay, 230 WarningBarDisplay, 231 HasFindReplace, 232 HasEnsureVisibleHandlers, 233 HasEnsureHeightHandlers, 234 HasResizeHandlers 235 { getSourceOnSave()236 HasValue<Boolean> getSourceOnSave(); ensureVisible()237 void ensureVisible(); 238 findSelectAll()239 void findSelectAll(); findFromSelection()240 void findFromSelection(); findFromSelection(String selectionValue)241 void findFromSelection(String selectionValue); 242 getStatusBar()243 StatusBar getStatusBar(); 244 isAttached()245 boolean isAttached(); 246 adaptToExtendedFileType(String extendedType)247 void adaptToExtendedFileType(String extendedType); onShinyApplicationStateChanged(String state)248 void onShinyApplicationStateChanged(String state); onPlumberAPIStateChanged(String state)249 void onPlumberAPIStateChanged(String state); 250 debug_dumpContents()251 void debug_dumpContents(); debug_importDump()252 void debug_importDump(); 253 setIsShinyFormat(boolean showOutputOptions, boolean isPresentation, boolean isShinyPrerendered)254 void setIsShinyFormat(boolean showOutputOptions, 255 boolean isPresentation, 256 boolean isShinyPrerendered); setIsNotShinyFormat()257 void setIsNotShinyFormat(); setIsNotebookFormat()258 void setIsNotebookFormat(); setFormatOptions(TextFileType fileType, boolean showRmdFormatMenu, boolean canEditFormatOptions, List<String> options, List<String> values, List<String> extensions, String selected)259 void setFormatOptions(TextFileType fileType, 260 boolean showRmdFormatMenu, 261 boolean canEditFormatOptions, 262 List<String> options, 263 List<String> values, 264 List<String> extensions, 265 String selected); setQuartoFormatOptions(TextFileType fileType, boolean showRmdFormatMenu, List<String> formats)266 void setQuartoFormatOptions(TextFileType fileType, boolean showRmdFormatMenu, List<String> formats); addRmdFormatChangedHandler( RmdOutputFormatChangedEvent.Handler handler)267 HandlerRegistration addRmdFormatChangedHandler( 268 RmdOutputFormatChangedEvent.Handler handler); 269 setPublishPath(String type, String publishPath)270 void setPublishPath(String type, String publishPath); invokePublish()271 void invokePublish(); 272 initWidgetSize()273 void initWidgetSize(); 274 toggleDocumentOutline()275 void toggleDocumentOutline(); toggleRmdVisualMode()276 void toggleRmdVisualMode(); toggleSoftWrapMode()277 void toggleSoftWrapMode(); toggleRainbowParens()278 void toggleRainbowParens(); 279 setNotebookUIVisible(boolean visible)280 void setNotebookUIVisible(boolean visible); 281 setAccessibleName(String name)282 void setAccessibleName(String name); 283 editorContainer()284 TextEditorContainer editorContainer(); 285 manageCommandUI()286 void manageCommandUI(); 287 addVisualModeFindReplaceButton(ToolbarButton findReplaceButton)288 void addVisualModeFindReplaceButton(ToolbarButton findReplaceButton); 289 getSourceColumn()290 SourceColumn getSourceColumn(); 291 } 292 293 private class SaveProgressIndicator implements ProgressIndicator 294 { SaveProgressIndicator(FileSystemItem file, TextFileType fileType, boolean suppressFileLockError, Command executeOnSuccess)295 public SaveProgressIndicator(FileSystemItem file, 296 TextFileType fileType, 297 boolean suppressFileLockError, 298 Command executeOnSuccess) 299 { 300 this(file, fileType, suppressFileLockError, executeOnSuccess, null); 301 } 302 SaveProgressIndicator(FileSystemItem file, TextFileType fileType, boolean suppressFileLockError, Command executeOnSuccess, Command executeOnSilentFailure)303 public SaveProgressIndicator(FileSystemItem file, 304 TextFileType fileType, 305 boolean suppressFileLockError, 306 Command executeOnSuccess, 307 Command executeOnSilentFailure) 308 { 309 file_ = file; 310 newFileType_ = fileType; 311 suppressFileLockError_ = suppressFileLockError; 312 executeOnSuccess_ = executeOnSuccess; 313 executeOnSilentFailure_ = executeOnSilentFailure; 314 } 315 onProgress(String message)316 public void onProgress(String message) 317 { 318 onProgress(message, null); 319 } 320 onProgress(String message, Operation onCancel)321 public void onProgress(String message, Operation onCancel) 322 { 323 } 324 clearProgress()325 public void clearProgress() 326 { 327 } 328 onCompleted()329 public void onCompleted() 330 { 331 isSaving_ = false; 332 333 // don't need to check again soon because we just saved 334 // (without this and when file monitoring is active we'd 335 // end up immediately checking for external edits) 336 externalEditCheckInterval_.reset(250); 337 boolean fileTypeChanged = true; 338 339 if (newFileType_ != null) 340 { 341 // if we already had a file type, see if the underlying type has changed 342 if (fileType_ != null) 343 { 344 fileTypeChanged = !StringUtil.equals(newFileType_.getTypeId(), fileType_.getTypeId()); 345 } 346 fileType_ = newFileType_; 347 } 348 349 if (file_ != null) 350 { 351 ignoreDeletes_ = false; 352 forceSaveCommandActive_ = false; 353 commands_.reopenSourceDocWithEncoding().setEnabled(true); 354 name_.setValue(file_.getName(), true); 355 // Make sure tooltip gets updated, even if name hasn't changed 356 name_.fireChangeEvent(); 357 358 // If we were dirty prior to saving, clean up the debug state so 359 // we don't continue highlighting after saving. (There are cases 360 // in which we want to restore highlighting after the dirty state 361 // is marked clean--i.e. when unwinding the undo stack.) 362 if (dirtyState_.getValue()) 363 endDebugHighlighting(); 364 365 dirtyState_.markClean(); 366 } 367 368 if (newFileType_ != null && fileTypeChanged) 369 { 370 // Make sure the icon gets updated, even if name hasn't changed 371 name_.fireChangeEvent(); 372 updateStatusBarLanguage(); 373 view_.adaptToFileType(newFileType_); 374 375 // turn R Markdown behavior (inline execution, previews, etc.) 376 // based on whether we just became an R Markdown type 377 setRMarkdownBehaviorEnabled(newFileType_.isRmd()); 378 379 events_.fireEvent(new FileTypeChangedEvent()); 380 if (!isSourceOnSaveEnabled() && docUpdateSentinel_.sourceOnSave()) 381 { 382 view_.getSourceOnSave().setValue(false, true); 383 } 384 } 385 386 if (executeOnSuccess_ != null) 387 executeOnSuccess_.execute(); 388 } 389 onError(final String message)390 public void onError(final String message) 391 { 392 isSaving_ = false; 393 394 // in case the error occurred saving a document that wasn't 395 // in the foreground 396 view_.ensureVisible(); 397 398 // command to show the error 399 final Command showErrorCommand = new Command() { 400 @Override 401 public void execute() 402 { 403 // do not show the error if it is a transient autosave related issue - this can occur fairly frequently 404 // when attempting to save files that are being backed up by external software 405 if (message.contains("The process cannot access the file because it is being used by another process") && suppressFileLockError_) 406 { 407 if (executeOnSilentFailure_ != null) 408 executeOnSilentFailure_.execute(); 409 410 return; 411 } 412 413 globalDisplay_.showErrorMessage("Error Saving File", message); 414 } 415 }; 416 417 // check whether the file exists and isn't writeable 418 if (file_ != null) 419 { 420 server_.isReadOnlyFile(file_.getPath(), 421 new ServerRequestCallback<Boolean>() { 422 423 @Override 424 public void onResponseReceived(Boolean isReadOnly) 425 { 426 if (isReadOnly) 427 { 428 String message = "This source file is read-only " + 429 "so changes cannot be saved"; 430 view_.showWarningBar(message); 431 432 String saveAsPath = file_.getParentPath().completePath( 433 file_.getStem() + "-copy" + file_.getExtension()); 434 saveNewFile( 435 saveAsPath, 436 null, 437 CommandUtil.join(postSaveCommand(), new Command() { 438 439 @Override 440 public void execute() 441 { 442 view_.hideWarningBar(); 443 } 444 })); 445 446 } 447 else 448 { 449 showErrorCommand.execute(); 450 } 451 } 452 453 @Override 454 public void onError(ServerError error) 455 { 456 Debug.logError(error); 457 showErrorCommand.execute(); 458 } 459 }); 460 } 461 else 462 { 463 showErrorCommand.execute(); 464 } 465 466 467 } 468 469 private final FileSystemItem file_; 470 private final TextFileType newFileType_; 471 private final boolean suppressFileLockError_; 472 private final Command executeOnSuccess_; 473 private final Command executeOnSilentFailure_; 474 } 475 476 @Inject TextEditingTarget(Commands commands, SourceServerOperations server, EventBus events, GlobalDisplay globalDisplay, FileDialogs fileDialogs, FileTypeRegistry fileTypeRegistry, FileTypeCommands fileTypeCommands, ConsoleDispatcher consoleDispatcher, WorkbenchContext workbenchContext, Session session, Synctex synctex, FontSizeManager fontSizeManager, DocDisplay docDisplay, UserPrefs prefs, UserState state, BreakpointManager breakpointManager, Source source, DependencyManager dependencyManager)477 public TextEditingTarget(Commands commands, 478 SourceServerOperations server, 479 EventBus events, 480 GlobalDisplay globalDisplay, 481 FileDialogs fileDialogs, 482 FileTypeRegistry fileTypeRegistry, 483 FileTypeCommands fileTypeCommands, 484 ConsoleDispatcher consoleDispatcher, 485 WorkbenchContext workbenchContext, 486 Session session, 487 Synctex synctex, 488 FontSizeManager fontSizeManager, 489 DocDisplay docDisplay, 490 UserPrefs prefs, 491 UserState state, 492 BreakpointManager breakpointManager, 493 Source source, 494 DependencyManager dependencyManager) 495 { 496 commands_ = commands; 497 server_ = server; 498 events_ = events; 499 globalDisplay_ = globalDisplay; 500 fileDialogs_ = fileDialogs; 501 fileTypeRegistry_ = fileTypeRegistry; 502 fileTypeCommands_ = fileTypeCommands; 503 consoleDispatcher_ = consoleDispatcher; 504 workbenchContext_ = workbenchContext; 505 session_ = session; 506 synctex_ = synctex; 507 fontSizeManager_ = fontSizeManager; 508 breakpointManager_ = breakpointManager; 509 source_ = source; 510 dependencyManager_ = dependencyManager; 511 512 docDisplay_ = docDisplay; 513 dirtyState_ = new DirtyState(docDisplay_, false); 514 lintManager_ = new LintManager(this, cppCompletionContext_); 515 prefs_ = prefs; 516 state_ = state; 517 compilePdfHelper_ = new TextEditingTargetCompilePdfHelper(docDisplay_); 518 rmarkdownHelper_ = new TextEditingTargetRMarkdownHelper(); 519 cppHelper_ = new TextEditingTargetCppHelper(cppCompletionContext_, 520 docDisplay_); 521 jsHelper_ = new TextEditingTargetJSHelper(docDisplay_); 522 sqlHelper_ = new TextEditingTargetSqlHelper(docDisplay_); 523 presentationHelper_ = new TextEditingTargetPresentationHelper( 524 docDisplay_); 525 rHelper_ = new TextEditingTargetRHelper(docDisplay_); 526 quartoHelper_ = new TextEditingTargetQuartoHelper(this, docDisplay_); 527 528 docDisplay_.setRnwCompletionContext(compilePdfHelper_); 529 docDisplay_.setCppCompletionContext(cppCompletionContext_); 530 docDisplay_.setRCompletionContext(rContext_); 531 scopeHelper_ = new TextEditingTargetScopeHelper(docDisplay_); 532 533 addRecordNavigationPositionHandler(releaseOnDismiss_, 534 docDisplay_, 535 events_, 536 this); 537 538 EditingTarget target = this; 539 docDisplay_.addKeyDownHandler(new KeyDownHandler() 540 { 541 public void onKeyDown(KeyDownEvent event) 542 { 543 NativeEvent ne = event.getNativeEvent(); 544 int mod = KeyboardShortcut.getModifierValue(ne); 545 546 if ((mod == KeyboardShortcut.META || ( 547 mod == KeyboardShortcut.CTRL && 548 !BrowseCap.hasMetaKey() && 549 !docDisplay_.isEmacsModeOn() && 550 (!docDisplay_.isVimModeOn() || docDisplay_.isVimInInsertMode()))) 551 && ne.getKeyCode() == 'F') 552 { 553 event.preventDefault(); 554 event.stopPropagation(); 555 commands_.findReplace().execute(); 556 } 557 else if (BrowseCap.hasMetaKey() && 558 (mod == KeyboardShortcut.META) && 559 (ne.getKeyCode() == 'E')) 560 { 561 event.preventDefault(); 562 event.stopPropagation(); 563 commands_.findFromSelection().execute(); 564 } 565 else if (mod == KeyboardShortcut.CTRL 566 && ne.getKeyCode() == KeyCodes.KEY_UP 567 && fileType_ == FileTypeRegistry.R) 568 { 569 event.preventDefault(); 570 event.stopPropagation(); 571 jumpToPreviousFunction(); 572 } 573 else if (mod == KeyboardShortcut.CTRL 574 && ne.getKeyCode() == KeyCodes.KEY_DOWN 575 && fileType_ == FileTypeRegistry.R) 576 { 577 event.preventDefault(); 578 event.stopPropagation(); 579 jumpToNextFunction(); 580 } 581 else if ((ne.getKeyCode() == KeyCodes.KEY_ESCAPE) && 582 prefs_.editorKeybindings().getValue() != UserPrefs.EDITOR_KEYBINDINGS_VIM) 583 { 584 event.preventDefault(); 585 event.stopPropagation(); 586 587 // Don't send an interrupt if a popup is visible 588 if (docDisplay_.isPopupVisible()) 589 return; 590 591 // Don't send an interrupt if we're in a source window 592 if (!SourceWindowManager.isMainSourceWindow()) 593 return; 594 595 if (commands_.interruptR().isEnabled()) 596 commands_.interruptR().execute(); 597 } 598 else if (continueSpecialCommentOnNewline(ne)) 599 { 600 // nothing to do; continueSpecialCommentOnNewline() does all the magic 601 } 602 else if ( 603 prefs_.continueCommentsOnNewline().getValue() && 604 !docDisplay_.isPopupVisible() && 605 ne.getKeyCode() == KeyCodes.KEY_ENTER && mod == 0 && 606 (fileType_.isC() || isCursorInRMode(docDisplay_) || isCursorInTexMode(docDisplay_))) 607 { 608 String line = docDisplay_.getCurrentLineUpToCursor(); 609 610 // validate that this line is composed of only comments and whitespace 611 // (necessary to check token type for e.g. Markdown documents) 612 // https://github.com/rstudio/rstudio/issues/6421 613 JsArray<Token> tokens = 614 docDisplay_.getTokens(docDisplay_.getCursorPosition().getRow()); 615 616 boolean isCommentLine = true; 617 for (int i = 0, n = tokens.length(); i < n; i++) 618 { 619 Token token = tokens.get(i); 620 621 // allow for empty whitespace tokens 622 String value = token.getValue(); 623 if (value.trim().isEmpty()) 624 continue; 625 626 // allow tokens explicitly declared as comments 627 if (token.hasType("comment")) 628 continue; 629 630 // if we got here, then we got a non-whitespace, non-comment token, 631 // so we cannot continue the comment 632 isCommentLine = false; 633 break; 634 } 635 636 Pattern pattern = null; 637 638 if (!isCommentLine) 639 { 640 pattern = null; 641 } 642 else if (isCursorInRMode(docDisplay_)) 643 { 644 pattern = Pattern.create("^(\\s*#+'?\\s*)"); 645 } 646 else if (isCursorInTexMode(docDisplay_)) 647 { 648 pattern = Pattern.create("^(\\s*%+'?\\s*)"); 649 } 650 else if (fileType_.isC()) 651 { 652 // bail on attributes 653 if (!line.matches("^\\s*//\\s*\\[\\[.*\\]\\].*")) 654 pattern = Pattern.create("^(\\s*//'?\\s*)"); 655 } 656 657 if (pattern != null) 658 { 659 Match match = pattern.match(line, 0); 660 if (match != null) 661 { 662 event.preventDefault(); 663 event.stopPropagation(); 664 docDisplay_.insertCode("\n" + match.getGroup(1)); 665 docDisplay_.ensureCursorVisible(); 666 } 667 } 668 } 669 else if ( 670 prefs_.continueCommentsOnNewline().getValue() && 671 !docDisplay_.isPopupVisible() && 672 ne.getKeyCode() == KeyCodes.KEY_ENTER && 673 mod == KeyboardShortcut.SHIFT) 674 { 675 event.preventDefault(); 676 event.stopPropagation(); 677 String indent = docDisplay_.getNextLineIndent(); 678 docDisplay_.insertCode("\n" + indent); 679 } 680 events_.fireEvent(new EditingTargetSelectedEvent(target)); 681 } 682 }); 683 684 docDisplay_.addClickHandler(new ClickHandler() 685 { 686 @Override 687 public void onClick(ClickEvent event) 688 { 689 events_.fireEvent(new EditingTargetSelectedEvent(target)); 690 } 691 }); 692 693 docDisplay_.addCommandClickHandler(new CommandClickEvent.Handler() 694 { 695 @Override 696 public void onCommandClick(CommandClickEvent event) 697 { 698 // bail if the target is a link marker (implies already handled) 699 NativeEvent nativeEvent = event.getNativeEvent(); 700 Element target = nativeEvent.getEventTarget().cast(); 701 if (target != null && target.hasClassName("ace_marker")) 702 { 703 nativeEvent.stopPropagation(); 704 nativeEvent.preventDefault(); 705 return; 706 } 707 708 // force cursor position 709 Position position = event.getEvent().getDocumentPosition(); 710 docDisplay_.setCursorPosition(position); 711 712 // delegate to handlers 713 if (fileType_.canCompilePDF() && 714 commands_.synctexSearch().isEnabled()) 715 { 716 // warn firefox users that this doesn't really work in Firefox 717 if (BrowseCap.isFirefox() && !BrowseCap.isMacintosh()) 718 SynctexUtils.maybeShowFirefoxWarning("PDF preview"); 719 720 doSynctexSearch(true); 721 } 722 else 723 { 724 docDisplay_.goToDefinition(); 725 } 726 } 727 }); 728 729 docDisplay_.addFindRequestedHandler(new FindRequestedEvent.Handler() { 730 @Override 731 public void onFindRequested(FindRequestedEvent event) 732 { 733 view_.showFindReplace(event.getDefaultForward()); 734 } 735 }); 736 737 docDisplay_.addScopeTreeReadyHandler(new ScopeTreeReadyEvent.Handler() 738 { 739 @Override 740 public void onScopeTreeReady(ScopeTreeReadyEvent event) 741 { 742 updateCurrentScope(); 743 } 744 }); 745 746 docDisplay_.addEditorBlurHandler((BlurEvent evt) -> 747 { 748 maybeAutoSaveOnBlur(); 749 }); 750 751 events_.addHandler( 752 ShinyApplicationStatusEvent.TYPE, 753 new ShinyApplicationStatusEvent.Handler() 754 { 755 @Override 756 public void onShinyApplicationStatus( 757 ShinyApplicationStatusEvent event) 758 { 759 // If the document appears to be inside the directory 760 // associated with the event, update the view to match the 761 // new state. 762 if (getPath() != null && 763 getPath().startsWith(event.getParams().getPath())) 764 { 765 String state = event.getParams().getState(); 766 if (event.getParams().getViewerType() != 767 UserPrefs.SHINY_VIEWER_TYPE_PANE && 768 event.getParams().getViewerType() != 769 UserPrefs.SHINY_VIEWER_TYPE_WINDOW) 770 { 771 // we can't control the state when it's not in an 772 // RStudio-owned window, so treat the app as stopped 773 state = ShinyApplicationParams.STATE_STOPPED; 774 } 775 view_.onShinyApplicationStateChanged(state); 776 } 777 } 778 }); 779 780 events_.addHandler( 781 PlumberAPIStatusEvent.TYPE, 782 new PlumberAPIStatusEvent.Handler() 783 { 784 @Override 785 public void onPlumberAPIStatus(PlumberAPIStatusEvent event) 786 { 787 // If the document appears to be inside the directory 788 // associated with the event, update the view to match the 789 // new state. 790 if (getPath() != null && 791 getPath().startsWith(event.getParams().getPath())) 792 { 793 String state = event.getParams().getState(); 794 if (event.getParams().getViewerType() != 795 UserPrefs.PLUMBER_VIEWER_TYPE_PANE && 796 event.getParams().getViewerType() != 797 UserPrefs.PLUMBER_VIEWER_TYPE_WINDOW) 798 { 799 // we can't control the state when it's not in an 800 // RStudio-owned window, so treat the app as stopped 801 state = PlumberAPIParams.STATE_STOPPED; 802 } 803 view_.onPlumberAPIStateChanged(state); 804 } 805 } 806 }); 807 808 events_.addHandler( 809 BreakpointsSavedEvent.TYPE, 810 new BreakpointsSavedEvent.Handler() 811 { 812 @Override 813 public void onBreakpointsSaved(BreakpointsSavedEvent event) 814 { 815 // if this document isn't ready for breakpoints, stop now 816 if (docUpdateSentinel_ == null) 817 { 818 return; 819 } 820 for (Breakpoint breakpoint: event.breakpoints()) 821 { 822 // discard the breakpoint if it's not related to the file this 823 // editor instance is concerned with 824 if (!breakpoint.isInFile(getPath())) 825 { 826 continue; 827 } 828 829 // if the breakpoint was saved successfully, enable it on the 830 // editor surface; otherwise, just remove it. 831 if (event.successful()) 832 { 833 docDisplay_.addOrUpdateBreakpoint(breakpoint); 834 } 835 else 836 { 837 // Show a warning for breakpoints that didn't get set (unless 838 // the reason the breakpoint wasn't set was that it's being 839 // removed) 840 if (breakpoint.getState() != Breakpoint.STATE_REMOVING) 841 { 842 view_.showWarningBar("Breakpoints can only be set inside "+ 843 "the body of a function. "); 844 } 845 docDisplay_.removeBreakpoint(breakpoint); 846 } 847 } 848 updateBreakpointWarningBar(); 849 } 850 }); 851 852 events_.addHandler(ConvertToShinyDocEvent.TYPE, 853 new ConvertToShinyDocEvent.Handler() 854 { 855 @Override 856 public void onConvertToShinyDoc(ConvertToShinyDocEvent event) 857 { 858 if (getPath() != null && 859 getPath().equals(event.getPath())) 860 { 861 String yaml = getRmdFrontMatter(); 862 if (yaml == null) 863 return; 864 String newYaml = rmarkdownHelper_.convertYamlToShinyDoc(yaml); 865 applyRmdFrontMatter(newYaml); 866 renderRmd(); 867 } 868 } 869 }); 870 871 events_.addHandler(RSConnectDeployInitiatedEvent.TYPE, 872 new RSConnectDeployInitiatedEvent.Handler() 873 { 874 @Override 875 public void onRSConnectDeployInitiated( 876 RSConnectDeployInitiatedEvent event) 877 { 878 // no need to process this event if this target doesn't have a 879 // path, or if the event's contents don't include additional 880 // files. 881 if (getPath() == null) 882 return; 883 884 // see if the event corresponds to a deployment of this file 885 if (!getPath().equals(event.getSource().getSourceFile())) 886 return; 887 888 RSConnectPublishSettings settings = event.getSettings(); 889 if (settings == null) 890 return; 891 892 // ignore deployments of static content generated from this 893 // file 894 if (settings.getAsStatic()) 895 return; 896 897 if (settings.getAdditionalFiles() != null && 898 settings.getAdditionalFiles().size() > 0) 899 { 900 addAdditionalResourceFiles(settings.getAdditionalFiles()); 901 } 902 } 903 }); 904 905 events_.addHandler( 906 SetEditorCommandBindingsEvent.TYPE, 907 new SetEditorCommandBindingsEvent.Handler() 908 { 909 @Override 910 public void onSetEditorCommandBindings(SetEditorCommandBindingsEvent event) 911 { 912 getDocDisplay().setEditorCommandBinding( 913 event.getId(), 914 event.getKeySequences()); 915 } 916 }); 917 918 events_.addHandler( 919 ResetEditorCommandsEvent.TYPE, 920 new ResetEditorCommandsEvent.Handler() 921 { 922 @Override 923 public void onResetEditorCommands(ResetEditorCommandsEvent event) 924 { 925 getDocDisplay().resetCommands(); 926 } 927 }); 928 929 events_.addHandler(DocTabDragStateChangedEvent.TYPE, 930 new DocTabDragStateChangedEvent.Handler() 931 { 932 933 @Override 934 public void onDocTabDragStateChanged( 935 DocTabDragStateChangedEvent e) 936 { 937 // enable text drag/drop only while we're not dragging tabs 938 boolean enabled = e.getState() == 939 DocTabDragStateChangedEvent.STATE_NONE; 940 941 // make editor read only while we're dragging and dropping 942 // tabs; otherwise the editor surface will accept a tab drop 943 // as text 944 docDisplay_.setReadOnly(!enabled); 945 } 946 }); 947 948 events_.addHandler( 949 AceAfterCommandExecutedEvent.TYPE, 950 new AceAfterCommandExecutedEvent.Handler() 951 { 952 @Override 953 public void onAceAfterCommandExecuted(AceAfterCommandExecutedEvent event) 954 { 955 JavaScriptObject data = event.getCommandData(); 956 if (isIncrementalSearchCommand(data)) 957 { 958 String message = getIncrementalSearchMessage(); 959 if (StringUtil.isNullOrEmpty(message)) 960 961 { 962 view_.getStatusBar().hideMessage(); 963 } 964 else 965 { 966 view_.getStatusBar().showMessage( 967 getIncrementalSearchMessage(), 968 2000); 969 } 970 } 971 } 972 }); 973 974 releaseOnDismiss_.add( 975 prefs.autoSaveOnBlur().addValueChangeHandler((ValueChangeEvent<Boolean> val) -> 976 { 977 // When the user turns on autosave, disable Source on Save if it was 978 // previously enabled; otherwise documents which were open with this 979 // setting enabled will start sourcing themselves on blur. 980 if (val.getValue()) 981 { 982 setSourceOnSave(false); 983 } 984 })); 985 releaseOnDismiss_.add( 986 prefs.autoSaveOnIdle().bind((String behavior) -> 987 { 988 if (behavior == UserPrefs.AUTO_SAVE_ON_IDLE_COMMIT) 989 { 990 // When switching into autosave on idle mode, start the timer 991 setSourceOnSave(false); 992 nudgeAutosave(); 993 } 994 else 995 { 996 // When leaving it, stop the timer 997 autoSaveTimer_.cancel(); 998 } 999 })); 1000 } 1001 1002 static { initializeIncrementalSearch()1003 initializeIncrementalSearch(); 1004 } 1005 initializeIncrementalSearch()1006 private static final native String initializeIncrementalSearch() /*-{ 1007 var IncrementalSearch = $wnd.require("ace/incremental_search").IncrementalSearch; 1008 (function() { 1009 this.message = $entry(function(msg) { 1010 @org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTarget::setIncrementalSearchMessage(Ljava/lang/String;)(msg); 1011 }); 1012 1013 }).call(IncrementalSearch.prototype); 1014 }-*/; 1015 isIncrementalSearchCommand(JavaScriptObject data)1016 private static final native boolean isIncrementalSearchCommand(JavaScriptObject data) /*-{ 1017 var command = data.command; 1018 if (command == null) 1019 return false; 1020 1021 var result = 1022 command.name === "iSearch" || 1023 command.name === "iSearchBackwards" || 1024 command.isIncrementalSearchCommand === true; 1025 1026 return result; 1027 }-*/; 1028 1029 private static String sIncrementalSearchMessage_ = null; setIncrementalSearchMessage(String message)1030 private static final void setIncrementalSearchMessage(String message) 1031 { 1032 sIncrementalSearchMessage_ = message; 1033 } 1034 getIncrementalSearchMessage()1035 private static final String getIncrementalSearchMessage() 1036 { 1037 return sIncrementalSearchMessage_; 1038 } 1039 moveCursorToNextSectionOrChunk(boolean includeSections)1040 private boolean moveCursorToNextSectionOrChunk(boolean includeSections) 1041 { 1042 Scope current = docDisplay_.getCurrentScope(); 1043 ScopeList scopes = new ScopeList(docDisplay_); 1044 Position cursorPos = docDisplay_.getCursorPosition(); 1045 1046 int n = scopes.size(); 1047 for (int i = 0; i < n; i++) 1048 { 1049 Scope scope = scopes.get(i); 1050 if (!(scope.isChunk() || (scope.isSection() && includeSections))) 1051 continue; 1052 1053 if (scope.equals(current)) 1054 continue; 1055 1056 if (scope.getPreamble().isAfter(cursorPos)) 1057 { 1058 moveCursorToNextPrevSection(scope.getPreamble()); 1059 return true; 1060 } 1061 } 1062 1063 return false; 1064 } 1065 moveCursorToPreviousSectionOrChunk(boolean includeSections)1066 private boolean moveCursorToPreviousSectionOrChunk(boolean includeSections) 1067 { 1068 ScopeList scopes = new ScopeList(docDisplay_); 1069 Position cursorPos = docDisplay_.getCursorPosition(); 1070 1071 int n = scopes.size(); 1072 for (int i = n - 1; i >= 0; i--) 1073 { 1074 Scope scope = scopes.get(i); 1075 if (!(scope.isChunk() || (includeSections && scope.isSection()))) 1076 continue; 1077 1078 if (scope.getPreamble().isBefore(cursorPos)) 1079 { 1080 moveCursorToNextPrevSection(scope.getPreamble()); 1081 return true; 1082 } 1083 } 1084 1085 return false; 1086 } 1087 moveCursorToNextPrevSection(Position pos)1088 private void moveCursorToNextPrevSection(Position pos) 1089 { 1090 docDisplay_.setCursorPosition(pos); 1091 docDisplay_.moveCursorNearTop(5); 1092 } 1093 1094 @Handler onSwitchFocusSourceConsole()1095 void onSwitchFocusSourceConsole() 1096 { 1097 if (docDisplay_.isFocused()) 1098 commands_.activateConsole().execute(); 1099 else 1100 commands_.activateSource().execute(); 1101 } 1102 1103 @Handler onGoToStartOfCurrentScope()1104 void onGoToStartOfCurrentScope() 1105 { 1106 docDisplay_.focus(); 1107 Scope scope = docDisplay_.getCurrentScope(); 1108 if (scope != null) 1109 { 1110 Position position = Position.create( 1111 scope.getBodyStart().getRow(), 1112 scope.getBodyStart().getColumn() + 1); 1113 docDisplay_.setCursorPosition(position); 1114 } 1115 } 1116 1117 @Handler onGoToEndOfCurrentScope()1118 void onGoToEndOfCurrentScope() 1119 { 1120 docDisplay_.focus(); 1121 Scope scope = docDisplay_.getCurrentScope(); 1122 if (scope != null) 1123 { 1124 Position end = scope.getEnd(); 1125 if (end != null) 1126 { 1127 Position position = Position.create( 1128 end.getRow(), 1129 Math.max(0, end.getColumn() - 1)); 1130 docDisplay_.setCursorPosition(position); 1131 } 1132 } 1133 } 1134 1135 @Handler onGoToNextSection()1136 void onGoToNextSection() 1137 { 1138 if (visualMode_.isActivated()) 1139 { 1140 visualMode_.goToNextSection(); 1141 } 1142 else 1143 { 1144 if (docDisplay_.getFileType().canGoNextPrevSection()) 1145 { 1146 if (!moveCursorToNextSectionOrChunk(true)) 1147 docDisplay_.gotoPageDown(); 1148 } 1149 else 1150 { 1151 docDisplay_.gotoPageDown(); 1152 } 1153 } 1154 1155 } 1156 1157 @Handler onGoToPrevSection()1158 void onGoToPrevSection() 1159 { 1160 if (visualMode_.isActivated()) 1161 { 1162 visualMode_.goToPreviousSection(); 1163 } 1164 else 1165 { 1166 if (docDisplay_.getFileType().canGoNextPrevSection()) 1167 { 1168 if (!moveCursorToPreviousSectionOrChunk(true)) 1169 docDisplay_.gotoPageUp(); 1170 } 1171 else 1172 { 1173 docDisplay_.gotoPageUp(); 1174 } 1175 } 1176 } 1177 1178 @Handler onGoToNextChunk()1179 void onGoToNextChunk() 1180 { 1181 if (visualMode_.isActivated()) 1182 { 1183 visualMode_.goToNextChunk(); 1184 } 1185 else 1186 { 1187 moveCursorToNextSectionOrChunk(false); 1188 } 1189 } 1190 1191 @Handler onGoToPrevChunk()1192 void onGoToPrevChunk() 1193 { 1194 if (visualMode_.isActivated()) 1195 { 1196 visualMode_.goToPreviousChunk(); 1197 } 1198 else 1199 { 1200 moveCursorToPreviousSectionOrChunk(false); 1201 } 1202 } 1203 1204 ensureTextEditorActive(Command command)1205 public void ensureTextEditorActive(Command command) 1206 { 1207 visualMode_.deactivate(command); 1208 } 1209 ensureVisualModeActive(Command command)1210 public void ensureVisualModeActive(Command command) 1211 { 1212 visualMode_.activate(command); 1213 } 1214 onVisualEditorBlur()1215 public void onVisualEditorBlur() 1216 { 1217 maybeAutoSaveOnBlur(); 1218 } 1219 navigateToXRef(String xref)1220 public void navigateToXRef(String xref) 1221 { 1222 ensureVisualModeActive(() -> { 1223 Scheduler.get().scheduleDeferred(() -> { 1224 visualMode_.navigateToXRef(xref, false); 1225 }); 1226 1227 }); 1228 } 1229 navigateToXRef(XRef xref, boolean forceVisualMode)1230 public void navigateToXRef(XRef xref, boolean forceVisualMode) 1231 { 1232 if (isVisualModeActivated() || forceVisualMode) 1233 { 1234 ensureVisualModeActive(() -> { 1235 Scheduler.get().scheduleDeferred(() -> { 1236 visualMode_.navigateToXRef(xref.getXRefString(), false); 1237 }); 1238 }); 1239 } 1240 else 1241 { 1242 String title = xref.getTitle(); 1243 for (int i = 0, n = docDisplay_.getRowCount(); i < n; i++) 1244 { 1245 String line = docDisplay_.getLine(i); 1246 int index = line.indexOf(title); 1247 if (index == -1) 1248 continue; 1249 1250 navigateToPosition( 1251 SourcePosition.create(i, index), 1252 false); 1253 } 1254 } 1255 1256 } 1257 1258 // the navigateToPosition methods are called by modules that explicitly 1259 // want the text editor active (e.g. debugging, find in files, etc.) so they 1260 // don't chec for visual mode 1261 1262 @Override navigateToPosition(SourcePosition position, boolean recordCurrent)1263 public void navigateToPosition(SourcePosition position, 1264 boolean recordCurrent) 1265 { 1266 navigateToVisualPosition(position, (disp, pos) -> 1267 { 1268 disp.navigateToPosition(pos, recordCurrent); 1269 }); 1270 } 1271 1272 @Override navigateToPosition(SourcePosition position, boolean recordCurrent, boolean highlightLine)1273 public void navigateToPosition(SourcePosition position, 1274 boolean recordCurrent, 1275 boolean highlightLine) 1276 { 1277 navigateToVisualPosition(position, (disp, pos) -> 1278 { 1279 disp.navigateToPosition(pos, recordCurrent, highlightLine, false); 1280 }); 1281 } 1282 1283 @Override navigateToPosition(SourcePosition position, boolean recordCurrent, boolean highlightLine, boolean moveCursor, Command onNavigationCompleted)1284 public void navigateToPosition(SourcePosition position, 1285 boolean recordCurrent, 1286 boolean highlightLine, 1287 boolean moveCursor, 1288 Command onNavigationCompleted) 1289 { 1290 navigateToVisualPosition(position, (disp, pos) -> 1291 { 1292 disp.navigateToPosition(pos, recordCurrent, highlightLine, !moveCursor); 1293 if (onNavigationCompleted != null) 1294 onNavigationCompleted.execute(); 1295 }); 1296 } 1297 1298 /** 1299 * Navigate to a source position, possibly in the visual editor. 1300 * 1301 * @param pos The position to navigate to 1302 * @param navCommand The command that actually performs the navigation 1303 */ navigateToVisualPosition(SourcePosition pos, CommandWith2Args<DocDisplay, SourcePosition> navCommand)1304 private void navigateToVisualPosition(SourcePosition pos, 1305 CommandWith2Args<DocDisplay, SourcePosition> navCommand) 1306 { 1307 if (isVisualEditorActive()) 1308 { 1309 VisualModeChunk chunk = visualMode_.getChunkAtRow(pos.getRow()); 1310 if (chunk == null) 1311 { 1312 // No editor chunk at this position, so we need to switch to text 1313 // editor mode. 1314 ensureTextEditorActive(() -> 1315 { 1316 navCommand.execute(docDisplay_, pos); 1317 }); 1318 } 1319 else 1320 { 1321 // Adjust the position based on the chunk's offset and navigate 1322 // there. 1323 SourcePosition newPos = SourcePosition.create( 1324 pos.getRow() - chunk.getScope().getPreamble().getRow(), 1325 pos.getColumn()); 1326 navCommand.execute(chunk.getAceInstance(), newPos); 1327 chunk.focus(); 1328 1329 // Scroll the cursor into view; we have to do this after a layout 1330 // pass so that Ace has time to render the cursor. 1331 Scheduler.get().scheduleDeferred(() -> 1332 { 1333 chunk.scrollCursorIntoView(); 1334 }); 1335 } 1336 } 1337 else 1338 { 1339 // No visual editor active, so navigate directly 1340 navCommand.execute(docDisplay_, pos); 1341 } 1342 } 1343 1344 // These methods are called by SourceNavigationHistory and source pane management 1345 // features (e.g. external source window and source columns) so need to check for 1346 // and dispatch to visual mode 1347 1348 @Override recordCurrentNavigationPosition()1349 public void recordCurrentNavigationPosition() 1350 { 1351 if (visualMode_.isActivated()) 1352 { 1353 visualMode_.recordCurrentNavigationPosition(); 1354 } 1355 else 1356 { 1357 docDisplay_.recordCurrentNavigationPosition(); 1358 } 1359 } 1360 1361 1362 @Override restorePosition(SourcePosition position)1363 public void restorePosition(SourcePosition position) 1364 { 1365 if (visualMode_.isVisualModePosition(position)) 1366 { 1367 ensureVisualModeActive(() -> { 1368 visualMode_.navigate(position, false); 1369 }); 1370 } 1371 else 1372 { 1373 ensureTextEditorActive(() -> { 1374 docDisplay_.restorePosition(position); 1375 }); 1376 } 1377 } 1378 1379 @Override currentPosition()1380 public SourcePosition currentPosition() 1381 { 1382 if (visualMode_.isActivated()) 1383 { 1384 return visualMode_.getSourcePosition(); 1385 } 1386 else 1387 { 1388 Position cursor = docDisplay_.getCursorPosition(); 1389 if (docDisplay_.hasLineWidgets()) 1390 { 1391 // if we have line widgets, they create an non-reproducible scroll 1392 // position, so use the cursor position only 1393 return SourcePosition.create(cursor.getRow(), cursor.getColumn()); 1394 } 1395 return SourcePosition.create(getContext(), cursor.getRow(), 1396 cursor.getColumn(), docDisplay_.getScrollTop()); 1397 } 1398 1399 } 1400 1401 @Override isAtSourceRow(SourcePosition position)1402 public boolean isAtSourceRow(SourcePosition position) 1403 { 1404 if (visualMode_.isActivated()) 1405 { 1406 return visualMode_.isAtRow(position); 1407 } 1408 else 1409 { 1410 return docDisplay_.isAtSourceRow(position); 1411 } 1412 1413 } 1414 1415 @Override setCursorPosition(Position position)1416 public void setCursorPosition(Position position) 1417 { 1418 docDisplay_.setCursorPosition(position); 1419 } 1420 1421 @Override ensureCursorVisible()1422 public void ensureCursorVisible() 1423 { 1424 docDisplay_.ensureCursorVisible(); 1425 } 1426 1427 @Override forceLineHighlighting()1428 public void forceLineHighlighting() 1429 { 1430 docDisplay_.setHighlightSelectedLine(true); 1431 } 1432 1433 @Override setSourceOnSave(boolean sourceOnSave)1434 public void setSourceOnSave(boolean sourceOnSave) 1435 { 1436 if (view_ != null) 1437 { 1438 view_.getSourceOnSave().setValue(sourceOnSave, true); 1439 } 1440 } 1441 1442 @Override highlightDebugLocation( SourcePosition startPos, SourcePosition endPos, boolean executing)1443 public void highlightDebugLocation( 1444 SourcePosition startPos, 1445 SourcePosition endPos, 1446 boolean executing) 1447 { 1448 debugStartPos_ = startPos; 1449 debugEndPos_ = endPos; 1450 docDisplay_.highlightDebugLocation(startPos, endPos, executing); 1451 updateDebugWarningBar(); 1452 } 1453 1454 @Override endDebugHighlighting()1455 public void endDebugHighlighting() 1456 { 1457 docDisplay_.endDebugHighlighting(); 1458 debugStartPos_ = null; 1459 debugEndPos_ = null; 1460 updateDebugWarningBar(); 1461 } 1462 1463 @Override beginCollabSession(CollabEditStartParams params)1464 public void beginCollabSession(CollabEditStartParams params) 1465 { 1466 visualMode_.deactivate(() -> { 1467 // the server may notify us of a collab session we're already 1468 // participating in; this is okay 1469 if (docDisplay_.hasActiveCollabSession()) 1470 { 1471 return; 1472 } 1473 1474 // were we waiting to process another set of params when these arrived? 1475 boolean paramQueueClear = queuedCollabParams_ == null; 1476 1477 // save params 1478 queuedCollabParams_ = params; 1479 1480 // if we're not waiting for another set of params to resolve, and we're 1481 // the active doc, process these params immediately 1482 if (paramQueueClear && isActiveDocument()) 1483 { 1484 beginQueuedCollabSession(); 1485 } 1486 }); 1487 } 1488 1489 @Override endCollabSession()1490 public void endCollabSession() 1491 { 1492 if (docDisplay_.hasActiveCollabSession()) 1493 docDisplay_.endCollabSession(); 1494 1495 // a collaboration session may have come and gone while the tab was not 1496 // focused 1497 queuedCollabParams_ = null; 1498 } 1499 beginQueuedCollabSession()1500 private void beginQueuedCollabSession() 1501 { 1502 // do nothing if we don't have an active path 1503 if (docUpdateSentinel_ == null || docUpdateSentinel_.getPath() == null) 1504 return; 1505 1506 // do nothing if we don't have queued params 1507 final CollabEditStartParams params = queuedCollabParams_; 1508 if (params == null) 1509 return; 1510 1511 // if we have local changes, and we're not the master copy nor rejoining a 1512 // previous edit session, we need to prompt the user 1513 if (dirtyState().getValue() && !params.isMaster() && 1514 !params.isRejoining()) 1515 { 1516 String filename = 1517 FilePathUtils.friendlyFileName(docUpdateSentinel_.getPath()); 1518 globalDisplay_.showYesNoMessage( 1519 GlobalDisplay.MSG_QUESTION, 1520 "Join Edit Session", 1521 "You have unsaved changes to " + filename + ", but another " + 1522 "user is editing the file. Do you want to discard your " + 1523 "changes and join their edit session, or make your own copy " + 1524 "of the file to work on?", 1525 false, // includeCancel 1526 new Operation() 1527 { 1528 @Override 1529 public void execute() 1530 { 1531 docDisplay_.beginCollabSession(params, dirtyState_); 1532 queuedCollabParams_ = null; 1533 } 1534 }, 1535 new Operation() 1536 { 1537 @Override 1538 public void execute() 1539 { 1540 // open a new tab for the user's local changes 1541 events_.fireEvent(new NewWorkingCopyEvent(fileType_, 1542 docUpdateSentinel_.getPath(), 1543 docUpdateSentinel_.getContents())); 1544 1545 // let the collab session initiate in this tab 1546 docDisplay_.beginCollabSession(params, dirtyState_); 1547 queuedCollabParams_ = null; 1548 } 1549 }, 1550 null, // cancelOperation, 1551 "Discard and Join", 1552 "Work on a Copy", 1553 true // yesIsDefault 1554 ); 1555 } 1556 else 1557 { 1558 // just begin the session right away 1559 docDisplay_.beginCollabSession(params, dirtyState_); 1560 queuedCollabParams_ = null; 1561 } 1562 } 1563 updateDebugWarningBar()1564 private void updateDebugWarningBar() 1565 { 1566 // show the warning bar if we're debugging and the document is dirty 1567 if (debugStartPos_ != null && 1568 dirtyState().getValue() && 1569 !isDebugWarningVisible_) 1570 { 1571 view_.showWarningBar("Debug lines may not match because the file contains unsaved changes."); 1572 isDebugWarningVisible_ = true; 1573 } 1574 // hide the warning bar if the dirty state or debug state change 1575 else if (isDebugWarningVisible_ && 1576 (debugStartPos_ == null || dirtyState().getValue() == false)) 1577 { 1578 view_.hideWarningBar(); 1579 // if we're still debugging, start highlighting the line again 1580 if (debugStartPos_ != null) 1581 { 1582 docDisplay_.highlightDebugLocation( 1583 debugStartPos_, 1584 debugEndPos_, false); 1585 } 1586 isDebugWarningVisible_ = false; 1587 } 1588 } 1589 showWarningMessage(String message)1590 public void showWarningMessage(String message) 1591 { 1592 view_.showWarningBar(message); 1593 } 1594 showRequiredPackagesMissingWarning(List<String> packages)1595 public void showRequiredPackagesMissingWarning(List<String> packages) 1596 { 1597 view_.showRequiredPackagesMissingWarning(packages); 1598 } 1599 showTexInstallationMissingWarning(String message)1600 public void showTexInstallationMissingWarning(String message) 1601 { 1602 view_.showTexInstallationMissingWarning(message); 1603 } 1604 installTinyTeX()1605 public void installTinyTeX() 1606 { 1607 Command onInstall = () -> { 1608 String code = "tinytex::install_tinytex()"; 1609 events_.fireEvent(new SendToConsoleEvent(code, true)); 1610 }; 1611 1612 dependencyManager_.withTinyTeX( 1613 "Installing tinytex", 1614 "Installing TinyTeX", 1615 onInstall); 1616 } 1617 jumpToPreviousFunction()1618 private void jumpToPreviousFunction() 1619 { 1620 Scope jumpTo = scopeHelper_.getPreviousFunction( 1621 docDisplay_.getCursorPosition()); 1622 1623 if (jumpTo != null) 1624 docDisplay_.navigateToPosition(toSourcePosition(jumpTo), true); 1625 } 1626 jumpToNextFunction()1627 private void jumpToNextFunction() 1628 { 1629 Scope jumpTo = scopeHelper_.getNextFunction( 1630 docDisplay_.getCursorPosition()); 1631 1632 if (jumpTo != null) 1633 docDisplay_.navigateToPosition(toSourcePosition(jumpTo), true); 1634 } 1635 initialize(SourceColumn column, final SourceDocument document, FileSystemContext fileContext, FileType type, EditingTargetNameProvider defaultNameProvider)1636 public void initialize(SourceColumn column, 1637 final SourceDocument document, 1638 FileSystemContext fileContext, 1639 FileType type, 1640 EditingTargetNameProvider defaultNameProvider) 1641 { 1642 id_ = document.getId(); 1643 fileContext_ = fileContext; 1644 fileType_ = (TextFileType) type; 1645 codeExecution_ = new EditingTargetCodeExecution(this, docDisplay_, getId(), 1646 this); 1647 extendedType_ = document.getExtendedType(); 1648 extendedType_ = rmarkdownHelper_.detectExtendedType(document.getContents(), 1649 extendedType_, 1650 fileType_); 1651 1652 themeHelper_ = new TextEditingTargetThemeHelper(this, events_, releaseOnDismiss_); 1653 1654 docUpdateSentinel_ = new DocUpdateSentinel( 1655 server_, 1656 docDisplay_, 1657 document, 1658 globalDisplay_.getProgressIndicator("Save File"), 1659 dirtyState_, 1660 events_, 1661 prefs_, 1662 () -> 1663 { 1664 // Implement chunk definition provider 1665 if (visualMode_.isVisualEditorActive()) 1666 { 1667 return visualMode_.getChunkDefs(); 1668 } 1669 else 1670 { 1671 return docDisplay_.getChunkDefs(); 1672 } 1673 }); 1674 1675 view_ = new TextEditingTargetWidget(this, 1676 docUpdateSentinel_, 1677 commands_, 1678 prefs_, 1679 state_, 1680 fileTypeRegistry_, 1681 docDisplay_, 1682 fileType_, 1683 extendedType_, 1684 events_, 1685 session_, 1686 column); 1687 1688 packageDependencyHelper_ = new TextEditingTargetPackageDependencyHelper(this, docUpdateSentinel_, docDisplay_); 1689 1690 // create notebook and forward resize events 1691 chunks_ = new TextEditingTargetChunks(this); 1692 notebook_ = new TextEditingTargetNotebook(this, chunks_, view_, 1693 docDisplay_, dirtyState_, docUpdateSentinel_, document, 1694 releaseOnDismiss_, dependencyManager_); 1695 view_.addResizeHandler(notebook_); 1696 1697 // apply project properties 1698 projConfig_ = document.getProjectConfig(); 1699 if (projConfig_ != null) 1700 { 1701 docDisplay_.setUseSoftTabs(projConfig_.useSoftTabs()); 1702 docDisplay_.setTabSize(projConfig_.getTabSize()); 1703 } 1704 1705 // ensure that Makefile and Makevars always use tabs 1706 name_.addValueChangeHandler(new ValueChangeHandler<String>() { 1707 @Override 1708 public void onValueChange(ValueChangeEvent<String> event) 1709 { 1710 view_.setAccessibleName(name_.getValue()); 1711 FileSystemItem item = FileSystemItem.createFile(event.getValue()); 1712 if (shouldEnforceHardTabs(item)) 1713 docDisplay_.setUseSoftTabs(false); 1714 } 1715 }); 1716 1717 String name = getNameFromDocument(document, defaultNameProvider); 1718 name_.setValue(name, true); 1719 String contents = document.getContents(); 1720 1721 // disable change detection when setting code (since we're just doing 1722 // this to ensure the document's state reflects the server state and so 1723 // these aren't changes that diverge the document's client state from 1724 // the server state) 1725 docUpdateSentinel_.withChangeDetectionSuspended(() -> 1726 { 1727 docDisplay_.setCode(contents, false); 1728 }); 1729 1730 // Discover dependencies on file first open. 1731 packageDependencyHelper_.discoverPackageDependencies(); 1732 1733 // Load and apply folds. 1734 final ArrayList<Fold> folds = Fold.decode(document.getFoldSpec()); 1735 Scheduler.get().scheduleDeferred(new ScheduledCommand() 1736 { 1737 @Override 1738 public void execute() 1739 { 1740 // disable change detection when adding folds (since we're just doing 1741 // this to ensure the document's state reflects the server state and so 1742 // these aren't changes that diverge the document's client state from 1743 // the server state) 1744 docUpdateSentinel_.withChangeDetectionSuspended(() -> 1745 { 1746 for (Fold fold : folds) 1747 docDisplay_.addFold(fold.getRange()); 1748 }); 1749 } 1750 }); 1751 1752 // Load and apply Vim marks (if they exist). 1753 if (document.getProperties().hasKey("marks")) 1754 { 1755 final String marksSpec = document.getProperties().getString("marks"); 1756 final JsMap<Position> marks = VimMarks.decode(marksSpec); 1757 1758 // Time out the marks setting just to avoid conflict with other 1759 // mutations of the editor. 1760 new Timer() 1761 { 1762 @Override 1763 public void run() 1764 { 1765 docDisplay_.setMarks(marks); 1766 } 1767 }.schedule(100); 1768 } 1769 1770 TextEditingTargetPrefsHelper.registerPrefs( 1771 releaseOnDismiss_, prefs_, projConfig_, docDisplay_, document); 1772 1773 // Initialize sourceOnSave, and keep it in sync. Don't source on save 1774 // (regardless of preference) in auto save mode, which is mutually 1775 // exclusive with the manual source-and-save workflow. 1776 boolean sourceOnSave = document.sourceOnSave(); 1777 if (prefs_.autoSaveEnabled()) 1778 sourceOnSave = false; 1779 view_.getSourceOnSave().setValue(sourceOnSave, false); 1780 view_.getSourceOnSave().addValueChangeHandler(new ValueChangeHandler<Boolean>() 1781 { 1782 public void onValueChange(ValueChangeEvent<Boolean> event) 1783 { 1784 docUpdateSentinel_.setSourceOnSave( 1785 event.getValue(), 1786 globalDisplay_.getProgressIndicator("Error Saving Setting")); 1787 } 1788 }); 1789 1790 if (document.isDirty()) 1791 dirtyState_.markDirty(false); 1792 else 1793 dirtyState_.markClean(); 1794 docDisplay_.addValueChangeHandler(new ValueChangeHandler<Void>() 1795 { 1796 public void onValueChange(ValueChangeEvent<Void> event) 1797 { 1798 dirtyState_.markDirty(true); 1799 docDisplay_.clearSelectionHistory(); 1800 1801 // Nudge autosave timer (so it doesn't fire while the document is 1802 // actively mutating) 1803 nudgeAutosave(); 1804 } 1805 }); 1806 1807 docDisplay_.addFocusHandler(new FocusHandler() 1808 { 1809 public void onFocus(FocusEvent event) 1810 { 1811 // let anyone listening know this doc just got focus 1812 events_.fireEvent(new DocFocusedEvent(getPath(), getId())); 1813 1814 if (queuedCollabParams_ != null) 1815 { 1816 // join an in-progress collab session if we aren't already part 1817 // of one 1818 if (docDisplay_ != null && !docDisplay_.hasActiveCollabSession()) 1819 { 1820 beginQueuedCollabSession(); 1821 } 1822 } 1823 1824 // check to see if the file's been saved externally--we do this even 1825 // in a collaborative editing session so we can get delete 1826 // notifications 1827 checkForExternalEdit(500); 1828 1829 // relint on blur 1830 lintManager_.relintAfterDelay(100); 1831 } 1832 }); 1833 1834 1835 if (fileType_.isR()) 1836 { 1837 docDisplay_.addBreakpointSetHandler(new BreakpointSetEvent.Handler() 1838 { 1839 @Override 1840 public void onBreakpointSet(BreakpointSetEvent event) 1841 { 1842 if (event.isSet()) 1843 { 1844 Breakpoint breakpoint = null; 1845 1846 // don't set breakpoints in Plumber documents 1847 if (SourceDocument.isPlumberFile(extendedType_)) 1848 { 1849 view_.showWarningBar("Breakpoints not supported in Plumber API files."); 1850 return; 1851 } 1852 1853 // don't try to set breakpoints in unsaved code 1854 if (isNewDoc()) 1855 { 1856 view_.showWarningBar("Breakpoints cannot be set until " + 1857 "the file is saved."); 1858 return; 1859 } 1860 1861 Position breakpointPosition = 1862 Position.create(event.getLineNumber() - 1, 1); 1863 1864 // if we're not in function scope, or this is a Shiny file, 1865 // set a top-level (aka. Shiny-deferred) breakpoint 1866 ScopeFunction innerFunction = null; 1867 if (extendedType_ == null || 1868 !extendedType_.startsWith(SourceDocument.XT_SHINY_PREFIX)) 1869 innerFunction = docDisplay_.getFunctionAtPosition( 1870 breakpointPosition, false); 1871 if (innerFunction == null || !innerFunction.isFunction() || 1872 StringUtil.isNullOrEmpty(innerFunction.getFunctionName())) 1873 { 1874 breakpoint = breakpointManager_.setTopLevelBreakpoint( 1875 getPath(), 1876 event.getLineNumber()); 1877 } 1878 1879 // the scope tree will find nested functions, but in R these 1880 // are addressable only as substeps of the parent function. 1881 // keep walking up the scope tree until we've reached the top 1882 // level function. 1883 else 1884 { 1885 while (innerFunction.getParentScope() != null && 1886 innerFunction.getParentScope().isFunction()) 1887 { 1888 innerFunction = (ScopeFunction) innerFunction.getParentScope(); 1889 } 1890 1891 String functionName = innerFunction.getFunctionName(); 1892 1893 breakpoint = breakpointManager_.setBreakpoint( 1894 getPath(), 1895 functionName, 1896 event.getLineNumber(), 1897 dirtyState().getValue() == false); 1898 } 1899 1900 docDisplay_.addOrUpdateBreakpoint(breakpoint); 1901 } 1902 else 1903 { 1904 breakpointManager_.removeBreakpoint(event.getBreakpointId()); 1905 } 1906 updateBreakpointWarningBar(); 1907 } 1908 }); 1909 1910 docDisplay_.addBreakpointMoveHandler(new BreakpointMoveEvent.Handler() 1911 { 1912 @Override 1913 public void onBreakpointMove(BreakpointMoveEvent event) 1914 { 1915 breakpointManager_.moveBreakpoint(event.getBreakpointId()); 1916 } 1917 }); 1918 } 1919 1920 // validate required components (e.g. Tex, knitr, C++ etc.) 1921 checkCompilePdfDependencies(); 1922 rmarkdownHelper_.verifyPrerequisites(view_, fileType_); 1923 1924 syncFontSize(releaseOnDismiss_, events_, view_, fontSizeManager_); 1925 1926 1927 releaseOnDismiss_.add(prefs_.softWrapRFiles().addValueChangeHandler( 1928 new ValueChangeHandler<Boolean>() 1929 { 1930 public void onValueChange(ValueChangeEvent<Boolean> evt) 1931 { 1932 view_.adaptToFileType(fileType_); 1933 } 1934 } 1935 )); 1936 1937 releaseOnDismiss_.add(events_.addHandler(FileChangeEvent.TYPE, 1938 new FileChangeEvent.Handler() { 1939 @Override 1940 public void onFileChange(FileChangeEvent event) 1941 { 1942 // screen out adds and events that aren't for our path 1943 FileChange fileChange = event.getFileChange(); 1944 if (fileChange.getType() == FileChange.ADD) 1945 return; 1946 else if (!fileChange.getFile().getPath().equals(getPath())) 1947 return; 1948 1949 // always check for changes if this is the active editor 1950 if (isActiveDocument()) 1951 checkForExternalEdit(); 1952 1953 // also check for changes on modifications if we are not dirty 1954 // note that we don't check for changes on removed files because 1955 // this will show a confirmation dialog 1956 else if (event.getFileChange().getType() == FileChange.MODIFIED && 1957 dirtyState().getValue() == false) 1958 { 1959 checkForExternalEdit(); 1960 } 1961 } 1962 })); 1963 1964 spelling_ = new TextEditingTargetSpelling(docDisplay_, docUpdateSentinel_, lintManager_, prefs_); 1965 1966 1967 // show/hide the debug toolbar when the dirty state changes. (note: 1968 // this doesn't yet handle the case where the user saves the document, 1969 // in which case we should still show some sort of warning.) 1970 dirtyState().addValueChangeHandler(new ValueChangeHandler<Boolean>() 1971 { 1972 public void onValueChange(ValueChangeEvent<Boolean> evt) 1973 { 1974 updateDebugWarningBar(); 1975 } 1976 } 1977 ); 1978 1979 // find all of the debug breakpoints set in this document and replay them 1980 // onto the edit surface 1981 ArrayList<Breakpoint> breakpoints = 1982 breakpointManager_.getBreakpointsInFile(getPath()); 1983 for (Breakpoint breakpoint: breakpoints) 1984 { 1985 docDisplay_.addOrUpdateBreakpoint(breakpoint); 1986 } 1987 1988 view_.addRmdFormatChangedHandler(new RmdOutputFormatChangedEvent.Handler() 1989 { 1990 @Override 1991 public void onRmdOutputFormatChanged(RmdOutputFormatChangedEvent event) 1992 { 1993 if (event.isQuarto()) 1994 setQuartoFormat(event.getFormat()); 1995 else 1996 setRmdFormat(event.getFormat()); 1997 } 1998 }); 1999 2000 docDisplay_.addCursorChangedHandler(new CursorChangedEvent.Handler() 2001 { 2002 final Timer timer_ = new Timer() 2003 { 2004 @Override 2005 public void run() 2006 { 2007 HashMap<String, String> properties = new HashMap<>(); 2008 2009 properties.put( 2010 PROPERTY_CURSOR_POSITION, 2011 Position.serialize(docDisplay_.getCursorPosition())); 2012 2013 properties.put( 2014 PROPERTY_SCROLL_LINE, 2015 String.valueOf(docDisplay_.getFirstFullyVisibleRow())); 2016 2017 docUpdateSentinel_.modifyProperties(properties); 2018 } 2019 }; 2020 2021 @Override 2022 public void onCursorChanged(CursorChangedEvent event) 2023 { 2024 if (prefs_.restoreSourceDocumentCursorPosition().getValue()) 2025 timer_.schedule(1000); 2026 } 2027 }); 2028 2029 2030 // initialize visual mode 2031 visualMode_ = new VisualMode( 2032 TextEditingTarget.this, 2033 view_, 2034 rmarkdownHelper_, 2035 docDisplay_, 2036 dirtyState_, 2037 docUpdateSentinel_, 2038 events_, 2039 releaseOnDismiss_ 2040 ); 2041 2042 // populate the popup menu with a list of available formats 2043 if (extendedType_.startsWith(SourceDocument.XT_RMARKDOWN_PREFIX) || 2044 extendedType_.equals(SourceDocument.XT_QUARTO_DOCUMENT)) 2045 { 2046 updateRmdFormat(); 2047 setRMarkdownBehaviorEnabled(true); 2048 } 2049 2050 2051 // provide find replace button to view 2052 view_.addVisualModeFindReplaceButton(visualMode_.getFindReplaceButton()); 2053 2054 // update status bar when visual mode status changes 2055 releaseOnDismiss_.add( 2056 docUpdateSentinel_.addPropertyValueChangeHandler(RMD_VISUAL_MODE, (value) -> { 2057 updateStatusBarLanguage(); 2058 }) 2059 ); 2060 2061 Scheduler.get().scheduleDeferred(new ScheduledCommand() 2062 { 2063 @Override 2064 public void execute() 2065 { 2066 if (!prefs_.restoreSourceDocumentCursorPosition().getValue()) 2067 return; 2068 2069 String cursorPosition = docUpdateSentinel_.getProperty( 2070 PROPERTY_CURSOR_POSITION, 2071 ""); 2072 2073 if (StringUtil.isNullOrEmpty(cursorPosition)) 2074 return; 2075 2076 2077 int scrollLine = StringUtil.parseInt( 2078 docUpdateSentinel_.getProperty(PROPERTY_SCROLL_LINE, "0"), 2079 0); 2080 2081 Position position = Position.deserialize(cursorPosition); 2082 docDisplay_.setCursorPosition(position); 2083 docDisplay_.scrollToLine(scrollLine, false); 2084 docDisplay_.setScrollLeft(0); 2085 } 2086 }); 2087 2088 syncPublishPath(document.getPath()); 2089 initStatusBar(); 2090 lintManager_.relintAfterDelay(prefs_.documentLoadLintDelay().getValue()); 2091 } 2092 updateBreakpointWarningBar()2093 private void updateBreakpointWarningBar() 2094 { 2095 // check to see if there are any inactive breakpoints in this file 2096 boolean hasInactiveBreakpoints = false; 2097 boolean hasDebugPendingBreakpoints = false; 2098 boolean hasPackagePendingBreakpoints = false; 2099 String pendingPackageName = ""; 2100 ArrayList<Breakpoint> breakpoints = 2101 breakpointManager_.getBreakpointsInFile(getPath()); 2102 for (Breakpoint breakpoint: breakpoints) 2103 { 2104 if (breakpoint.getState() == Breakpoint.STATE_INACTIVE) 2105 { 2106 if (breakpoint.isPendingDebugCompletion()) 2107 { 2108 hasDebugPendingBreakpoints = true; 2109 } 2110 else if (breakpoint.isPackageBreakpoint()) 2111 { 2112 hasPackagePendingBreakpoints = true; 2113 pendingPackageName = breakpoint.getPackageName(); 2114 } 2115 else 2116 { 2117 hasInactiveBreakpoints = true; 2118 } 2119 break; 2120 } 2121 } 2122 boolean showWarning = hasDebugPendingBreakpoints || 2123 hasInactiveBreakpoints || 2124 hasPackagePendingBreakpoints; 2125 2126 if (showWarning && !isBreakpointWarningVisible_) 2127 { 2128 String message = ""; 2129 if (hasDebugPendingBreakpoints) 2130 { 2131 message = "Breakpoints will be activated when the file or " + 2132 "function is finished executing."; 2133 } 2134 else if (isPackageFile()) 2135 { 2136 message = "Breakpoints will be activated when the package is " + 2137 "built and reloaded."; 2138 } 2139 else if (hasPackagePendingBreakpoints) 2140 { 2141 message = "Breakpoints will be activated when an updated version " + 2142 "of the " + pendingPackageName + " package is loaded"; 2143 } 2144 else 2145 { 2146 message = "Breakpoints will be activated when this file is " + 2147 "sourced."; 2148 } 2149 view_.showWarningBar(message); 2150 isBreakpointWarningVisible_ = true; 2151 } 2152 else if (!showWarning && isBreakpointWarningVisible_) 2153 { 2154 hideBreakpointWarningBar(); 2155 } 2156 } 2157 hideBreakpointWarningBar()2158 private void hideBreakpointWarningBar() 2159 { 2160 if (isBreakpointWarningVisible_) 2161 { 2162 view_.hideWarningBar(); 2163 isBreakpointWarningVisible_ = false; 2164 } 2165 } 2166 isPackageFile()2167 private boolean isPackageFile() 2168 { 2169 // not a package file if we're not in package development mode 2170 String type = session_.getSessionInfo().getBuildToolsType(); 2171 if (!type.equals(SessionInfo.BUILD_TOOLS_PACKAGE)) 2172 { 2173 return false; 2174 } 2175 2176 // get the directory associated with the project and see if the file is 2177 // inside that directory 2178 FileSystemItem projectDir = session_.getSessionInfo() 2179 .getActiveProjectDir(); 2180 return getPath().startsWith(projectDir.getPath() + "/R"); 2181 } 2182 isPackageDocumentationFile()2183 private boolean isPackageDocumentationFile() 2184 { 2185 if (getPath() == null) 2186 { 2187 return false; 2188 } 2189 2190 String type = session_.getSessionInfo().getBuildToolsType(); 2191 if (!type.equals(SessionInfo.BUILD_TOOLS_PACKAGE)) 2192 { 2193 return false; 2194 } 2195 2196 FileSystemItem srcFile = FileSystemItem.createFile(getPath()); 2197 FileSystemItem projectDir = session_.getSessionInfo() 2198 .getActiveProjectDir(); 2199 if (srcFile.getPath().startsWith(projectDir.getPath() + "/vignettes")) 2200 return true; 2201 else if (srcFile.getParentPathString().equals(projectDir.getPath()) && 2202 srcFile.getExtension().toLowerCase().equals(".md")) 2203 return true; 2204 else 2205 return false; 2206 } 2207 checkCompilePdfDependencies()2208 private void checkCompilePdfDependencies() 2209 { 2210 compilePdfHelper_.checkCompilers(view_, fileType_); 2211 } 2212 initStatusBar()2213 private void initStatusBar() 2214 { 2215 statusBar_ = view_.getStatusBar(); 2216 docDisplay_.addCursorChangedHandler(new CursorChangedEvent.Handler() 2217 { 2218 public void onCursorChanged(CursorChangedEvent event) 2219 { 2220 updateStatusBarPosition(); 2221 if (docDisplay_.isScopeTreeReady(event.getPosition().getRow())) 2222 updateCurrentScope(); 2223 2224 } 2225 }); 2226 updateStatusBarPosition(); 2227 updateStatusBarLanguage(); 2228 2229 // build file type menu dynamically (so it can change according 2230 // to whether e.g. knitr is installed) 2231 statusBar_.getLanguage().addMouseDownHandler(new MouseDownHandler() { 2232 2233 @Override 2234 public void onMouseDown(MouseDownEvent event) 2235 { 2236 // build menu with all file types - also track whether we need 2237 // to add the current type (may be the case for types which we 2238 // support but don't want to expose on the menu -- e.g. Rmd 2239 // files when knitr isn't installed) 2240 boolean addCurrentType = true; 2241 final StatusBarPopupMenu menu = new StatusBarPopupMenu(); 2242 List<TextFileType> fileTypes = fileTypeCommands_.statusBarFileTypes(); 2243 for (TextFileType type : fileTypes) 2244 { 2245 menu.addItem(createMenuItemForType(type)); 2246 if (addCurrentType && type.equals(fileType_)) 2247 addCurrentType = false; 2248 } 2249 2250 // add the current type if isn't on the menu 2251 if (addCurrentType) 2252 menu.addItem(createMenuItemForType(fileType_)); 2253 2254 // show the menu 2255 menu.showRelativeToUpward((UIObject) statusBar_.getLanguage(), 2256 true); 2257 } 2258 }); 2259 2260 statusBar_.getScope().addMouseDownHandler(new MouseDownHandler() 2261 { 2262 public void onMouseDown(MouseDownEvent event) 2263 { 2264 // Unlike the other status bar elements, the function outliner 2265 // needs its menu built on demand 2266 if (fileType_.isRpres()) 2267 { 2268 String path = docUpdateSentinel_.getPath(); 2269 if (path != null) 2270 { 2271 presentationHelper_.buildSlideMenu( 2272 docUpdateSentinel_.getPath(), 2273 dirtyState_.getValue(), 2274 TextEditingTarget.this, 2275 new CommandWithArg<StatusBarPopupRequest>() { 2276 2277 @Override 2278 public void execute(StatusBarPopupRequest request) 2279 { 2280 showStatusBarPopupMenu(request); 2281 } 2282 }); 2283 } 2284 } 2285 else if (isVisualEditorActive()) 2286 { 2287 showStatusBarPopupMenu(visualMode_.getStatusBarPopup()); 2288 } 2289 else 2290 { 2291 final StatusBarPopupMenu menu = new StatusBarPopupMenu(); 2292 JsArray<Scope> tree = docDisplay_.getScopeTree(); 2293 MenuItem defaultItem = addFunctionsToMenu( 2294 menu, tree, "", docDisplay_.getCurrentScope(), true); 2295 2296 showStatusBarPopupMenu(new StatusBarPopupRequest(menu, 2297 defaultItem)); 2298 } 2299 } 2300 }); 2301 } 2302 showStatusBarPopupMenu(StatusBarPopupRequest popupRequest)2303 private void showStatusBarPopupMenu(StatusBarPopupRequest popupRequest) 2304 { 2305 final StatusBarPopupMenu menu = popupRequest.getMenu(); 2306 MenuItem defaultItem = popupRequest.getDefaultMenuItem(); 2307 if (defaultItem != null) 2308 { 2309 menu.selectItem(defaultItem); 2310 Scheduler.get().scheduleFinally(new RepeatingCommand() 2311 { 2312 public boolean execute() 2313 { 2314 menu.ensureSelectedIsVisible(); 2315 return false; 2316 } 2317 }); 2318 } 2319 menu.showRelativeToUpward((UIObject) statusBar_.getScope(), false); 2320 } 2321 createMenuItemForType(final TextFileType type)2322 private MenuItem createMenuItemForType(final TextFileType type) 2323 { 2324 SafeHtmlBuilder labelBuilder = new SafeHtmlBuilder(); 2325 labelBuilder.appendEscaped(type.getLabel()); 2326 2327 MenuItem menuItem = new MenuItem( 2328 labelBuilder.toSafeHtml(), 2329 new Command() 2330 { 2331 public void execute() 2332 { 2333 ensureTextEditorActive(() -> { 2334 docUpdateSentinel_.changeFileType( 2335 type.getTypeId(), 2336 new SaveProgressIndicator(null, type, false,null)); 2337 2338 Scheduler.get().scheduleDeferred(new ScheduledCommand() { 2339 @Override 2340 public void execute() 2341 { 2342 focus(); 2343 } 2344 }); 2345 }); 2346 } 2347 }); 2348 2349 return menuItem; 2350 } 2351 addScopeStyle(MenuItem item, Scope scope)2352 private void addScopeStyle(MenuItem item, Scope scope) 2353 { 2354 if (scope.isSection()) 2355 item.getElement().getStyle().setFontWeight(FontWeight.BOLD); 2356 } 2357 addFunctionsToMenu(StatusBarPopupMenu menu, final JsArray<Scope> funcs, String indent, Scope defaultFunction, boolean includeNoFunctionsMessage)2358 private MenuItem addFunctionsToMenu(StatusBarPopupMenu menu, 2359 final JsArray<Scope> funcs, 2360 String indent, 2361 Scope defaultFunction, 2362 boolean includeNoFunctionsMessage) 2363 { 2364 MenuItem defaultMenuItem = null; 2365 2366 if (funcs.length() == 0 && includeNoFunctionsMessage) 2367 { 2368 String type = fileType_.canExecuteChunks() ? "chunks" : "functions"; 2369 MenuItem noFunctions = new MenuItem("(No " + type + " defined)", 2370 false, 2371 (Command) null); 2372 noFunctions.setEnabled(false); 2373 noFunctions.getElement().addClassName("disabled"); 2374 menu.addItem(noFunctions); 2375 } 2376 2377 for (int i = 0; i < funcs.length(); i++) 2378 { 2379 final Scope func = funcs.get(i); 2380 2381 String childIndent = indent; 2382 if (!StringUtil.isNullOrEmpty(func.getLabel())) 2383 { 2384 SafeHtmlBuilder labelBuilder = new SafeHtmlBuilder(); 2385 labelBuilder.appendHtmlConstant(indent); 2386 labelBuilder.appendEscaped(func.getLabel()); 2387 2388 final MenuItem menuItem = new MenuItem( 2389 labelBuilder.toSafeHtml(), 2390 new Command() 2391 { 2392 public void execute() 2393 { 2394 docDisplay_.navigateToPosition(toSourcePosition(func), 2395 true); 2396 } 2397 }); 2398 addScopeStyle(menuItem, func); 2399 menu.addItem(menuItem); 2400 2401 childIndent = indent + " "; 2402 2403 if (defaultFunction != null && defaultMenuItem == null && 2404 func.getLabel() == defaultFunction.getLabel() && 2405 func.getPreamble().getRow() == defaultFunction.getPreamble().getRow() && 2406 func.getPreamble().getColumn() == defaultFunction.getPreamble().getColumn()) 2407 { 2408 defaultMenuItem = menuItem; 2409 } 2410 } 2411 2412 MenuItem childDefaultMenuItem = addFunctionsToMenu( 2413 menu, 2414 func.getChildren(), 2415 childIndent, 2416 defaultMenuItem == null ? defaultFunction : null, 2417 false); 2418 if (childDefaultMenuItem != null) 2419 defaultMenuItem = childDefaultMenuItem; 2420 } 2421 2422 return defaultMenuItem; 2423 } 2424 updateStatusBarLanguage()2425 private void updateStatusBarLanguage() 2426 { 2427 statusBar_.getLanguage().setValue(fileType_.getLabel()); 2428 boolean canShowScope = fileType_.canShowScopeTree(); 2429 statusBar_.setScopeVisible(canShowScope); 2430 } 2431 updateStatusBarPosition()2432 private void updateStatusBarPosition() 2433 { 2434 Position pos = docDisplay_.getCursorPosition(); 2435 statusBar_.getPosition().setValue((pos.getRow() + 1) + ":" + 2436 (pos.getColumn() + 1)); 2437 } 2438 updateStatusBarLocation(String title, int type)2439 public void updateStatusBarLocation(String title, int type) 2440 { 2441 statusBar_.setScopeType(type); 2442 statusBar_.getScope().setValue(title); 2443 } 2444 updateCurrentScope()2445 private void updateCurrentScope() 2446 { 2447 // don't sync scope if we can't show a scope tree or in visual mode (which 2448 // is responsible for updating the scope visualization itself) 2449 if (fileType_ == null || !fileType_.canShowScopeTree() || isVisualModeActivated()) 2450 return; 2451 2452 // special handing for presentations since we extract 2453 // the slide structure in a different manner than 2454 // the editor scope trees 2455 if (fileType_.isRpres()) 2456 { 2457 statusBar_.getScope().setValue( 2458 presentationHelper_.getCurrentSlide()); 2459 statusBar_.setScopeType(StatusBar.SCOPE_SLIDE); 2460 2461 } 2462 else 2463 { 2464 Scope scope = docDisplay_.getCurrentScope(); 2465 String label = scope != null 2466 ? scope.getLabel() 2467 : null; 2468 statusBar_.getScope().setValue(label); 2469 2470 if (scope != null) 2471 { 2472 boolean useChunk = 2473 scope.isChunk() || 2474 (fileType_.isRnw() && scope.isTopLevel()); 2475 if (useChunk) 2476 statusBar_.setScopeType(StatusBar.SCOPE_CHUNK); 2477 else if (scope.isNamespace()) 2478 statusBar_.setScopeType(StatusBar.SCOPE_NAMESPACE); 2479 else if (scope.isClass()) 2480 statusBar_.setScopeType(StatusBar.SCOPE_CLASS); 2481 else if (scope.isSection()) 2482 statusBar_.setScopeType(StatusBar.SCOPE_SECTION); 2483 else if (scope.isTopLevel()) 2484 statusBar_.setScopeType(StatusBar.SCOPE_TOP_LEVEL); 2485 else if (scope.isFunction()) 2486 statusBar_.setScopeType(StatusBar.SCOPE_FUNCTION); 2487 else if (scope.isLambda()) 2488 statusBar_.setScopeType(StatusBar.SCOPE_LAMBDA); 2489 else if (scope.isAnon()) 2490 statusBar_.setScopeType(StatusBar.SCOPE_ANON); 2491 } 2492 } 2493 } 2494 getNameFromDocument(SourceDocument document, EditingTargetNameProvider defaultNameProvider)2495 private String getNameFromDocument(SourceDocument document, 2496 EditingTargetNameProvider defaultNameProvider) 2497 { 2498 if (document.getPath() != null) 2499 return FileSystemItem.getNameFromPath(document.getPath()); 2500 2501 String name = document.getProperties().getString("tempName"); 2502 if (!StringUtil.isNullOrEmpty(name)) 2503 return name; 2504 2505 String defaultName = defaultNameProvider.defaultNamePrefix(this); 2506 docUpdateSentinel_.setProperty("tempName", defaultName, null); 2507 return defaultName; 2508 } 2509 getFileSizeLimit()2510 public long getFileSizeLimit() 2511 { 2512 return 5 * 1024 * 1024; 2513 } 2514 getLargeFileSize()2515 public long getLargeFileSize() 2516 { 2517 return 2 * 1024 * 1024; 2518 } 2519 getPixelWidth()2520 public int getPixelWidth() 2521 { 2522 if (isVisualEditorActive()) 2523 { 2524 return visualMode_.getPixelWidth(); 2525 } 2526 else 2527 { 2528 return docDisplay_.getPixelWidth(); 2529 } 2530 } 2531 insertCode(String source, boolean blockMode)2532 public void insertCode(String source, boolean blockMode) 2533 { 2534 docDisplay_.insertCode(source, blockMode); 2535 } 2536 getSupportedCommands()2537 public HashSet<AppCommand> getSupportedCommands() 2538 { 2539 // start with the set of commands supported by the file type 2540 HashSet<AppCommand> commands = fileType_.getSupportedCommands(commands_); 2541 2542 // if the file has a path, it can also be renamed 2543 if (getPath() != null) 2544 { 2545 commands.add(commands_.renameSourceDoc()); 2546 } 2547 2548 return commands; 2549 } 2550 2551 @Override manageCommands()2552 public void manageCommands() 2553 { 2554 if (fileType_.isRmd()) 2555 notebook_.manageCommands(); 2556 2557 if (fileType_.isMarkdown()) 2558 { 2559 visualMode_.manageCommands(); 2560 quartoHelper_.manageCommands(); 2561 } 2562 2563 } 2564 2565 @Override getPaletteEntryProvider()2566 public CommandPaletteEntryProvider getPaletteEntryProvider() 2567 { 2568 if (visualMode_.isActivated()) 2569 { 2570 return visualMode_.getPaletteEntryProvider(); 2571 } 2572 else 2573 return null; 2574 } 2575 2576 @Override canCompilePdf()2577 public boolean canCompilePdf() 2578 { 2579 return fileType_.canCompilePDF(); 2580 } 2581 canExecuteChunks()2582 public boolean canExecuteChunks() 2583 { 2584 return fileType_.canExecuteChunks(); 2585 } 2586 2587 2588 @Override verifyCppPrerequisites()2589 public void verifyCppPrerequisites() 2590 { 2591 // NOTE: will be a no-op for non-c/c++ file types 2592 cppHelper_.checkBuildCppDependencies(this, view_, fileType_); 2593 } 2594 2595 @Override verifyPythonPrerequisites()2596 public void verifyPythonPrerequisites() 2597 { 2598 // TODO: ensure 'reticulate' installed 2599 } 2600 2601 @Override verifyD3Prerequisites()2602 public void verifyD3Prerequisites() 2603 { 2604 verifyD3Prequisites(null); 2605 } 2606 verifyD3Prequisites(final Command command)2607 private void verifyD3Prequisites(final Command command) 2608 { 2609 dependencyManager_.withR2D3("Previewing D3 scripts", new Command() { 2610 @Override 2611 public void execute() { 2612 if (command != null) 2613 command.execute(); 2614 } 2615 }); 2616 } 2617 2618 @Override verifyNewSqlPrerequisites()2619 public void verifyNewSqlPrerequisites() 2620 { 2621 verifyNewSqlPrerequisites(null); 2622 } 2623 verifyNewSqlPrerequisites(final Command command)2624 private void verifyNewSqlPrerequisites(final Command command) 2625 { 2626 dependencyManager_.withRSQLite("Previewing SQL scripts", new Command() { 2627 @Override 2628 public void execute() { 2629 if (command != null) 2630 command.execute(); 2631 } 2632 }); 2633 } 2634 verifySqlPrerequisites(final Command command)2635 private void verifySqlPrerequisites(final Command command) 2636 { 2637 dependencyManager_.withDBI("Previewing SQL scripts", new Command() { 2638 @Override 2639 public void execute() { 2640 if (command != null) 2641 command.execute(); 2642 } 2643 }); 2644 } 2645 focus()2646 public void focus() 2647 { 2648 if (isVisualModeActivated()) 2649 { 2650 visualMode_.focus(() -> 2651 { 2652 // Initialize notebook after activation if present (and notebook is 2653 // uninitialized) 2654 if (notebook_ != null && 2655 notebook_.getState() == TextEditingTargetNotebook.STATE_NONE) 2656 { 2657 notebook_.onRenderFinished(null); 2658 } 2659 }); 2660 } 2661 else 2662 { 2663 view_.editorContainer().focus(); 2664 } 2665 } 2666 replaceSelection(String value, Command callback)2667 public void replaceSelection(String value, Command callback) 2668 { 2669 if (isVisualModeActivated()) 2670 { 2671 ensureVisualModeActive(() -> 2672 { 2673 visualMode_.replaceSelection(value); 2674 callback.execute(); 2675 }); 2676 } 2677 else 2678 { 2679 ensureTextEditorActive(() -> 2680 { 2681 if (docDisplay_.hasSelection()) 2682 { 2683 docDisplay_.replaceSelection(value); 2684 } 2685 else 2686 { 2687 docDisplay_.insertCode(value); 2688 } 2689 2690 callback.execute(); 2691 }); 2692 } 2693 } 2694 getSelectedText()2695 public String getSelectedText() 2696 { 2697 if (docDisplay_.hasSelection()) 2698 return docDisplay_.getSelectionValue(); 2699 else 2700 return ""; 2701 } 2702 addEnsureVisibleHandler(EnsureVisibleEvent.Handler handler)2703 public HandlerRegistration addEnsureVisibleHandler(EnsureVisibleEvent.Handler handler) 2704 { 2705 return view_.addEnsureVisibleHandler(handler); 2706 } 2707 addEnsureHeightHandler(EnsureHeightEvent.Handler handler)2708 public HandlerRegistration addEnsureHeightHandler(EnsureHeightEvent.Handler handler) 2709 { 2710 return view_.addEnsureHeightHandler(handler); 2711 } 2712 addCloseHandler(CloseHandler<java.lang.Void> handler)2713 public HandlerRegistration addCloseHandler(CloseHandler<java.lang.Void> handler) 2714 { 2715 return handlers_.addHandler(CloseEvent.getType(), handler); 2716 } 2717 addEditorThemeStyleChangedHandler( EditorThemeStyleChangedEvent.Handler handler)2718 public HandlerRegistration addEditorThemeStyleChangedHandler( 2719 EditorThemeStyleChangedEvent.Handler handler) 2720 { 2721 return themeHelper_.addEditorThemeStyleChangedHandler(handler); 2722 } 2723 addInterruptChunkHandler(InterruptChunkEvent.Handler handler)2724 public HandlerRegistration addInterruptChunkHandler(InterruptChunkEvent.Handler handler) 2725 { 2726 return handlers_.addHandler(InterruptChunkEvent.TYPE, handler); 2727 } 2728 fireEvent(GwtEvent<?> event)2729 public void fireEvent(GwtEvent<?> event) 2730 { 2731 handlers_.fireEvent(event); 2732 } 2733 isActivated()2734 public boolean isActivated() 2735 { 2736 return commandHandlerReg_ != null; 2737 } 2738 onActivate()2739 public void onActivate() 2740 { 2741 // IMPORTANT NOTE: most of this logic is duplicated in 2742 // CodeBrowserEditingTarget (no straightforward way to create a 2743 // re-usable implementation) so changes here need to be synced 2744 2745 // If we're already hooked up for some reason, unhook. 2746 // This shouldn't happen though. 2747 if (commandHandlerReg_ != null) 2748 { 2749 Debug.log("Warning: onActivate called twice without intervening onDeactivate"); 2750 commandHandlerReg_.removeHandler(); 2751 commandHandlerReg_ = null; 2752 } 2753 commandHandlerReg_ = commandBinder.bind(commands_, this); 2754 2755 // show outline if not yet rendered (deferred so that widget itself can 2756 // be sized first) 2757 if (!docDisplay_.isRendered()) 2758 { 2759 Scheduler.get().scheduleDeferred(new ScheduledCommand() 2760 { 2761 @Override 2762 public void execute() 2763 { 2764 view_.initWidgetSize(); 2765 } 2766 }); 2767 } 2768 2769 Scheduler.get().scheduleFinally(new ScheduledCommand() 2770 { 2771 public void execute() 2772 { 2773 // This has to be executed in a scheduleFinally because 2774 // Source.manageCommands gets called after this.onActivate, 2775 // and if we're going from a non-editor (like data view) to 2776 // an editor, setEnabled(true) will be called on the command 2777 // in manageCommands. 2778 commands_.reopenSourceDocWithEncoding().setEnabled( 2779 docUpdateSentinel_.getPath() != null); 2780 } 2781 }); 2782 2783 // notify notebook of activation if necessary 2784 if (notebook_ != null) 2785 notebook_.onActivate(); 2786 2787 view_.onActivate(); 2788 } 2789 onDeactivate()2790 public void onDeactivate() 2791 { 2792 // IMPORTANT NOTE: most of this logic is duplicated in 2793 // CodeBrowserEditingTarget (no straightforward way to create a 2794 // re-usable implementation) so changes here need to be synced 2795 2796 externalEditCheckInvalidation_.invalidate(); 2797 2798 commandHandlerReg_.removeHandler(); 2799 commandHandlerReg_ = null; 2800 2801 // switching tabs is a navigation action 2802 try 2803 { 2804 recordCurrentNavigationPosition(); 2805 } 2806 catch(Exception e) 2807 { 2808 Debug.log("Exception recording nav position: " + e.toString()); 2809 } 2810 2811 visualMode_.unmanageCommands(); 2812 } 2813 2814 @Override onInitiallyLoaded()2815 public void onInitiallyLoaded() 2816 { 2817 checkForExternalEdit(); 2818 } 2819 onBeforeDismiss()2820 public boolean onBeforeDismiss() 2821 { 2822 final Command closeCommand = new Command() 2823 { 2824 public void execute() 2825 { 2826 // notify visual mode 2827 visualMode_.onClosing(); 2828 2829 // fire close event 2830 CloseEvent.fire(TextEditingTarget.this, null); 2831 } 2832 }; 2833 2834 2835 final Command promptCommand = new Command() 2836 { 2837 public void execute() 2838 { 2839 if (dirtyState_.getValue()) 2840 saveWithPrompt(closeCommand, null); 2841 else 2842 closeCommand.execute(); 2843 } 2844 }; 2845 2846 if (docDisplay_.hasFollowingCollabSession()) 2847 { 2848 globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_WARNING, 2849 getName().getValue() + " - Active Following Session", 2850 "You're actively following another user's cursor " + 2851 "in '" + getName().getValue() + "'.\n\n" + 2852 "If you close this file, you won't see their " + 2853 "cursor until they edit another file.", 2854 false, 2855 new Operation() 2856 { 2857 public void execute() 2858 { 2859 promptCommand.execute(); 2860 } 2861 }, 2862 null, 2863 null, 2864 "Close Anyway", 2865 "Cancel", 2866 false); 2867 } 2868 else 2869 { 2870 promptCommand.execute(); 2871 } 2872 2873 return false; 2874 } 2875 save()2876 public void save() 2877 { 2878 if (isSaving_) 2879 return; 2880 2881 save(new Command() { 2882 @Override 2883 public void execute() 2884 { 2885 }}); 2886 } 2887 autoSave(Command onCompleted, Command onSilentFailure)2888 private void autoSave(Command onCompleted, Command onSilentFailure) 2889 { 2890 saveThenExecute(null, false, CommandUtil.join(postSaveCommand(), onCompleted), onSilentFailure); 2891 } 2892 save(Command onCompleted)2893 public void save(Command onCompleted) 2894 { 2895 saveThenExecute(null, true, CommandUtil.join(postSaveCommand(), onCompleted)); 2896 } 2897 saveWithPrompt(final Command command, final Command onCancelled)2898 public void saveWithPrompt(final Command command, final Command onCancelled) 2899 { 2900 view_.ensureVisible(); 2901 2902 globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_WARNING, 2903 getName().getValue() + " - Unsaved Changes", 2904 "The document '" + getName().getValue() + 2905 "' has unsaved changes.\n\n" + 2906 "Do you want to save these changes?", 2907 true, 2908 new Operation() { 2909 public void execute() { saveThenExecute(null, true, command); } 2910 }, 2911 new Operation() { 2912 public void execute() { command.execute(); } 2913 }, 2914 new Operation() { 2915 public void execute() { 2916 if (onCancelled != null) 2917 onCancelled.execute(); 2918 } 2919 }, 2920 "Save", 2921 "Don't Save", 2922 true); 2923 } 2924 revertChanges(Command onCompleted)2925 public void revertChanges(Command onCompleted) 2926 { 2927 docUpdateSentinel_.revert(onCompleted, ignoreDeletes_); 2928 } 2929 saveThenExecute(String encodingOverride, boolean retryWrite, final Command command)2930 public void saveThenExecute(String encodingOverride, boolean retryWrite, final Command command) 2931 { 2932 saveThenExecute(encodingOverride, retryWrite, command, null); 2933 } 2934 saveThenExecute(String encodingOverride, boolean retryWrite, final Command command, final Command onSilentFailure)2935 public void saveThenExecute(String encodingOverride, boolean retryWrite, final Command command, final Command onSilentFailure) 2936 { 2937 isSaving_ = true; 2938 2939 checkCompilePdfDependencies(); 2940 2941 final String path = docUpdateSentinel_.getPath(); 2942 if (path == null) 2943 { 2944 saveNewFile(null, encodingOverride, command); 2945 return; 2946 } 2947 2948 withEncodingRequiredUnlessAscii( 2949 encodingOverride, 2950 new CommandWithArg<String>() 2951 { 2952 public void execute(String encoding) 2953 { 2954 fixupCodeBeforeSaving(() -> { 2955 docUpdateSentinel_.save(path, 2956 null, 2957 encoding, 2958 retryWrite, 2959 new SaveProgressIndicator( 2960 FileSystemItem.createFile(path), 2961 null, 2962 !retryWrite, 2963 command, 2964 onSilentFailure 2965 )); 2966 }); 2967 } 2968 }); 2969 } 2970 saveNewFile(final String suggestedPath, String encodingOverride, final Command executeOnSuccess)2971 private void saveNewFile(final String suggestedPath, 2972 String encodingOverride, 2973 final Command executeOnSuccess) 2974 { 2975 withEncodingRequiredUnlessAscii( 2976 encodingOverride, 2977 new CommandWithArg<String>() 2978 { 2979 public void execute(String encoding) 2980 { 2981 saveNewFileWithEncoding(suggestedPath, 2982 encoding, 2983 executeOnSuccess); 2984 } 2985 }); 2986 } 2987 withEncodingRequiredUnlessAscii( final String encodingOverride, final CommandWithArg<String> command)2988 private void withEncodingRequiredUnlessAscii( 2989 final String encodingOverride, 2990 final CommandWithArg<String> command) 2991 { 2992 String preferredDocumentEncoding = null; 2993 if (docDisplay_.getFileType().isRmd()) 2994 preferredDocumentEncoding = "UTF-8"; 2995 2996 final String encoding = StringUtil.firstNotNullOrEmpty(new String[] { 2997 encodingOverride, 2998 docUpdateSentinel_.getEncoding(), 2999 preferredDocumentEncoding, 3000 prefs_.defaultEncoding().getValue() 3001 }); 3002 3003 if (StringUtil.isNullOrEmpty(encoding)) 3004 { 3005 if (docUpdateSentinel_.isAscii()) 3006 { 3007 // Don't bother asking when it's just ASCII 3008 command.execute(null); 3009 } 3010 else 3011 { 3012 withChooseEncoding(session_.getSessionInfo().getSystemEncoding(), 3013 new CommandWithArg<String>() 3014 { 3015 public void execute(String newEncoding) 3016 { 3017 command.execute(newEncoding); 3018 } 3019 }); 3020 } 3021 } 3022 else 3023 { 3024 command.execute(encoding); 3025 } 3026 } 3027 withChooseEncoding(final String defaultEncoding, final CommandWithArg<String> command)3028 private void withChooseEncoding(final String defaultEncoding, 3029 final CommandWithArg<String> command) 3030 { 3031 view_.ensureVisible(); 3032 3033 server_.iconvlist(new SimpleRequestCallback<IconvListResult>() 3034 { 3035 @Override 3036 public void onResponseReceived(IconvListResult response) 3037 { 3038 // Stupid compiler. Use this Value shim to make the dialog available 3039 // in its own handler. 3040 final HasValue<ChooseEncodingDialog> d = new Value<>(null); 3041 d.setValue(new ChooseEncodingDialog( 3042 response.getCommon(), 3043 response.getAll(), 3044 defaultEncoding, 3045 false, 3046 true, 3047 new OperationWithInput<String>() 3048 { 3049 public void execute(String newEncoding) 3050 { 3051 if (newEncoding == null) 3052 return; 3053 3054 if (d.getValue().isSaveAsDefault()) 3055 { 3056 prefs_.defaultEncoding().setGlobalValue(newEncoding); 3057 prefs_.writeUserPrefs(); 3058 } 3059 3060 command.execute(newEncoding); 3061 } 3062 })); 3063 d.getValue().showModal(); 3064 } 3065 }); 3066 3067 } 3068 saveNewFileWithEncoding(String suggestedPath, final String encoding, final Command executeOnSuccess)3069 private void saveNewFileWithEncoding(String suggestedPath, 3070 final String encoding, 3071 final Command executeOnSuccess) 3072 { 3073 view_.ensureVisible(); 3074 3075 FileSystemItem fsi; 3076 if (suggestedPath != null) 3077 fsi = FileSystemItem.createFile(suggestedPath); 3078 else 3079 fsi = getSaveFileDefaultDir(); 3080 3081 fileDialogs_.saveFile( 3082 "Save File - " + getName().getValue(), 3083 fileContext_, 3084 fsi, 3085 fileType_.getDefaultExtension(), 3086 false, 3087 new ProgressOperationWithInput<FileSystemItem>() 3088 { 3089 public void execute(final FileSystemItem saveItem, 3090 ProgressIndicator indicator) 3091 { 3092 // null here implies the user cancelled the save 3093 if (saveItem == null) 3094 { 3095 isSaving_ = false; 3096 return; 3097 } 3098 3099 try 3100 { 3101 workbenchContext_.setDefaultFileDialogDir( 3102 saveItem.getParentPath()); 3103 3104 final TextFileType fileType = 3105 fileTypeRegistry_.getTextTypeForFile(saveItem); 3106 3107 final Command saveCommand = new Command() { 3108 @Override 3109 public void execute() 3110 { 3111 if (getPath() != null && 3112 !getPath().equals(saveItem.getPath())) 3113 { 3114 // breakpoints are file-specific, so when saving 3115 // as a different file, clear the display of 3116 // breakpoints from the old file name 3117 docDisplay_.removeAllBreakpoints(); 3118 3119 // update publish settings 3120 syncPublishPath(saveItem.getPath()); 3121 } 3122 3123 fixupCodeBeforeSaving(() -> { 3124 docUpdateSentinel_.save( 3125 saveItem.getPath(), 3126 fileType.getTypeId(), 3127 encoding, 3128 true, 3129 new SaveProgressIndicator(saveItem, 3130 fileType, 3131 false, 3132 executeOnSuccess)); 3133 3134 events_.fireEvent( 3135 new SourceFileSavedEvent(getId(), 3136 saveItem.getPath())); 3137 }); 3138 3139 3140 } 3141 3142 }; 3143 3144 // if we are switching from an R file type 3145 // to a non-R file type then confirm 3146 if (fileType_.isR() && !fileType.isR()) 3147 { 3148 globalDisplay_.showYesNoMessage( 3149 MessageDialog.WARNING, 3150 "Confirm Change File Type", 3151 "This file was created as an R script however " + 3152 "the file extension you specified will change " + 3153 "it into another file type that will no longer " + 3154 "open as an R script.\n\n" + 3155 "Are you sure you want to change the type of " + 3156 "the file so that it is no longer an R script?", 3157 new Operation() { 3158 3159 @Override 3160 public void execute() 3161 { 3162 saveCommand.execute(); 3163 } 3164 }, 3165 false); 3166 } 3167 else 3168 { 3169 saveCommand.execute(); 3170 } 3171 } 3172 catch (Exception e) 3173 { 3174 indicator.onError(e.toString()); 3175 return; 3176 } 3177 3178 indicator.onCompleted(); 3179 } 3180 }); 3181 } 3182 3183 fixupCodeBeforeSaving(Command ready)3184 private void fixupCodeBeforeSaving(Command ready) 3185 { 3186 int lineCount = docDisplay_.getRowCount(); 3187 if (lineCount < 1) 3188 { 3189 ready.execute(); 3190 return; 3191 } 3192 3193 if (docDisplay_.hasActiveCollabSession()) 3194 { 3195 // mutating the code (especially as below where the entire document 3196 // contents are changed) during a save operation inside a collaborative 3197 // editing session would require some nuanced orchestration so for now 3198 // these preferences don't apply to shared editing sessions 3199 // note that visual editing is currently disabled for collab sessions 3200 // so none of the visual editing code below would apply 3201 ready.execute(); 3202 return; 3203 } 3204 3205 3206 // apply visual mode fixups then continue w/ standard fixups 3207 applyVisualModeFixups(() -> { 3208 3209 boolean stripTrailingWhitespace = (projConfig_ == null) 3210 ? prefs_.stripTrailingWhitespace().getValue() 3211 : projConfig_.stripTrailingWhitespace(); 3212 3213 // override preference for certain files 3214 boolean dontStripWhitespace = 3215 fileType_.isMarkdown() || 3216 fileType_.isPython() || 3217 name_.getValue().equals("DESCRIPTION"); 3218 3219 if (dontStripWhitespace) 3220 stripTrailingWhitespace = false; 3221 3222 if (stripTrailingWhitespace) 3223 { 3224 String code = docDisplay_.getCode(); 3225 Pattern pattern = Pattern.create("[ \t]+$"); 3226 String strippedCode = pattern.replaceAll(code, ""); 3227 if (!strippedCode.equals(code)) 3228 { 3229 // Calling 'setCode' can remove folds in the document; cache the folds 3230 // and reapply them after document mutation. 3231 JsArray<AceFold> folds = docDisplay_.getFolds(); 3232 docDisplay_.setCode(strippedCode, true); 3233 for (AceFold fold : JsUtil.asIterable(folds)) 3234 docDisplay_.addFold(fold.getRange()); 3235 } 3236 } 3237 3238 boolean autoAppendNewline = (projConfig_ == null) 3239 ? prefs_.autoAppendNewline().getValue() 3240 : projConfig_.ensureTrailingNewline(); 3241 3242 // auto-append newlines for commonly-used R startup files 3243 String path = StringUtil.notNull(docUpdateSentinel_.getPath()); 3244 boolean isStartupFile = 3245 path.endsWith("/.Rprofile") || 3246 path.endsWith("/.Rprofile.site") || 3247 path.endsWith("/.Renviron") || 3248 path.endsWith("/.Renviron.site"); 3249 3250 if (autoAppendNewline || isStartupFile || fileType_.isPython()) 3251 { 3252 String lastLine = docDisplay_.getLine(lineCount - 1); 3253 if (lastLine.length() != 0) 3254 docDisplay_.insertCode(docDisplay_.getEnd().getEnd(), "\n"); 3255 } 3256 3257 // callback 3258 ready.execute(); 3259 }); 3260 } 3261 applyVisualModeFixups(Command onComplete)3262 private void applyVisualModeFixups(Command onComplete) 3263 { 3264 // only do this for markdown files 3265 if (fileType_.isMarkdown()) 3266 { 3267 // check canonical pref 3268 boolean canonical = prefs_.visualMarkdownEditingCanonical().getValue(); 3269 3270 // if we are cannonical but the global value isn't canonical then make sure this 3271 // file is in the current project 3272 if (canonical && !prefs_.visualMarkdownEditingCanonical().getGlobalValue()) 3273 { 3274 canonical = VisualModeUtil.isDocInProject(workbenchContext_, docUpdateSentinel_); 3275 } 3276 3277 // check for a file based canonical setting 3278 String yaml = YamlFrontMatter.getFrontMatter(docDisplay_); 3279 String yamlCanonical = RmdEditorOptions.getMarkdownOption(yaml, "canonical"); 3280 if (!yamlCanonical.isEmpty()) 3281 canonical = YamlTree.isTrue(yamlCanonical); 3282 3283 // if visual mode is active then we need to grab its edits before proceeding 3284 if (visualMode_.isActivated()) 3285 { 3286 visualMode_.syncToEditor(SyncType.SyncTypeNormal, onComplete); 3287 } 3288 3289 // if visual mode is not active and we are doing canonical saves 3290 // then we need to apply any changes implied by canonical transformation 3291 // of our source 3292 else if (canonical && visualMode_.canWriteCanonical()) 3293 { 3294 String code = docDisplay_.getCode(); 3295 visualMode_.getCanonicalChanges(code, (changes) -> { 3296 // null changes means an error occurred (user has already been shown an alert) 3297 if (changes != null) 3298 { 3299 if (changes.changes != null) 3300 docDisplay_.applyChanges(changes.changes, true); 3301 else if (changes.code != null) 3302 docDisplay_.setCode(changes.code, true); 3303 } 3304 // need to continue in order to not permanetly break save 3305 // (user has seen an error message so will still likely report) 3306 onComplete.execute(); 3307 }); 3308 } 3309 3310 // otherwise nothing to do 3311 else 3312 { 3313 onComplete.execute(); 3314 } 3315 } 3316 // not a markdown file 3317 else 3318 { 3319 onComplete.execute(); 3320 } 3321 } 3322 3323 // When the editor loses focus, perform an autosave if enabled, the 3324 // buffer is dirty, and we have a file to save to maybeAutoSaveOnBlur()3325 private void maybeAutoSaveOnBlur() { 3326 if (prefs_.autoSaveOnBlur().getValue() && 3327 dirtyState_.getValue() && 3328 getPath() != null && 3329 !docDisplay_.hasActiveCollabSession()) 3330 { 3331 try 3332 { 3333 save(); 3334 } 3335 catch(Exception e) 3336 { 3337 // Autosave exceptions are logged rather than displayed 3338 Debug.logException(e); 3339 } 3340 } 3341 } 3342 3343 getSaveFileDefaultDir()3344 private FileSystemItem getSaveFileDefaultDir() 3345 { 3346 FileSystemItem fsi = null; 3347 SessionInfo si = session_.getSessionInfo(); 3348 3349 if (si.getBuildToolsType() == SessionInfo.BUILD_TOOLS_PACKAGE) 3350 { 3351 FileSystemItem pkg = FileSystemItem.createDir(si.getBuildTargetDir()); 3352 3353 if (fileType_.isR()) 3354 { 3355 fsi = FileSystemItem.createDir(pkg.completePath("R")); 3356 } 3357 else if (fileType_.isC() && si.getHasPackageSrcDir()) 3358 { 3359 fsi = FileSystemItem.createDir(pkg.completePath("src")); 3360 } 3361 else if (fileType_.isRd()) 3362 { 3363 fsi = FileSystemItem.createDir(pkg.completePath("man")); 3364 } 3365 else if ((fileType_.isRnw() || fileType_.isRmd()) && 3366 si.getHasPackageVignetteDir()) 3367 { 3368 fsi = FileSystemItem.createDir(pkg.completePath("vignettes")); 3369 } 3370 } 3371 3372 if (fsi == null) 3373 fsi = workbenchContext_.getDefaultFileDialogDir(); 3374 3375 return fsi; 3376 } 3377 onDismiss(int dismissType)3378 public void onDismiss(int dismissType) 3379 { 3380 docUpdateSentinel_.stop(); 3381 3382 if (spelling_ != null) 3383 spelling_.onDismiss(); 3384 3385 if (visualMode_ != null) 3386 visualMode_.onDismiss(); 3387 3388 while (releaseOnDismiss_.size() > 0) 3389 releaseOnDismiss_.remove(0).removeHandler(); 3390 3391 docDisplay_.endCollabSession(); 3392 3393 codeExecution_.detachLastExecuted(); 3394 3395 if (notebook_ != null) 3396 notebook_.onDismiss(); 3397 3398 if (inlinePreviewer_ != null) 3399 inlinePreviewer_.onDismiss(); 3400 } 3401 dirtyState()3402 public ReadOnlyValue<Boolean> dirtyState() 3403 { 3404 return dirtyState_; 3405 } 3406 3407 @Override isSaveCommandActive()3408 public boolean isSaveCommandActive() 3409 { 3410 return 3411 // force active? 3412 forceSaveCommandActive_ || 3413 3414 // standard check of dirty state 3415 (dirtyState().getValue() == true) || 3416 3417 // empty untitled document (allow for immediate save) 3418 ((getPath() == null) && docDisplay_.getCode().isEmpty()) || 3419 3420 // source on save is active 3421 (isSourceOnSaveEnabled() && docUpdateSentinel_.sourceOnSave()); 3422 } 3423 3424 3425 @Override forceSaveCommandActive()3426 public void forceSaveCommandActive() 3427 { 3428 forceSaveCommandActive_ = true; 3429 } 3430 asWidget()3431 public Widget asWidget() 3432 { 3433 return (Widget) view_; 3434 } 3435 getId()3436 public String getId() 3437 { 3438 return id_; 3439 } 3440 3441 @Override adaptToExtendedFileType(String extendedType)3442 public void adaptToExtendedFileType(String extendedType) 3443 { 3444 // extended type can affect publish options; we need to sync here even if the type 3445 // hasn't changed as the path may have changed 3446 syncPublishPath(docUpdateSentinel_.getPath()); 3447 3448 // if autosaves are enabled and the extended type hasn't changed, then 3449 // don't do any further work as adapting to the extended type can cause 3450 // disruptive side effects during autosave (e.g., knocking down 3451 // autocomplete dialogs, resetting vim mode) 3452 if (StringUtil.equals(extendedType, extendedType_) && 3453 prefs_.autoSaveEnabled()) 3454 { 3455 return; 3456 } 3457 3458 3459 view_.adaptToExtendedFileType(extendedType); 3460 if (extendedType.startsWith(SourceDocument.XT_RMARKDOWN_PREFIX) || 3461 extendedType.equals(SourceDocument.XT_QUARTO_DOCUMENT)) 3462 { 3463 updateRmdFormat(); 3464 } 3465 extendedType_ = extendedType; 3466 3467 quartoHelper_.manageCommands(); 3468 } 3469 3470 @Override getExtendedFileType()3471 public String getExtendedFileType() 3472 { 3473 return extendedType_; 3474 } 3475 3476 @Override isShinyPrerenderedDoc()3477 public boolean isShinyPrerenderedDoc() 3478 { 3479 try 3480 { 3481 String yaml = getRmdFrontMatter(); 3482 if (yaml == null) 3483 return false; 3484 return rmarkdownHelper_.isRuntimeShinyPrerendered(yaml); 3485 } 3486 catch(Exception e) 3487 { 3488 Debug.log(e.getMessage()); 3489 return false; 3490 } 3491 } getName()3492 public HasValue<String> getName() 3493 { 3494 return name_; 3495 } 3496 getTitle()3497 public String getTitle() 3498 { 3499 return getName().getValue(); 3500 } 3501 getPath()3502 public String getPath() 3503 { 3504 if (docUpdateSentinel_ == null) 3505 return null; 3506 return docUpdateSentinel_.getPath(); 3507 } 3508 getContext()3509 public String getContext() 3510 { 3511 return null; 3512 } 3513 getIcon()3514 public FileIcon getIcon() 3515 { 3516 return fileType_.getDefaultFileIcon(); 3517 } 3518 getTabTooltip()3519 public String getTabTooltip() 3520 { 3521 return getPath(); 3522 } 3523 3524 @Override getFileType()3525 public FileType getFileType() 3526 { 3527 return fileType_; 3528 } 3529 3530 @Override getTextFileType()3531 public TextFileType getTextFileType() 3532 { 3533 return fileType_; 3534 } 3535 3536 @Handler onToggleDocumentOutline()3537 void onToggleDocumentOutline() 3538 { 3539 view_.toggleDocumentOutline(); 3540 } 3541 3542 @Handler onToggleRmdVisualMode()3543 void onToggleRmdVisualMode() 3544 { 3545 recordCurrentNavigationPosition(); 3546 view_.toggleRmdVisualMode(); 3547 } 3548 3549 @Handler onToggleSoftWrapMode()3550 void onToggleSoftWrapMode() 3551 { 3552 view_.toggleSoftWrapMode(); 3553 } 3554 3555 @Handler onToggleRainbowParens()3556 void onToggleRainbowParens() 3557 { 3558 view_.toggleRainbowParens(); 3559 } 3560 3561 @Handler onEnableProsemirrorDevTools()3562 void onEnableProsemirrorDevTools() 3563 { 3564 visualMode_.activateDevTools(); 3565 } 3566 3567 @Handler onReformatCode()3568 void onReformatCode() 3569 { 3570 withActiveEditor((disp) -> 3571 { 3572 // Only allow if entire selection in R mode for now 3573 if (!DocumentMode.isSelectionInRMode(disp)) 3574 { 3575 showRModeWarning("Reformat Code"); 3576 return; 3577 } 3578 3579 new TextEditingTargetReformatHelper(disp).insertPrettyNewlines(); 3580 }); 3581 } 3582 3583 @Handler onRenameInScope()3584 void onRenameInScope() 3585 { 3586 withActiveEditor((disp) -> 3587 { 3588 renameInScope(disp); 3589 }); 3590 } 3591 renameInScope(DocDisplay display)3592 void renameInScope(DocDisplay display) 3593 { 3594 display.focus(); 3595 3596 // Save folds (we need to remove them temporarily for the rename helper) 3597 final JsArray<AceFold> folds = display.getFolds(); 3598 display.unfoldAll(); 3599 3600 int matches = (new TextEditingTargetRenameHelper(display)).renameInScope(); 3601 if (matches <= 0) 3602 { 3603 if (!display.getSelectionValue().isEmpty()) 3604 { 3605 String message = "No matches for '" + display.getSelectionValue() + "'"; 3606 view_.getStatusBar().showMessage(message, 1000); 3607 } 3608 3609 for (AceFold fold : JsUtil.asIterable(folds)) 3610 display.addFold(fold.getRange()); 3611 return; 3612 } 3613 3614 String message = "Found " + matches; 3615 if (matches == 1) 3616 message += " match"; 3617 else 3618 message += " matches"; 3619 3620 String selectedItem = display.getSelectionValue(); 3621 message += " for " + selectedItem + "."; 3622 3623 display.disableSearchHighlight(); 3624 view_.getStatusBar().showMessage(message, new HideMessageHandler() 3625 { 3626 private boolean onRenameFinished(boolean value) 3627 { 3628 for (AceFold fold : JsUtil.asIterable(folds)) 3629 display.addFold(fold.getRange()); 3630 return value; 3631 } 3632 3633 @Override 3634 public boolean onNativePreviewEvent(NativePreviewEvent preview) 3635 { 3636 int type = preview.getTypeInt(); 3637 if (display.isPopupVisible()) 3638 return false; 3639 3640 // End if the user clicks somewhere 3641 if (type == Event.ONCLICK) 3642 { 3643 display.exitMultiSelectMode(); 3644 display.clearSelection(); 3645 display.enableSearchHighlight(); 3646 return onRenameFinished(true); 3647 } 3648 3649 // Otherwise, handle key events 3650 else if (type == Event.ONKEYDOWN) 3651 { 3652 switch (preview.getNativeEvent().getKeyCode()) 3653 { 3654 case KeyCodes.KEY_ENTER: 3655 preview.cancel(); 3656 case KeyCodes.KEY_UP: 3657 case KeyCodes.KEY_DOWN: 3658 case KeyCodes.KEY_ESCAPE: 3659 display.exitMultiSelectMode(); 3660 display.clearSelection(); 3661 display.enableSearchHighlight(); 3662 return onRenameFinished(true); 3663 } 3664 } 3665 3666 return false; 3667 } 3668 }); 3669 } 3670 3671 @Handler onInsertRoxygenSkeleton()3672 void onInsertRoxygenSkeleton() 3673 { 3674 withActiveEditor((disp) -> 3675 { 3676 new RoxygenHelper(disp, view_).insertRoxygenSkeleton(); 3677 }); 3678 } 3679 3680 @Handler onExpandSelection()3681 void onExpandSelection() 3682 { 3683 withActiveEditor((disp) -> 3684 { 3685 disp.expandSelection(); 3686 }); 3687 } 3688 3689 @Handler onShrinkSelection()3690 void onShrinkSelection() 3691 { 3692 withActiveEditor((disp) -> 3693 { 3694 disp.shrinkSelection(); 3695 }); 3696 } 3697 3698 @Handler onExpandRaggedSelection()3699 void onExpandRaggedSelection() 3700 { 3701 withActiveEditor((disp) -> 3702 { 3703 disp.expandRaggedSelection(); 3704 }); 3705 } 3706 3707 @Handler onShowDiagnosticsActiveDocument()3708 void onShowDiagnosticsActiveDocument() 3709 { 3710 lintManager_.lint(true, true, false); 3711 } 3712 withSavedDoc(Command onsaved)3713 public void withSavedDoc(Command onsaved) 3714 { 3715 docUpdateSentinel_.withSavedDoc(onsaved); 3716 } 3717 withSavedDocNoRetry(Command onsaved)3718 public void withSavedDocNoRetry(Command onsaved) 3719 { 3720 docUpdateSentinel_.withSavedDocNoRetry(onsaved); 3721 } 3722 3723 @Handler onWordCount()3724 void onWordCount() 3725 { 3726 prepareForVisualExecution(() -> 3727 { 3728 int totalWords = 0; 3729 int selectionWords = 0; 3730 3731 Range selectionRange = null; 3732 3733 // A selection in visual mode may span multiple editors and blocks of 3734 // prose, which we can't count here. 3735 if (!isVisualEditorActive()) 3736 { 3737 selectionRange = docDisplay_.getSelectionRange(); 3738 } 3739 3740 TextFileType fileType = docDisplay_.getFileType(); 3741 Iterator<Range> wordIter = docDisplay_.getWords( 3742 fileType.getTokenPredicate(), 3743 docDisplay_.getFileType().getCharPredicate(), 3744 Position.create(0, 0), 3745 null).iterator(); 3746 3747 while (wordIter.hasNext()) 3748 { 3749 Range r = wordIter.next(); 3750 totalWords++; 3751 if (selectionRange != null && selectionRange.intersects(r)) 3752 selectionWords++; 3753 } 3754 3755 String selectedWordsText = selectionWords == 0 ? "" : "\nSelected words: " + selectionWords; 3756 globalDisplay_.showMessage(MessageDisplay.MSG_INFO, 3757 "Word Count", 3758 "Total words: " + totalWords + " " + selectedWordsText); 3759 }); 3760 } 3761 3762 @Handler onCheckSpelling()3763 void onCheckSpelling() 3764 { 3765 if (visualMode_.isActivated()) 3766 { 3767 ensureVisualModeActive(() -> { 3768 visualMode_.checkSpelling(); 3769 }); 3770 } 3771 else 3772 { 3773 ensureTextEditorActive(() -> { 3774 spelling_.checkSpelling(docDisplay_.getSpellingDoc()); 3775 }); 3776 } 3777 3778 3779 } 3780 3781 @Handler onDebugDumpContents()3782 void onDebugDumpContents() 3783 { 3784 view_.debug_dumpContents(); 3785 } 3786 3787 @Handler onDebugImportDump()3788 void onDebugImportDump() 3789 { 3790 view_.debug_importDump(); 3791 } 3792 3793 @Handler onReopenSourceDocWithEncoding()3794 void onReopenSourceDocWithEncoding() 3795 { 3796 final Command action = () -> { 3797 withChooseEncoding( 3798 docUpdateSentinel_.getEncoding(), 3799 (String encoding) -> docUpdateSentinel_.reopenWithEncoding(encoding)); 3800 }; 3801 3802 // NOTE: we previously attempted to save any existing document diffs 3803 // and then re-opened the document with the requested encoding, but 3804 // this is a perilous action to take as if the user has opened a document 3805 // without specifying the correct encoding, the representation of the document 3806 // in the front-end might be corrupt / incorrect and so attempting to save 3807 // a document diff could further corrupt the document! 3808 // 3809 // Since the most common user workflow here should be: 3810 // 3811 // 1. Open a document, 3812 // 2. Discover the document was not opened with the correct encoding, 3813 // 3. Attempt to re-open with a separate encoding 3814 // 3815 // it's most likely that they do not want to persist any changes made to the 3816 // "incorrect" version of the document and instead want to discard any 3817 // changes and re-open the document as it exists on disk. 3818 if (dirtyState_.getValue()) 3819 { 3820 String caption = "Reopen with Encoding"; 3821 3822 String message = 3823 "This document has unsaved changes. These changes will be " + 3824 "discarded when re-opening the document.\n\n" + 3825 "Would you like to proceed?"; 3826 3827 globalDisplay_.showYesNoMessage( 3828 GlobalDisplay.MSG_WARNING, 3829 caption, 3830 message, 3831 true, 3832 () -> action.execute(), 3833 () -> {}, 3834 () -> {}, 3835 "Reopen Document", 3836 "Cancel", 3837 true); 3838 } 3839 else 3840 { 3841 action.execute(); 3842 } 3843 } 3844 3845 @Handler onSaveSourceDoc()3846 void onSaveSourceDoc() 3847 { 3848 if (isSaving_) 3849 return; 3850 3851 saveThenExecute(null, true, postSaveCommand()); 3852 } 3853 3854 @Handler onSaveSourceDocAs()3855 void onSaveSourceDocAs() 3856 { 3857 saveNewFile(docUpdateSentinel_.getPath(), 3858 null, 3859 postSaveCommand()); 3860 } 3861 3862 @Handler onRenameSourceDoc()3863 void onRenameSourceDoc() 3864 { 3865 events_.fireEvent(new RenameSourceFileEvent(docUpdateSentinel_.getPath())); 3866 } 3867 3868 @Handler onCopySourceDocPath()3869 void onCopySourceDocPath() 3870 { 3871 events_.fireEvent(new CopySourcePathEvent(docUpdateSentinel_.getPath())); 3872 } 3873 3874 @Handler onSaveSourceDocWithEncoding()3875 void onSaveSourceDocWithEncoding() 3876 { 3877 withChooseEncoding( 3878 StringUtil.firstNotNullOrEmpty(new String[] { 3879 docUpdateSentinel_.getEncoding(), 3880 prefs_.defaultEncoding().getValue(), 3881 session_.getSessionInfo().getSystemEncoding() 3882 }), 3883 new CommandWithArg<String>() 3884 { 3885 public void execute(String encoding) 3886 { 3887 saveThenExecute(encoding, true, postSaveCommand()); 3888 } 3889 }); 3890 } 3891 3892 @Handler onPrintSourceDoc()3893 void onPrintSourceDoc() 3894 { 3895 Scheduler.get().scheduleDeferred(new ScheduledCommand() 3896 { 3897 public void execute() 3898 { 3899 docDisplay_.print(); 3900 } 3901 }); 3902 } 3903 3904 @Handler onVcsFileDiff()3905 void onVcsFileDiff() 3906 { 3907 Command showDiffCommand = new Command() { 3908 @Override 3909 public void execute() 3910 { 3911 events_.fireEvent(new ShowVcsDiffEvent( 3912 FileSystemItem.createFile(docUpdateSentinel_.getPath()))); 3913 } 3914 }; 3915 3916 if (dirtyState_.getValue()) 3917 saveWithPrompt(showDiffCommand, null); 3918 else 3919 showDiffCommand.execute(); 3920 } 3921 3922 @Handler onVcsFileLog()3923 void onVcsFileLog() 3924 { 3925 events_.fireEvent(new ShowVcsHistoryEvent( 3926 FileSystemItem.createFile(docUpdateSentinel_.getPath()))); 3927 } 3928 3929 @Handler onVcsFileRevert()3930 void onVcsFileRevert() 3931 { 3932 events_.fireEvent(new VcsRevertFileEvent( 3933 FileSystemItem.createFile(docUpdateSentinel_.getPath()))); 3934 } 3935 3936 @Handler onVcsViewOnGitHub()3937 void onVcsViewOnGitHub() 3938 { 3939 fireVcsViewOnGithubEvent(GitHubViewRequest.VCS_VIEW); 3940 } 3941 3942 @Handler onVcsBlameOnGitHub()3943 void onVcsBlameOnGitHub() 3944 { 3945 fireVcsViewOnGithubEvent(GitHubViewRequest.VCS_BLAME); 3946 } 3947 fireVcsViewOnGithubEvent(int type)3948 private void fireVcsViewOnGithubEvent(int type) 3949 { 3950 FileSystemItem file = 3951 FileSystemItem.createFile(docUpdateSentinel_.getPath()); 3952 3953 if (docDisplay_.getSelectionValue().length() > 0) 3954 { 3955 int start = docDisplay_.getSelectionStart().getRow() + 1; 3956 int end = docDisplay_.getSelectionEnd().getRow() + 1; 3957 events_.fireEvent(new VcsViewOnGitHubEvent( 3958 new GitHubViewRequest(file, type, start, end))); 3959 } 3960 else 3961 { 3962 events_.fireEvent(new VcsViewOnGitHubEvent( 3963 new GitHubViewRequest(file, type))); 3964 } 3965 } 3966 3967 @Handler onExtractLocalVariable()3968 void onExtractLocalVariable() 3969 { 3970 withActiveEditor((disp) -> 3971 { 3972 extractLocalVariable(disp); 3973 }); 3974 } 3975 extractLocalVariable(DocDisplay display)3976 void extractLocalVariable(DocDisplay display) 3977 { 3978 if (!isCursorInRMode(display)) 3979 { 3980 showRModeWarning("Extract Variable"); 3981 return; 3982 } 3983 3984 display.focus(); 3985 3986 String initialSelection = display.getSelectionValue(); 3987 final String refactoringName = "Extract local variable"; 3988 final String pleaseSelectCodeMessage = "Please select the code to " + 3989 "extract into a variable."; 3990 if (checkSelectionAndAlert(refactoringName, 3991 pleaseSelectCodeMessage, 3992 initialSelection)) return; 3993 3994 display.fitSelectionToLines(false); 3995 3996 final String code = display.getSelectionValue(); 3997 if (checkSelectionAndAlert(refactoringName, 3998 pleaseSelectCodeMessage, 3999 code)) 4000 return; 4001 4002 // get the first line of the selection and calculate it's indentation 4003 String firstLine = display.getLine( 4004 display.getSelectionStart().getRow()); 4005 final String indentation = extractIndentation(firstLine); 4006 4007 // used to parse the code 4008 server_.detectFreeVars(code, 4009 new RefactorServerRequestCallback(refactoringName) 4010 { 4011 @Override 4012 void doExtract(JsArrayString response) 4013 { 4014 globalDisplay_.promptForText( 4015 refactoringName, 4016 "Variable Name", 4017 "", 4018 new OperationWithInput<String>() 4019 { 4020 public void execute(String input) 4021 { 4022 final String extractedCode = indentation 4023 + input.trim() 4024 + " <- " 4025 + code 4026 + "\n"; 4027 InputEditorPosition insertPosition = display 4028 .getSelection() 4029 .extendToLineStart() 4030 .getStart(); 4031 display.replaceSelection( 4032 input.trim()); 4033 display.insertCode( 4034 insertPosition, 4035 extractedCode); 4036 } 4037 } 4038 ); 4039 } 4040 } 4041 ); 4042 } 4043 showRModeWarning(String command)4044 private void showRModeWarning(String command) 4045 { 4046 globalDisplay_.showMessage(MessageDisplay.MSG_WARNING, 4047 "Command Not Available", 4048 "The "+ command + " command is " + 4049 "only valid for R code chunks."); 4050 } 4051 4052 4053 @Handler onExtractFunction()4054 void onExtractFunction() 4055 { 4056 withActiveEditor((disp) -> 4057 { 4058 extractActiveFunction(disp); 4059 }); 4060 } 4061 extractActiveFunction(DocDisplay display)4062 void extractActiveFunction(DocDisplay display) 4063 { 4064 if (!isCursorInRMode(display)) 4065 { 4066 showRModeWarning("Extract Function"); 4067 return; 4068 } 4069 4070 display.focus(); 4071 4072 String initialSelection = display.getSelectionValue(); 4073 final String refactoringName = "Extract Function"; 4074 final String pleaseSelectCodeMessage = "Please select the code to " + 4075 "extract into a function."; 4076 if (checkSelectionAndAlert(refactoringName, 4077 pleaseSelectCodeMessage, 4078 initialSelection)) return; 4079 4080 display.fitSelectionToLines(false); 4081 4082 final String code = display.getSelectionValue(); 4083 if (checkSelectionAndAlert(refactoringName, 4084 pleaseSelectCodeMessage, 4085 code)) return; 4086 4087 final String indentation = extractIndentation(code); 4088 server_.detectFreeVars(code, 4089 new RefactorServerRequestCallback(refactoringName) 4090 { 4091 @Override 4092 void doExtract(final JsArrayString response) 4093 { 4094 globalDisplay_.promptForText( 4095 refactoringName, 4096 "Function Name", 4097 "", 4098 new OperationWithInput<String>() 4099 { 4100 public void execute(String input) 4101 { 4102 String prefix; 4103 if (display.getSelectionOffset(true) == 0) 4104 prefix = ""; 4105 else prefix = "\n"; 4106 String args = response != null ? response.join(", ") 4107 : ""; 4108 display.replaceSelection( 4109 prefix 4110 + indentation 4111 + input.trim() 4112 + " <- " 4113 + "function(" + args + ") {\n" 4114 + StringUtil.indent(code, " ") 4115 + "\n" 4116 + indentation 4117 + "}"); 4118 } 4119 } 4120 ); 4121 } 4122 } 4123 ); 4124 } 4125 4126 isSourceOnSaveEnabled()4127 private boolean isSourceOnSaveEnabled() 4128 { 4129 return fileType_.canSourceOnSave() || StringUtil.equals(extendedType_, SourceDocument.XT_QUARTO_DOCUMENT); 4130 } 4131 checkSelectionAndAlert(String refactoringName, String pleaseSelectCodeMessage, String selection)4132 private boolean checkSelectionAndAlert(String refactoringName, 4133 String pleaseSelectCodeMessage, 4134 String selection) 4135 { 4136 if (isSelectionValueEmpty(selection)) 4137 { 4138 globalDisplay_.showErrorMessage(refactoringName, 4139 pleaseSelectCodeMessage); 4140 return true; 4141 } 4142 return false; 4143 } 4144 extractIndentation(String code)4145 private String extractIndentation(String code) 4146 { 4147 Pattern leadingWhitespace = Pattern.create("^(\\s*)"); 4148 Match match = leadingWhitespace.match(code, 0); 4149 return match == null ? "" : match.getGroup(1); 4150 } 4151 isSelectionValueEmpty(String selection)4152 private boolean isSelectionValueEmpty(String selection) 4153 { 4154 return selection == null || selection.trim().length() == 0; 4155 } 4156 4157 @Handler onCommentUncomment()4158 void onCommentUncomment() 4159 { 4160 withActiveEditor((disp) -> 4161 { 4162 commentUncomment(disp); 4163 }); 4164 } 4165 commentUncomment(DocDisplay display)4166 void commentUncomment(DocDisplay display) 4167 { 4168 if (isCursorInTexMode(display)) 4169 doCommentUncomment(display, "%", null); 4170 else if (isCursorInRMode(display) || isCursorInYamlMode(display)) 4171 doCommentUncomment(display, "#", null); 4172 else if (fileType_.isCpp() || fileType_.isStan() || fileType_.isC()) 4173 doCommentUncomment(display, "//", null); 4174 else if (fileType_.isPlainMarkdown()) 4175 doCommentUncomment(display, "<!--", "-->"); 4176 else if (DocumentMode.isSelectionInMarkdownMode(display)) 4177 doCommentUncomment(display, "<!--", "-->"); 4178 else if (DocumentMode.isSelectionInPythonMode(display)) 4179 doCommentUncomment(display, "#", null); 4180 } 4181 4182 /** 4183 * Push the current contents and state of the text editor into the local 4184 * copy of the source database 4185 */ syncLocalSourceDb()4186 public void syncLocalSourceDb() 4187 { 4188 SourceWindowManager manager = 4189 RStudioGinjector.INSTANCE.getSourceWindowManager(); 4190 JsArray<SourceDocument> docs = manager.getSourceDocs(); 4191 for (int i = 0; i < docs.length(); i++) 4192 { 4193 if (docs.get(i).getId() == getId()) 4194 { 4195 docs.get(i).getNotebookDoc().setChunkDefs( 4196 docDisplay_.getChunkDefs()); 4197 docs.get(i).setContents(docDisplay_.getCode()); 4198 docs.get(i).setDirty(dirtyState_.getValue()); 4199 break; 4200 } 4201 } 4202 } 4203 4204 @Handler onPopoutDoc()4205 void onPopoutDoc() 4206 { 4207 if (docUpdateSentinel_ != null) 4208 { 4209 // ensure doc is synchronized with source database before popping it 4210 // out 4211 docUpdateSentinel_.withSavedDoc(new Command() 4212 { 4213 @Override 4214 public void execute() 4215 { 4216 // push the new doc state into the local source database 4217 syncLocalSourceDb(); 4218 4219 // fire popout event (this triggers a close in the current window 4220 // and the creation of a new window with the doc) 4221 events_.fireEvent(new PopoutDocEvent(getId(), 4222 currentPosition(), null)); 4223 } 4224 }); 4225 } 4226 } 4227 4228 4229 @Handler onReturnDocToMain()4230 void onReturnDocToMain() 4231 { 4232 // ensure doc is synchronized with source database before returning it 4233 if (!SourceWindowManager.isMainSourceWindow() && 4234 docUpdateSentinel_ != null) 4235 { 4236 docUpdateSentinel_.withSavedDoc(new Command() 4237 { 4238 @Override 4239 public void execute() 4240 { 4241 events_.fireEventToMainWindow(new DocWindowChangedEvent( 4242 getId(), SourceWindowManager.getSourceWindowId(), "", 4243 DocTabDragParams.create(getId(), currentPosition()), 4244 docUpdateSentinel_.getDoc().getCollabParams(), 0, -1)); 4245 } 4246 }); 4247 } 4248 } 4249 4250 @Handler onNotebookCollapseAllOutput()4251 public void onNotebookCollapseAllOutput() 4252 { 4253 if (notebook_ != null) 4254 notebook_.onNotebookCollapseAllOutput(); 4255 } 4256 4257 @Handler onNotebookExpandAllOutput()4258 public void onNotebookExpandAllOutput() 4259 { 4260 if (notebook_ != null) 4261 notebook_.onNotebookExpandAllOutput(); 4262 } 4263 4264 @Handler onNotebookClearOutput()4265 public void onNotebookClearOutput() 4266 { 4267 if (notebook_ != null) 4268 notebook_.onNotebookClearOutput(); 4269 } 4270 4271 @Handler onNotebookClearAllOutput()4272 public void onNotebookClearAllOutput() 4273 { 4274 if (notebook_ != null) 4275 notebook_.onNotebookClearAllOutput(); 4276 } 4277 4278 @Handler onNotebookToggleExpansion()4279 public void onNotebookToggleExpansion() 4280 { 4281 if (notebook_ != null) 4282 notebook_.onNotebookToggleExpansion(); 4283 } 4284 4285 @Handler onRestartRRunAllChunks()4286 public void onRestartRRunAllChunks() 4287 { 4288 if (notebook_ != null) 4289 notebook_.onRestartRRunAllChunks(); 4290 } 4291 4292 @Handler onRestartRClearOutput()4293 public void onRestartRClearOutput() 4294 { 4295 if (notebook_ != null) 4296 notebook_.onRestartRClearOutput(); 4297 } 4298 4299 @SuppressWarnings("deprecation") // GWT emulation only provides isSpace doCommentUncomment(DocDisplay display, String commentStart, String commentEnd)4300 private void doCommentUncomment(DocDisplay display, 4301 String commentStart, 4302 String commentEnd) 4303 { 4304 Range initialRange = display.getSelectionRange(); 4305 4306 int rowStart = initialRange.getStart().getRow(); 4307 int rowEnd = initialRange.getEnd().getRow(); 4308 4309 boolean isSingleLineAction = rowStart == rowEnd; 4310 boolean commentWhitespace = commentEnd == null; 4311 4312 // Also figure out if we're commenting an Roxygen block. 4313 boolean looksLikeRoxygen = false; 4314 4315 // Skip commenting the last line if the selection is 4316 // multiline and ends on the first column of the end row. 4317 boolean dontCommentLastLine = false; 4318 if (rowStart != rowEnd && initialRange.getEnd().getColumn() == 0) 4319 dontCommentLastLine = true; 4320 4321 Range expanded = Range.create( 4322 rowStart, 4323 0, 4324 rowEnd, 4325 dontCommentLastLine ? 0 : display.getLine(rowEnd).length()); 4326 display.setSelectionRange(expanded); 4327 4328 String[] lines = JsUtil.toStringArray( 4329 display.getLines(rowStart, rowEnd - (dontCommentLastLine ? 1 : 0))); 4330 4331 String commonPrefix = StringUtil.getCommonPrefix( 4332 lines, 4333 true, 4334 true); 4335 4336 String commonIndent = StringUtil.getIndent(commonPrefix); 4337 4338 // First, figure out whether we're commenting or uncommenting. 4339 // If we discover any line that doesn't start with the comment sequence, 4340 // then we'll comment the whole selection. 4341 4342 // ignore empty lines at start, end of selection when detecting comments 4343 // https://github.com/rstudio/rstudio/issues/4163 4344 4345 int start = 0; 4346 for (int i = 0; i < lines.length; i++) 4347 { 4348 if (lines[i].trim().isEmpty()) 4349 continue; 4350 4351 start = i; 4352 break; 4353 } 4354 4355 int end = lines.length; 4356 for (int i = lines.length; i > 0; i--) 4357 { 4358 if (lines[i - 1].trim().isEmpty()) 4359 continue; 4360 4361 end = i; 4362 break; 4363 } 4364 4365 boolean isCommentAction = false; 4366 for (int i = start; i < end; i++) 4367 { 4368 String line = lines[i]; 4369 String trimmed = line.trim(); 4370 4371 // Ignore lines that are just whitespace. 4372 if (!commentWhitespace && trimmed.isEmpty()) 4373 continue; 4374 4375 if (!isCommentAction) 4376 { 4377 if (!trimmed.startsWith(commentStart)) 4378 isCommentAction = true; 4379 } 4380 4381 if (display.getFileType().isR()) 4382 { 4383 if (!looksLikeRoxygen) 4384 { 4385 if (trimmed.startsWith("@")) 4386 looksLikeRoxygen = true; 4387 else if (trimmed.startsWith("#'")) 4388 looksLikeRoxygen = true; 4389 } 4390 } 4391 } 4392 4393 if (looksLikeRoxygen) 4394 commentStart += "'"; 4395 4396 // Now, construct a new, commented selection to replace with. 4397 StringBuilder builder = new StringBuilder(); 4398 if (isCommentAction) 4399 { 4400 for (String line : lines) 4401 { 4402 String trimmed = line.trim(); 4403 4404 if (!commentWhitespace && trimmed.isEmpty()) 4405 { 4406 builder.append("\n"); 4407 continue; 4408 } 4409 4410 builder.append(commonIndent); 4411 builder.append(commentStart); 4412 builder.append(" "); 4413 builder.append(line.substring(commonIndent.length())); 4414 if (commentEnd != null) 4415 { 4416 builder.append(" "); 4417 builder.append(commentEnd); 4418 } 4419 4420 builder.append("\n"); 4421 } 4422 } 4423 else 4424 { 4425 for (String line : lines) 4426 { 4427 String trimmed = line.trim(); 4428 4429 if (trimmed.isEmpty()) 4430 { 4431 builder.append("\n"); 4432 continue; 4433 } 4434 4435 boolean isCommentedLine = true; 4436 int commentStartIdx = line.indexOf(commentStart); 4437 if (commentStartIdx == -1) 4438 isCommentedLine = false; 4439 4440 int commentEndIdx = line.length(); 4441 if (commentEnd != null) 4442 { 4443 commentEndIdx = line.lastIndexOf(commentEnd); 4444 if (commentEndIdx == -1) 4445 isCommentedLine = false; 4446 } 4447 4448 if (!isCommentedLine) 4449 { 4450 builder.append(line); 4451 continue; 4452 } 4453 4454 // We want to strip out the leading comment prefix, 4455 // but preserve the indent. 4456 int startIdx = commentStartIdx + commentStart.length(); 4457 if (Character.isSpace(StringUtil.charAt(line, startIdx))) 4458 startIdx++; 4459 4460 int endIdx = commentEndIdx; 4461 String afterComment = line.substring(startIdx, endIdx); 4462 builder.append(StringUtil.trimRight(commonIndent + afterComment)); 4463 4464 builder.append("\n"); 4465 } 4466 } 4467 4468 String newSelection = dontCommentLastLine ? 4469 builder.toString() : 4470 builder.substring(0, builder.length() - 1); 4471 4472 display.replaceSelection(newSelection); 4473 4474 // Nudge the selection to match the commented action. 4475 if (isSingleLineAction) 4476 { 4477 int diff = newSelection.length() - lines[0].length(); 4478 if (commentEnd != null) 4479 diff = diff < 0 ? 4480 diff + commentEnd.length() + 1 : 4481 diff - commentEnd.length() - 1; 4482 4483 int colStart = initialRange.getStart().getColumn(); 4484 int colEnd = initialRange.getEnd().getColumn(); 4485 Range newRange = Range.create( 4486 rowStart, 4487 colStart + diff, 4488 rowStart, 4489 colEnd + diff); 4490 display.setSelectionRange(newRange); 4491 } 4492 } 4493 4494 @Handler 4495 void onReflowComment() 4496 { 4497 withActiveEditor((disp) -> 4498 { 4499 reflowComment(disp); 4500 }); 4501 } 4502 reflowComment(DocDisplay display)4503 void reflowComment(DocDisplay display) 4504 { 4505 if (DocumentMode.isSelectionInRMode(display) || 4506 DocumentMode.isSelectionInPythonMode(display)) 4507 { 4508 doReflowComment(display, "(#)"); 4509 } 4510 else if (DocumentMode.isSelectionInCppMode(display)) 4511 { 4512 String currentLine = display.getLine( 4513 display.getCursorPosition().getRow()); 4514 if (currentLine.startsWith(" *")) 4515 doReflowComment(display, "( \\*[^/])", false); 4516 else 4517 doReflowComment(display, "(//)"); 4518 } 4519 else if (DocumentMode.isSelectionInTexMode(display)) 4520 doReflowComment(display, "(%)"); 4521 else if (DocumentMode.isSelectionInMarkdownMode(display)) 4522 doReflowComment(display, "()"); 4523 else if (display.getFileType().isText()) 4524 doReflowComment(display, "()"); 4525 } 4526 reflowText()4527 public void reflowText() 4528 { 4529 if (docDisplay_.getSelectionValue().isEmpty()) 4530 docDisplay_.setSelectionRange( 4531 Range.fromPoints( 4532 Position.create(docDisplay_.getCursorPosition().getRow(), 0), 4533 Position.create(docDisplay_.getCursorPosition().getRow(), 4534 docDisplay_.getCurrentLine().length()))); 4535 4536 onReflowComment(); 4537 docDisplay_.setCursorPosition( 4538 Position.create( 4539 docDisplay_.getSelectionEnd().getRow(), 4540 0)); 4541 } 4542 showHelpAtCursor()4543 public void showHelpAtCursor() 4544 { 4545 docDisplay_.goToHelp(); 4546 } 4547 4548 @Handler onDebugBreakpoint()4549 void onDebugBreakpoint() 4550 { 4551 docDisplay_.toggleBreakpointAtCursor(); 4552 } 4553 4554 @Handler onRsconnectDeploy()4555 void onRsconnectDeploy() 4556 { 4557 view_.invokePublish(); 4558 } 4559 4560 @Handler onRsconnectConfigure()4561 void onRsconnectConfigure() 4562 { 4563 events_.fireEvent(RSConnectActionEvent.ConfigureAppEvent( 4564 docUpdateSentinel_.getPath())); 4565 } 4566 4567 @Handler onEditRmdFormatOptions()4568 void onEditRmdFormatOptions() 4569 { 4570 rmarkdownHelper_.withRMarkdownPackage( 4571 "Editing R Markdown options", 4572 false, 4573 new CommandWithArg<RMarkdownContext>() { 4574 4575 @Override 4576 public void execute(RMarkdownContext arg) 4577 { 4578 showFrontMatterEditor(); 4579 } 4580 }); 4581 } 4582 showFrontMatterEditor()4583 private void showFrontMatterEditor() 4584 { 4585 final String yaml = getRmdFrontMatter(); 4586 if (yaml == null) 4587 { 4588 globalDisplay_.showErrorMessage("Edit Format Failed", 4589 "Can't find the YAML front matter for this document. Make " + 4590 "sure the front matter is enclosed by lines containing only " + 4591 "three dashes: ---."); 4592 return; 4593 } 4594 rmarkdownHelper_.convertFromYaml(yaml, new CommandWithArg<RmdYamlData>() 4595 { 4596 @Override 4597 public void execute(RmdYamlData arg) 4598 { 4599 String errCaption = "Edit Format Failed"; 4600 String errMsg = 4601 "The YAML front matter in this document could not be " + 4602 "successfully parsed. This parse error needs to be " + 4603 "resolved before format options can be edited."; 4604 4605 if (arg == null) 4606 { 4607 globalDisplay_.showErrorMessage(errCaption, errMsg); 4608 } 4609 else if (!arg.parseSucceeded()) 4610 { 4611 // try to find where the YAML segment begins in the document 4612 // so we can show an adjusted line number for the error 4613 int numLines = docDisplay_.getRowCount(); 4614 int offsetLine = 0; 4615 String separator = RmdFrontMatter.FRONTMATTER_SEPARATOR.trim(); 4616 for (int i = 0; i < numLines; i++) 4617 { 4618 if (docDisplay_.getLine(i).equals(separator)) 4619 { 4620 offsetLine = i + 1; 4621 break; 4622 } 4623 } 4624 globalDisplay_.showErrorMessage(errCaption, 4625 errMsg + "\n\n" + arg.getOffsetParseError(offsetLine)); 4626 } 4627 else 4628 { 4629 showFrontMatterEditorDialog(yaml, arg); 4630 } 4631 } 4632 }); 4633 } 4634 showFrontMatterEditorDialog(String yaml, RmdYamlData data)4635 private void showFrontMatterEditorDialog(String yaml, RmdYamlData data) 4636 { 4637 RmdSelectedTemplate selTemplate = 4638 rmarkdownHelper_.getTemplateFormat(yaml); 4639 if (selTemplate == null) 4640 { 4641 // we don't expect this to happen since we disable the dialog 4642 // entry point when we can't find an associated template 4643 globalDisplay_.showErrorMessage("Edit Format Failed", 4644 "Couldn't determine the format options from the YAML front " + 4645 "matter. Make sure the YAML defines a supported output " + 4646 "format in its 'output' field."); 4647 return; 4648 } 4649 RmdTemplateOptionsDialog dialog = 4650 new RmdTemplateOptionsDialog(selTemplate.template, 4651 selTemplate.format, 4652 data.getFrontMatter(), 4653 getPath() == null ? null : FileSystemItem.createFile(getPath()), 4654 selTemplate.isShiny, 4655 new OperationWithInput<RmdTemplateOptionsDialog.Result>() 4656 { 4657 @Override 4658 public void execute(RmdTemplateOptionsDialog.Result in) 4659 { 4660 // when the dialog is completed successfully, apply the new 4661 // front matter 4662 applyRmdFormatOptions(in.format, in.outputOptions); 4663 } 4664 }, 4665 new Operation() 4666 { 4667 @Override 4668 public void execute() 4669 { 4670 // when the dialog is cancelled, update the view's format list 4671 // (to cancel in-place changes) 4672 updateRmdFormat(); 4673 } 4674 }); 4675 dialog.showModal(); 4676 } 4677 applyRmdFormatOptions(String format, RmdFrontMatterOutputOptions options)4678 private void applyRmdFormatOptions(String format, 4679 RmdFrontMatterOutputOptions options) 4680 { 4681 rmarkdownHelper_.replaceOutputFormatOptions( 4682 getRmdFrontMatter(), format, options, 4683 new OperationWithInput<String>() 4684 { 4685 @Override 4686 public void execute(String input) 4687 { 4688 applyRmdFrontMatter(input); 4689 } 4690 }); 4691 } 4692 getRmdFrontMatter()4693 private String getRmdFrontMatter() 4694 { 4695 if (isVisualEditorActive()) 4696 { 4697 return visualMode_.getYamlFrontMatter(); 4698 } 4699 else 4700 { 4701 return YamlFrontMatter.getFrontMatter(docDisplay_); 4702 } 4703 } 4704 applyRmdFrontMatter(String yaml)4705 private void applyRmdFrontMatter(String yaml) 4706 { 4707 boolean applied = false; 4708 if (isVisualEditorActive()) 4709 { 4710 applied = visualMode_.applyYamlFrontMatter(yaml); 4711 } 4712 else 4713 { 4714 applied = YamlFrontMatter.applyFrontMatter(docDisplay_, yaml); 4715 } 4716 if (applied) 4717 updateRmdFormat(); 4718 } 4719 getSelectedTemplate()4720 private RmdSelectedTemplate getSelectedTemplate() 4721 { 4722 // try to extract the front matter and ascertain the template to which 4723 // it refers 4724 String yaml = getRmdFrontMatter(); 4725 if (yaml == null) 4726 return null; 4727 return rmarkdownHelper_.getTemplateFormat(yaml); 4728 } 4729 getOutputFormats()4730 private List<String> getOutputFormats() 4731 { 4732 String yaml = getRmdFrontMatter(); 4733 if (yaml == null) 4734 return new ArrayList<>(); 4735 List<String> formats = TextEditingTargetRMarkdownHelper.getOutputFormats(yaml); 4736 if (formats == null) 4737 formats = new ArrayList<>(); 4738 return formats; 4739 } 4740 4741 getQuartoOutputFormats()4742 private List<String> getQuartoOutputFormats() 4743 { 4744 String yaml = getRmdFrontMatter(); 4745 if (yaml == null) 4746 return new ArrayList<>(); 4747 List<String> formats = TextEditingTargetRMarkdownHelper.getQuartoOutputFormats(yaml); 4748 if (formats == null) 4749 formats = new ArrayList<>(); 4750 return formats; 4751 } 4752 updateRmdFormat()4753 private void updateRmdFormat() 4754 { 4755 String formatUiName = ""; 4756 List<String> formatList = new ArrayList<>(); 4757 List<String> valueList = new ArrayList<>(); 4758 List<String> extensionList = new ArrayList<>(); 4759 4760 RmdSelectedTemplate selTemplate = getSelectedTemplate(); 4761 4762 // skip all of the format stuff for quarto docs 4763 if (extendedType_.equals(SourceDocument.XT_QUARTO_DOCUMENT)) 4764 { 4765 if (isShinyPrerenderedDoc()) 4766 { 4767 view_.setIsShinyFormat(false, false, true); 4768 } 4769 else 4770 { 4771 view_.setIsNotShinyFormat(); 4772 4773 List<String> formats = getQuartoOutputFormats(); 4774 view_.setQuartoFormatOptions(fileType_, 4775 getCustomKnit().length() == 0, 4776 formats); 4777 } 4778 4779 } 4780 else if (selTemplate != null && selTemplate.isShiny) 4781 { 4782 view_.setIsShinyFormat(selTemplate.format != null, 4783 selTemplate.format != null && 4784 selTemplate.format.endsWith( 4785 RmdOutputFormat.OUTPUT_PRESENTATION_SUFFIX), 4786 isShinyPrerenderedDoc()); 4787 } 4788 // could be runtime: shiny with a custom format 4789 else if (isShinyDoc()) 4790 { 4791 view_.setIsShinyFormat(false, // no output options b/c no template 4792 false, // not a presentation (unknown format) 4793 isShinyPrerenderedDoc()); 4794 } 4795 else 4796 { 4797 view_.setIsNotShinyFormat(); 4798 if (selTemplate != null) 4799 { 4800 JsArray<RmdTemplateFormat> formats = selTemplate.template.getFormats(); 4801 for (int i = 0; i < formats.length(); i++) 4802 { 4803 // skip notebook format (will enable it later if discovered) 4804 if (formats.get(i).getName() == 4805 RmdOutputFormat.OUTPUT_HTML_NOTEBOOK) 4806 { 4807 continue; 4808 } 4809 4810 // hide powerpoint if not available 4811 if (!session_.getSessionInfo().getPptAvailable() && 4812 StringUtil.equals(formats.get(i).getName(), 4813 RmdOutputFormat.OUTPUT_PPT_PRESENTATION)) 4814 { 4815 continue; 4816 } 4817 4818 String uiName = formats.get(i).getUiName(); 4819 formatList.add(uiName); 4820 valueList.add(formats.get(i).getName()); 4821 extensionList.add(formats.get(i).getExtension()); 4822 if (formats.get(i).getName() == selTemplate.format) 4823 { 4824 formatUiName = uiName; 4825 } 4826 } 4827 } 4828 4829 // add formats not in the selected template 4830 boolean isNotebook = false; 4831 List<String> outputFormats = getOutputFormats(); 4832 for (int i = 0; i < outputFormats.size(); i++) 4833 { 4834 String format = outputFormats.get(i); 4835 if (format == RmdOutputFormat.OUTPUT_HTML_NOTEBOOK) 4836 { 4837 if (i == 0) 4838 isNotebook = true; 4839 formatList.add(0, "Notebook"); 4840 valueList.add(0, format); 4841 extensionList.add(0, ".nb.html"); 4842 continue; 4843 } 4844 if (!valueList.contains(format)) 4845 { 4846 String uiName = format; 4847 int nsLoc = uiName.indexOf("::"); 4848 if (nsLoc != -1) 4849 uiName = uiName.substring(nsLoc + 2); 4850 formatList.add(uiName); 4851 valueList.add(format); 4852 extensionList.add(null); 4853 } 4854 } 4855 4856 view_.setFormatOptions(fileType_, 4857 // can choose output formats 4858 getCustomKnit().length() == 0, 4859 // can edit format options 4860 selTemplate != null, 4861 formatList, 4862 valueList, 4863 extensionList, 4864 formatUiName); 4865 4866 // update notebook-specific options 4867 if (isNotebook) 4868 { 4869 // if the user manually set the output to console in a notebook, 4870 // respect that (even though it's weird) 4871 String outputType = RmdEditorOptions.getString( 4872 YamlFrontMatter.getFrontMatter(docDisplay_), 4873 TextEditingTargetNotebook.CHUNK_OUTPUT_TYPE, null); 4874 if (outputType != TextEditingTargetNotebook.CHUNK_OUTPUT_CONSOLE) 4875 { 4876 // chunk output should always be inline in notebooks 4877 outputType = docUpdateSentinel_.getProperty( 4878 TextEditingTargetNotebook.CHUNK_OUTPUT_TYPE); 4879 if (outputType != TextEditingTargetNotebook.CHUNK_OUTPUT_INLINE) 4880 { 4881 docUpdateSentinel_.setProperty( 4882 TextEditingTargetNotebook.CHUNK_OUTPUT_TYPE, 4883 TextEditingTargetNotebook.CHUNK_OUTPUT_INLINE); 4884 } 4885 } 4886 view_.setIsNotebookFormat(); 4887 } 4888 } 4889 4890 if (isShinyDoc()) 4891 { 4892 // turn off inline output in Shiny documents (if it's not already) 4893 if (docDisplay_.showChunkOutputInline()) 4894 docDisplay_.setShowChunkOutputInline(false); 4895 } 4896 } 4897 setQuartoFormat(String formatName)4898 private void setQuartoFormat(String formatName) 4899 { 4900 // see if we need to change the format 4901 List<String> outputFormats = getQuartoOutputFormats(); 4902 if (outputFormats.size() == 0 || !outputFormats.get(0).equals(formatName)) 4903 { 4904 String yaml = rmarkdownHelper_.setOuartoOutputFormat(getRmdFrontMatter(), formatName); 4905 if (yaml != null) 4906 applyRmdFrontMatter(yaml); 4907 } 4908 4909 // render 4910 renderRmd(); 4911 } 4912 4913 setRmdFormat(String formatName)4914 private void setRmdFormat(String formatName) 4915 { 4916 // If the target format name already matches the first format then just 4917 // render and return 4918 List<String> outputFormats = getOutputFormats(); 4919 if (outputFormats.size() > 0 && outputFormats.get(0).equals(formatName)) 4920 { 4921 renderRmd(); 4922 return; 4923 } 4924 4925 rmarkdownHelper_.setOutputFormat(getRmdFrontMatter(), formatName, 4926 new CommandWithArg<String>() 4927 { 4928 @Override 4929 public void execute(String yaml) 4930 { 4931 if (yaml != null) 4932 applyRmdFrontMatter(yaml); 4933 4934 // re-knit the document 4935 renderRmd(); 4936 } 4937 }); 4938 } 4939 doReflowComment(DocDisplay display, String commentPrefix)4940 void doReflowComment(DocDisplay display, String commentPrefix) 4941 { 4942 doReflowComment(display, commentPrefix, true); 4943 } 4944 doReflowComment(DocDisplay display, String commentPrefix, boolean multiParagraphIndent)4945 void doReflowComment(DocDisplay display, String commentPrefix, boolean multiParagraphIndent) 4946 { 4947 display.focus(); 4948 4949 InputEditorSelection originalSelection = display.getSelection(); 4950 InputEditorSelection selection = originalSelection; 4951 4952 if (selection.isEmpty()) 4953 { 4954 selection = selection.growToIncludeLines("^\\s*" + commentPrefix + ".*$"); 4955 } 4956 else 4957 { 4958 selection = selection.shrinkToNonEmptyLines(); 4959 selection = selection.extendToLineStart(); 4960 selection = selection.extendToLineEnd(); 4961 } 4962 if (selection.isEmpty()) 4963 return; 4964 4965 reflowComments(display, 4966 commentPrefix, 4967 multiParagraphIndent, 4968 selection, 4969 originalSelection.isEmpty() ? 4970 originalSelection.getStart() : 4971 null); 4972 } 4973 reflowComments(DocDisplay display, String commentPrefix, final boolean multiParagraphIndent, InputEditorSelection selection, final InputEditorPosition cursorPos)4974 private void reflowComments(DocDisplay display, 4975 String commentPrefix, 4976 final boolean multiParagraphIndent, 4977 InputEditorSelection selection, 4978 final InputEditorPosition cursorPos) 4979 { 4980 String code = display.getCode(selection); 4981 String[] lines = code.split("\n"); 4982 String prefix = StringUtil.getCommonPrefix(lines, true, false); 4983 Pattern pattern = Pattern.create("^\\s*" + commentPrefix + "+('?)\\s*"); 4984 Match match = pattern.match(prefix, 0); 4985 // Selection includes non-comments? Abort. 4986 if (match == null) 4987 return; 4988 prefix = match.getValue(); 4989 final boolean roxygen = match.hasGroup(1); 4990 4991 int cursorRowIndex = 0; 4992 int cursorColIndex = 0; 4993 if (cursorPos != null) 4994 { 4995 cursorRowIndex = display.selectionToPosition(cursorPos).getRow() - 4996 display.selectionToPosition(selection.getStart()).getRow(); 4997 cursorColIndex = 4998 Math.max(0, cursorPos.getPosition() - prefix.length()); 4999 } 5000 final WordWrapCursorTracker wwct = new WordWrapCursorTracker( 5001 cursorRowIndex, cursorColIndex); 5002 5003 int maxLineLength = prefs_.marginColumn().getValue() - prefix.length(); 5004 5005 WordWrap wordWrap = new WordWrap(maxLineLength, false) 5006 { 5007 @Override 5008 protected boolean forceWrapBefore(String line) 5009 { 5010 String trimmed = line.trim(); 5011 if (roxygen && trimmed.startsWith("@") && !trimmed.startsWith("@@")) 5012 { 5013 // Roxygen tags always need to be at the start of a line. If 5014 // there is content immediately following the roxygen tag, then 5015 // content should be wrapped until the next roxygen tag is 5016 // encountered. 5017 5018 indent_ = ""; 5019 if (TAG_WITH_CONTENTS.match(line, 0) != null) 5020 { 5021 indentRestOfLines_ = true; 5022 } 5023 return true; 5024 } 5025 // empty line disables indentation 5026 else if (!multiParagraphIndent && (line.trim().length() == 0)) 5027 { 5028 indent_ = ""; 5029 indentRestOfLines_ = false; 5030 } 5031 5032 return super.forceWrapBefore(line); 5033 } 5034 5035 @Override 5036 protected void onChunkWritten(String chunk, 5037 int insertionRow, 5038 int insertionCol, 5039 int indexInOriginalString) 5040 { 5041 if (indentRestOfLines_) 5042 { 5043 indentRestOfLines_ = false; 5044 indent_ = " "; // TODO: Use real indent from settings 5045 } 5046 5047 wwct.onChunkWritten(chunk, insertionRow, insertionCol, 5048 indexInOriginalString); 5049 } 5050 5051 private boolean indentRestOfLines_ = false; 5052 private final Pattern TAG_WITH_CONTENTS = Pattern.create("@\\w+\\s+[^\\s]"); 5053 }; 5054 5055 for (String line : lines) 5056 { 5057 String content = line.substring(Math.min(line.length(), 5058 prefix.length())); 5059 5060 if (content.matches("^\\s*\\@examples\\b.*$")) 5061 wordWrap.setWrappingEnabled(false); 5062 else if (content.trim().startsWith("@")) 5063 wordWrap.setWrappingEnabled(true); 5064 5065 wwct.onBeginInputRow(); 5066 wordWrap.appendLine(content); 5067 } 5068 5069 String wrappedString = wordWrap.getOutput(); 5070 5071 StringBuilder finalOutput = new StringBuilder(); 5072 for (String line : StringUtil.getLineIterator(wrappedString)) 5073 finalOutput.append(prefix).append(line).append("\n"); 5074 // Remove final \n 5075 if (finalOutput.length() > 0) 5076 finalOutput.deleteCharAt(finalOutput.length()-1); 5077 5078 String reflowed = finalOutput.toString(); 5079 5080 // Remove trailing whitespace that might have leaked in earlier 5081 reflowed = reflowed.replaceAll("\\s+\\n", "\n"); 5082 5083 display.setSelection(selection); 5084 if (!reflowed.equals(code)) 5085 { 5086 display.replaceSelection(reflowed); 5087 } 5088 5089 if (cursorPos != null) 5090 { 5091 if (wwct.getResult() != null) 5092 { 5093 int row = wwct.getResult().getY(); 5094 int col = wwct.getResult().getX(); 5095 row += display.selectionToPosition(selection.getStart()).getRow(); 5096 col += prefix.length(); 5097 Position pos = Position.create(row, col); 5098 display.setSelection(docDisplay_.createSelection(pos, pos)); 5099 } 5100 else 5101 { 5102 display.collapseSelection(false); 5103 } 5104 } 5105 } 5106 5107 @Handler onExecuteCodeWithoutFocus()5108 void onExecuteCodeWithoutFocus() 5109 { 5110 withVisualModeSelection(() -> 5111 { 5112 codeExecution_.executeSelection(false); 5113 }); 5114 } 5115 5116 @Handler onProfileCodeWithoutFocus()5117 void onProfileCodeWithoutFocus() 5118 { 5119 dependencyManager_.withProfvis("The profiler", new Command() 5120 { 5121 @Override 5122 public void execute() 5123 { 5124 withVisualModeSelection(() -> 5125 { 5126 codeExecution_.executeSelection(false, false, "profvis::profvis", true); 5127 }); 5128 } 5129 }); 5130 } 5131 5132 @Handler onExecuteCodeWithoutMovingCursor()5133 void onExecuteCodeWithoutMovingCursor() 5134 { 5135 if (docDisplay_.isFocused() || visualMode_.isVisualEditorActive()) 5136 { 5137 withVisualModeSelection(() -> 5138 { 5139 codeExecution_.executeSelection(true, false); 5140 }); 5141 } 5142 else if (view_.isAttached()) 5143 { 5144 view_.findSelectAll(); 5145 } 5146 } 5147 5148 @Handler onExecuteCode()5149 void onExecuteCode() 5150 { 5151 if (fileType_.isScript()) 5152 { 5153 codeExecution_.sendSelectionToTerminal(true); 5154 } 5155 else 5156 { 5157 withVisualModeSelection(() -> 5158 { 5159 codeExecution_.executeSelection(true); 5160 }); 5161 } 5162 } 5163 5164 /** 5165 * Performs a command after synchronizing the document and selection state 5166 * from visual mode (useful for executing code). The command is not executed if 5167 * there is no active code editor in visual mode (e.g., the cursor is outside 5168 * a code chunk) 5169 * 5170 * @param command The command to perform 5171 */ withVisualModeSelection(Command command)5172 private void withVisualModeSelection(Command command) 5173 { 5174 if (isVisualEditorActive()) 5175 { 5176 visualMode_.performWithSelection((pos) -> 5177 { 5178 // A null position indicates that the cursor is outside a code chunk. 5179 if (pos != null) 5180 { 5181 command.execute(); 5182 } 5183 }); 5184 } 5185 else 5186 { 5187 command.execute(); 5188 } 5189 } 5190 5191 /** 5192 * Performs a command after synchronizing the document and selection state 5193 * from visual mode. The command will be passed the current position of the 5194 * cursor after synchronizing, or null if the cursor in visual mode has no 5195 * corresponding location in source mode. 5196 * 5197 * @param command The command to perform. 5198 */ withVisualModeSelection(CommandWithArg<Position> command)5199 private void withVisualModeSelection(CommandWithArg<Position> command) 5200 { 5201 if (isVisualEditorActive()) 5202 { 5203 visualMode_.performWithSelection(command); 5204 } 5205 else 5206 { 5207 command.execute(docDisplay_.getCursorPosition()); 5208 } 5209 } 5210 5211 @Handler onRunSelectionAsJob()5212 void onRunSelectionAsJob() 5213 { 5214 withVisualModeSelection(() -> 5215 { 5216 codeExecution_.runSelectionAsJob(false /*useLauncher*/); 5217 }); 5218 } 5219 5220 @Handler onRunSelectionAsLauncherJob()5221 void onRunSelectionAsLauncherJob() 5222 { 5223 withVisualModeSelection(() -> 5224 { 5225 codeExecution_.runSelectionAsJob(true /*useLauncher*/); 5226 }); 5227 } 5228 5229 @Handler onExecuteCurrentLine()5230 void onExecuteCurrentLine() 5231 { 5232 withVisualModeSelection(() -> 5233 { 5234 codeExecution_.executeBehavior(UserPrefs.EXECUTION_BEHAVIOR_LINE); 5235 }); 5236 } 5237 5238 @Handler onExecuteCurrentStatement()5239 void onExecuteCurrentStatement() 5240 { 5241 withVisualModeSelection(() -> 5242 { 5243 codeExecution_.executeBehavior(UserPrefs.EXECUTION_BEHAVIOR_STATEMENT); 5244 }); 5245 } 5246 5247 @Handler onExecuteCurrentParagraph()5248 void onExecuteCurrentParagraph() 5249 { 5250 withVisualModeSelection(() -> 5251 { 5252 codeExecution_.executeBehavior(UserPrefs.EXECUTION_BEHAVIOR_PARAGRAPH); 5253 }); 5254 } 5255 5256 @Handler onSendToTerminal()5257 void onSendToTerminal() 5258 { 5259 withVisualModeSelection(() -> 5260 { 5261 codeExecution_.sendSelectionToTerminal(false); 5262 }); 5263 } 5264 5265 @Handler onOpenNewTerminalAtEditorLocation()5266 void onOpenNewTerminalAtEditorLocation() 5267 { 5268 codeExecution_.openNewTerminalHere(); 5269 } 5270 5271 @Handler onSendFilenameToTerminal()5272 void onSendFilenameToTerminal() 5273 { 5274 codeExecution_.sendFilenameToTerminal(); 5275 } 5276 5277 @Override extractCode(DocDisplay docDisplay, Range range)5278 public String extractCode(DocDisplay docDisplay, Range range) 5279 { 5280 Scope sweaveChunk = null; 5281 5282 if (fileType_.canExecuteChunks()) 5283 sweaveChunk = scopeHelper_.getCurrentSweaveChunk(range.getStart()); 5284 5285 String code = sweaveChunk != null 5286 ? scopeHelper_.getSweaveChunkText(sweaveChunk, range) 5287 : docDisplay_.getCode(range.getStart(), range.getEnd()); 5288 5289 return code; 5290 } 5291 5292 5293 5294 @Handler onExecuteAllCode()5295 void onExecuteAllCode() 5296 { 5297 boolean executeChunks = fileType_.canCompilePDF() || 5298 fileType_.canKnitToHTML() || 5299 fileType_.isRpres(); 5300 5301 if (executeChunks) 5302 { 5303 prepareForVisualExecution(() -> 5304 { 5305 executeChunks(Position.create( 5306 docDisplay_.getDocumentEnd().getRow() + 1, 5307 0), 5308 TextEditingTargetScopeHelper.PREVIOUS_CHUNKS); 5309 }); 5310 } 5311 else 5312 { 5313 sourceActiveDocument(true); 5314 } 5315 } 5316 5317 @Handler onExecuteToCurrentLine()5318 void onExecuteToCurrentLine() 5319 { 5320 withVisualModeSelection(() -> 5321 { 5322 if (!isVisualEditorActive()) 5323 { 5324 docDisplay_.focus(); 5325 } 5326 5327 int row = docDisplay_.getSelectionEnd().getRow(); 5328 int col = docDisplay_.getLength(row); 5329 5330 codeExecution_.executeRange(Range.fromPoints(Position.create(0, 0), 5331 Position.create(row, col))); 5332 }); 5333 } 5334 5335 @Handler onExecuteFromCurrentLine()5336 void onExecuteFromCurrentLine() 5337 { 5338 withVisualModeSelection(() -> 5339 { 5340 if (!isVisualEditorActive()) 5341 { 5342 docDisplay_.focus(); 5343 } 5344 5345 int startRow = docDisplay_.getSelectionStart().getRow(); 5346 int startColumn = 0; 5347 Position start = Position.create(startRow, startColumn); 5348 5349 codeExecution_.executeRange(Range.fromPoints(start, endPosition())); 5350 }); 5351 } 5352 5353 @Handler onExecuteCurrentFunction()5354 void onExecuteCurrentFunction() 5355 { 5356 withVisualModeSelection(() -> 5357 { 5358 if (!isVisualEditorActive()) 5359 { 5360 docDisplay_.focus(); 5361 5362 // HACK: This is just to force the entire function tree to be built. 5363 // It's the easiest way to make sure getCurrentScope() returns 5364 // a Scope with an end. 5365 // 5366 // We don't need to do this in visual mode since we force a scope 5367 // tree rebuild in the process of synchronizing the selection. 5368 docDisplay_.getScopeTree(); 5369 } 5370 5371 Scope currentFunction = docDisplay_.getCurrentFunction(false); 5372 5373 // Check if we're at the top level (i.e. not in a function), or in 5374 // an unclosed function 5375 if (currentFunction == null || currentFunction.getEnd() == null) 5376 return; 5377 5378 Position start = currentFunction.getPreamble(); 5379 Position end = currentFunction.getEnd(); 5380 5381 codeExecution_.executeRange(Range.fromPoints(start, end)); 5382 }); 5383 } 5384 5385 @Handler onExecuteCurrentSection()5386 void onExecuteCurrentSection() 5387 { 5388 withVisualModeSelection(() -> 5389 { 5390 if (!isVisualEditorActive()) 5391 { 5392 docDisplay_.focus(); 5393 docDisplay_.getScopeTree(); 5394 } 5395 5396 // Determine the current section. 5397 Scope currentSection = docDisplay_.getCurrentSection(); 5398 if (currentSection == null) 5399 return; 5400 5401 // Determine the start and end of the section 5402 Position start = currentSection.getBodyStart(); 5403 if (start == null) 5404 start = Position.create(0, 0); 5405 Position end = currentSection.getEnd(); 5406 if (end == null) 5407 end = endPosition(); 5408 5409 codeExecution_.executeRange(Range.fromPoints(start, end)); 5410 }); 5411 } 5412 endPosition()5413 private Position endPosition() 5414 { 5415 int endRow = Math.max(0, docDisplay_.getRowCount() - 1); 5416 int endColumn = docDisplay_.getLength(endRow); 5417 return Position.create(endRow, endColumn); 5418 } 5419 5420 // splits a chunk into two chunks 5421 // chunk 1: first chunk line to linePos (not including linePos) 5422 // chunk 2: linePos line to end splitChunk(Scope chunk, int linePos)5423 private void splitChunk(Scope chunk, int linePos) 5424 { 5425 Position chunkStart = chunk.getBodyStart(); 5426 Position chunkEnd = chunk.getEnd(); 5427 if (chunkEnd == null) 5428 chunkEnd = docDisplay_.getDocumentEnd(); 5429 Position preamble = chunk.getPreamble(); 5430 String preambleLine = docDisplay_.getLine(preamble.getRow()) + "\n"; 5431 5432 // if the cursor line is in the preamble of the chunk 5433 // reset it to the body of the chunk to get the same semantics 5434 // (empty chunk followed by the entire existing chunk) 5435 if (linePos < chunkStart.getRow()) 5436 linePos = chunkStart.getRow(); 5437 5438 // get chunk contents from chunk start up to the specified line 5439 Range firstChunkRange = Range.create(chunkStart.getRow(), chunkStart.getColumn(), linePos, 0); 5440 String firstChunkContents = docDisplay_.getTextForRange(firstChunkRange); 5441 firstChunkContents = firstChunkContents.trim(); 5442 5443 // add preamble line and ending line for new first chunk 5444 firstChunkContents = preambleLine + firstChunkContents; 5445 if (!firstChunkContents.endsWith("\n")) 5446 firstChunkContents += "\n"; 5447 firstChunkContents += getChunkEnd() + "\n\n"; 5448 5449 // get second chunk contents from what's left 5450 Range secondChunkRange = Range.create(linePos, 0, chunkEnd.getRow(), chunkEnd.getColumn()); 5451 String secondChunkContents = docDisplay_.getTextForRange(secondChunkRange); 5452 secondChunkContents = secondChunkContents.trim(); 5453 5454 // add the preamble line of the original chunk to the second chunk (so we have the correct language) 5455 secondChunkContents = preambleLine + secondChunkContents; 5456 5457 // modify contents of original chunk with second chunk contents 5458 Range chunkRange = Range.create(chunkStart.getRow() - 1, chunkStart.getColumn(), chunkEnd.getRow(), chunkEnd.getColumn()); 5459 docDisplay_.replaceRange(chunkRange, secondChunkContents); 5460 5461 // insert new chunk with first chunk contents 5462 docDisplay_.setCursorPosition(Position.create(chunkStart.getRow() - 1, chunkStart.getColumn())); 5463 docDisplay_.insertCode(firstChunkContents, false); 5464 } 5465 5466 // splits a chunk into three chunks 5467 // chunk 1: first chunk line to start pos 5468 // chunk 2: start pos to end pos 5469 // chunk 3: end pos to end of chunk splitChunk(Scope chunk, Position startPos, Position endPos)5470 private void splitChunk(Scope chunk, Position startPos, Position endPos) 5471 { 5472 Position chunkStart = chunk.getBodyStart(); 5473 Position chunkEnd = chunk.getEnd(); 5474 if (chunkEnd == null) 5475 chunkEnd = docDisplay_.getDocumentEnd(); 5476 Position preamble = chunk.getPreamble(); 5477 String preambleLine = docDisplay_.getLine(preamble.getRow()) + "\n"; 5478 5479 // if the selected position is only within the preamble, do nothing as it makes no sense to do any splitting 5480 if (startPos.getRow() == preamble.getRow() && endPos.getRow() == startPos.getRow()) 5481 return; 5482 5483 // if the selected position starts within the preamble, reset the start position to not include the preamble 5484 // as it does not make any sense to do any splitting within the preamble 5485 if (startPos.getRow() == preamble.getRow()) 5486 { 5487 startPos.setRow(preamble.getRow() + 1); 5488 startPos.setColumn(0); 5489 } 5490 5491 // if the selected position ends within the footer (```), reset the end position to not include it 5492 if (endPos.getRow() == chunkEnd.getRow() && endPos.getColumn() > 0) 5493 { 5494 endPos.setColumn(0); 5495 } 5496 5497 // get chunk contents from chunk start up to the specified start pos 5498 Range firstChunkRange = Range.create(chunkStart.getRow(), chunkStart.getColumn(), startPos.getRow(), startPos.getColumn()); 5499 String firstChunkContents = docDisplay_.getTextForRange(firstChunkRange); 5500 firstChunkContents = firstChunkContents.trim(); 5501 5502 // add preamble line and ending line for new first chunk 5503 firstChunkContents = preambleLine + firstChunkContents; 5504 if (!firstChunkContents.endsWith("\n")) 5505 firstChunkContents += "\n"; 5506 firstChunkContents += getChunkEnd() + "\n\n"; 5507 5508 // get middle chunk contents from selected positions 5509 Range middleChunkRange = Range.create(startPos.getRow(), startPos.getColumn(), endPos.getRow(), endPos.getColumn()); 5510 String middleChunkContents = docDisplay_.getTextForRange(middleChunkRange); 5511 middleChunkContents = middleChunkContents.trim(); 5512 5513 // // add preamble line and ending line for middle chunk 5514 middleChunkContents = preambleLine + middleChunkContents; 5515 if (!middleChunkContents.endsWith("\n")) 5516 middleChunkContents += "\n"; 5517 middleChunkContents += getChunkEnd() + "\n\n"; 5518 5519 // get final chunk contents from ending selection position to ending chunk position 5520 Range finalChunkRange = Range.create(endPos.getRow(), endPos.getColumn(), chunkEnd.getRow(), chunkEnd.getColumn()); 5521 String finalChunkContents = docDisplay_.getTextForRange(finalChunkRange); 5522 finalChunkContents = finalChunkContents.trim(); 5523 5524 // add preamble to final chunk 5525 finalChunkContents = preambleLine + finalChunkContents; 5526 5527 // modify contents of original chunk with final chunk contents 5528 Range chunkRange = Range.create(chunkStart.getRow() - 1, chunkStart.getColumn(), chunkEnd.getRow(), chunkEnd.getColumn()); 5529 docDisplay_.replaceRange(chunkRange, finalChunkContents); 5530 5531 // insert first and middle chunk contents as new chunks 5532 docDisplay_.setCursorPosition(Position.create(chunkStart.getRow() - 1, chunkStart.getColumn())); 5533 docDisplay_.insertCode(firstChunkContents, false); 5534 Position middleChunkPos = docDisplay_.getCursorPosition(); 5535 docDisplay_.insertCode(middleChunkContents, false); 5536 5537 // reset cursor position to middle chunk (selected text) 5538 docDisplay_.setCursorPosition(middleChunkPos); 5539 } 5540 onInsertChunk(String chunkPlaceholder, int rowOffset, int colOffset)5541 private void onInsertChunk(String chunkPlaceholder, int rowOffset, int colOffset) 5542 { 5543 // allow visual mode to handle 5544 if (visualMode_.isActivated()) 5545 { 5546 // strip off the leading backticks (if rowOffset is 0 then adjust colOffset) 5547 chunkPlaceholder = chunkPlaceholder.replaceFirst("```", ""); 5548 if (rowOffset == 0) 5549 colOffset -= 3; 5550 5551 // strip off the trailing backticks 5552 chunkPlaceholder = chunkPlaceholder.replaceAll("\\n```\\n$", ""); 5553 5554 // do the insert 5555 visualMode_.insertChunk(chunkPlaceholder, rowOffset, colOffset); 5556 5557 // all done! 5558 return; 5559 } 5560 5561 String sel = ""; 5562 Range selRange = null; 5563 5564 // if currently in a chunk 5565 // with no selection, split this chunk into two chunks at the current line 5566 // with selection, split this chunk into three chunks (prior to selection, selected, post selection) 5567 Scope currentChunk = docDisplay_.getCurrentChunk(); 5568 if (currentChunk != null) 5569 { 5570 // record current selection before manipulating text 5571 sel = docDisplay_.getSelectionValue(); 5572 selRange = docDisplay_.getSelectionRange(); 5573 5574 if (selRange.isEmpty()) 5575 { 5576 splitChunk(currentChunk, docDisplay_.getCursorPosition().getRow()); 5577 return; 5578 } 5579 else 5580 { 5581 splitChunk(currentChunk, selRange.getStart() ,selRange.getEnd()); 5582 return; 5583 } 5584 } 5585 5586 Position pos = moveCursorToNextInsertLocation(); 5587 InsertChunkInfo insertChunkInfo = docDisplay_.getInsertChunkInfo(); 5588 if (insertChunkInfo != null) 5589 { 5590 // inject the chunk skeleton 5591 docDisplay_.insertCode(chunkPlaceholder, false); 5592 5593 // if we had text selected, inject it into the chunk 5594 if (!StringUtil.isNullOrEmpty(sel)) 5595 { 5596 Position contentPosition = insertChunkInfo.getContentPosition(); 5597 Position docContentPos = Position.create( 5598 pos.getRow() + contentPosition.getRow(), 5599 contentPosition.getColumn()); 5600 Position endPos = Position.create(docContentPos.getRow(), 5601 docContentPos.getColumn()); 5602 5603 // move over newline if selected 5604 if (sel.endsWith("\n")) 5605 endPos.setRow(endPos.getRow() + 1); 5606 docDisplay_.replaceRange( 5607 Range.fromPoints(docContentPos, endPos), sel); 5608 docDisplay_.replaceRange(selRange, ""); 5609 } 5610 5611 Position cursorPosition = insertChunkInfo.getCursorPosition(); 5612 docDisplay_.setCursorPosition(Position.create( 5613 pos.getRow() + cursorPosition.getRow() + rowOffset, 5614 colOffset)); 5615 docDisplay_.focus(); 5616 } 5617 else 5618 { 5619 assert false : "Mode did not have insertChunkInfo available"; 5620 } 5621 } 5622 getChunkEnd()5623 String getChunkEnd() 5624 { 5625 InsertChunkInfo info = docDisplay_.getInsertChunkInfo(); 5626 if (info == null) 5627 return "```"; // default to Rmd 5628 5629 // chunks are delimited by 2 new lines 5630 // if not, we will fallback on an empty chunk end just for safety 5631 String[] chunkParts = info.getValue().split("\n\n"); 5632 if (chunkParts.length == 2) 5633 return chunkParts[1]; 5634 else 5635 return ""; 5636 } 5637 5638 @Handler onInsertChunk()5639 void onInsertChunk() 5640 { 5641 if (fileType_.isQuartoMarkdown()) 5642 { 5643 onQuartoInsertChunk(); 5644 return; 5645 } 5646 5647 InsertChunkInfo info = docDisplay_.getInsertChunkInfo(); 5648 if (info == null) 5649 return; 5650 5651 onInsertChunk(info.getValue(), 1, 0); 5652 } 5653 5654 @Handler onInsertChunkR()5655 void onInsertChunkR() 5656 { 5657 onInsertChunk("```{r}\n\n```\n", 1, 0); 5658 } 5659 5660 @Handler onInsertChunkBash()5661 void onInsertChunkBash() 5662 { 5663 onInsertChunk("```{bash}\n\n```\n", 1, 0); 5664 } 5665 5666 @Handler onInsertChunkPython()5667 void onInsertChunkPython() 5668 { 5669 onInsertChunk("```{python}\n\n```\n", 1, 0); 5670 } 5671 5672 @Handler onInsertChunkRCPP()5673 void onInsertChunkRCPP() 5674 { 5675 onInsertChunk("```{Rcpp}\n\n```\n", 1, 0); 5676 } 5677 5678 @Handler onInsertChunkStan()5679 void onInsertChunkStan() 5680 { 5681 onInsertChunk("```{stan output.var=}\n\n```\n", 0, 20); 5682 } 5683 5684 @Handler onInsertChunkSQL()5685 void onInsertChunkSQL() 5686 { 5687 server_.defaultSqlConnectionName(new ServerRequestCallback<String>() 5688 { 5689 @Override 5690 public void onResponseReceived(String name) 5691 { 5692 if (name != null) 5693 { 5694 onInsertChunk("```{sql connection=" + name + "}\n\n```\n", 1, 0); 5695 } 5696 else 5697 { 5698 onInsertChunk("```{sql connection=}\n\n```\n", 0, 19); 5699 } 5700 } 5701 5702 @Override 5703 public void onError(ServerError error) 5704 { 5705 Debug.logError(error); 5706 onInsertChunk("```{sql connection=}\n\n```\n", 0, 19); 5707 } 5708 }); 5709 } 5710 5711 @Handler onInsertChunkD3()5712 void onInsertChunkD3() 5713 { 5714 if (notebook_ != null) { 5715 Scope setupScope = notebook_.getSetupChunkScope(); 5716 5717 if (setupScope == null && !visualMode_.isActivated()) 5718 { 5719 onInsertChunk("```{r setup}\nlibrary(r2d3)\n```\n\n```{d3 data=}\n\n```\n", 4, 12); 5720 } 5721 else { 5722 onInsertChunk("```{d3 data=}\n\n```\n", 0, 12); 5723 } 5724 } 5725 } 5726 5727 // for qmd files, we default to python unless there is already an 5728 // r or ojs chunk in the file onQuartoInsertChunk()5729 void onQuartoInsertChunk() 5730 { 5731 JsArrayString lines = docDisplay_.getLines(); 5732 for (int i=0; i<lines.length(); i++) 5733 { 5734 Match match = RegexUtil.RE_RMARKDOWN_CHUNK_BEGIN.match(lines.get(i), 0); 5735 if (match != null) 5736 { 5737 String engine = match.getGroup(1); 5738 Match matchName = RegexUtil.RE_RMARKDOWN_ENGINE_NAME.match(engine, 0); 5739 if (matchName != null) 5740 { 5741 onInsertChunk("```{" + matchName.getValue() + "}\n\n```\n", 1, 0); 5742 return; 5743 } 5744 5745 } 5746 } 5747 5748 // no other qualifying previous chunks, use r 5749 onInsertChunkR(); 5750 } 5751 5752 @Handler onInsertSection()5753 void onInsertSection() 5754 { 5755 globalDisplay_.promptForText( 5756 "Insert Section", 5757 "Section label:", 5758 MessageDisplay.INPUT_OPTIONAL_TEXT, 5759 new OperationWithInput<String>() { 5760 @Override 5761 public void execute(String label) 5762 { 5763 // move cursor to next insert location 5764 Position pos = moveCursorToNextInsertLocation(); 5765 5766 // truncate length to print margin - 5 5767 int printMarginColumn = prefs_.marginColumn().getValue(); 5768 int length = printMarginColumn - 5; 5769 5770 // truncate label to maxLength - 10 (but always allow at 5771 // least 20 chars for the label) 5772 int maxLabelLength = length - 10; 5773 maxLabelLength = Math.max(maxLabelLength, 20); 5774 if (label.length() > maxLabelLength) 5775 label = label.substring(0, maxLabelLength-1); 5776 5777 // prefix 5778 String prefix = "# "; 5779 if (!label.isEmpty()) 5780 prefix = prefix + label + " "; 5781 5782 // fill to maxLength (bit ensure at least 4 fill characters 5783 // so the section parser is sure to pick it up) 5784 StringBuffer sectionLabel = new StringBuffer(); 5785 sectionLabel.append("\n"); 5786 sectionLabel.append(prefix); 5787 int fillChars = length - prefix.length(); 5788 fillChars = Math.max(fillChars, 4); 5789 for (int i=0; i<fillChars; i++) 5790 sectionLabel.append("-"); 5791 sectionLabel.append("\n\n"); 5792 5793 // insert code and move cursor 5794 docDisplay_.insertCode(sectionLabel.toString(), false); 5795 docDisplay_.setCursorPosition(Position.create(pos.getRow() + 3, 5796 0)); 5797 docDisplay_.focus(); 5798 5799 } 5800 }); 5801 } 5802 moveCursorToNextInsertLocation()5803 private Position moveCursorToNextInsertLocation() 5804 { 5805 docDisplay_.collapseSelection(true); 5806 if (!docDisplay_.moveSelectionToBlankLine()) 5807 { 5808 int lastRow = docDisplay_.getRowCount(); 5809 int lastCol = docDisplay_.getLength(lastRow); 5810 Position endPos = Position.create(lastRow, lastCol); 5811 docDisplay_.setCursorPosition(endPos); 5812 docDisplay_.insertCode("\n", false); 5813 } 5814 return docDisplay_.getCursorPosition(); 5815 5816 } 5817 executeChunk(Position position)5818 public void executeChunk(Position position) 5819 { 5820 docDisplay_.getScopeTree(); 5821 executeSweaveChunk(scopeHelper_.getCurrentSweaveChunk(position), 5822 NotebookQueueUnit.EXEC_MODE_SINGLE, false); 5823 } 5824 dequeueChunk(int row)5825 public void dequeueChunk(int row) 5826 { 5827 notebook_.dequeueChunk(row); 5828 } 5829 5830 @Handler onExecuteCurrentChunk()5831 void onExecuteCurrentChunk() 5832 { 5833 withVisualModeSelection(() -> 5834 { 5835 // HACK: This is just to force the entire function tree to be built. 5836 // It's the easiest way to make sure getCurrentScope() returns 5837 // a Scope with an end. 5838 docDisplay_.getScopeTree(); 5839 5840 executeSweaveChunk(scopeHelper_.getCurrentSweaveChunk(), 5841 NotebookQueueUnit.EXEC_MODE_SINGLE, false); 5842 }); 5843 } 5844 5845 @Handler onExecuteNextChunk()5846 void onExecuteNextChunk() 5847 { 5848 withVisualModeSelection((pos) -> 5849 { 5850 Scope nextChunk = null; 5851 if (pos == null) 5852 { 5853 // We are outside a chunk in visual mode, so get the nearest chunk below 5854 nextChunk = visualMode_.getNearestChunkScope(TextEditingTargetScopeHelper.FOLLOWING_CHUNKS); 5855 if (nextChunk == null) 5856 { 5857 // No next chunk to execute 5858 return; 5859 } 5860 } 5861 else 5862 { 5863 // Force scope tree rebuild and get chunk from source mode 5864 docDisplay_.getScopeTree(); 5865 nextChunk = scopeHelper_.getNextSweaveChunk(); 5866 } 5867 5868 executeSweaveChunk(nextChunk, NotebookQueueUnit.EXEC_MODE_SINGLE, 5869 true); 5870 docDisplay_.setCursorPosition(nextChunk.getBodyStart()); 5871 docDisplay_.ensureCursorVisible(); 5872 }); 5873 } 5874 5875 @Handler onExecutePreviousChunks()5876 void onExecutePreviousChunks() 5877 { 5878 executeScopedChunks(TextEditingTargetScopeHelper.PREVIOUS_CHUNKS); 5879 } 5880 5881 @Handler onExecuteSubsequentChunks()5882 void onExecuteSubsequentChunks() 5883 { 5884 executeScopedChunks(TextEditingTargetScopeHelper.FOLLOWING_CHUNKS); 5885 } 5886 5887 /** 5888 * Executes all chunks in the given direction (previous or following) 5889 * 5890 * @param dir The direction in which to execute 5891 */ executeScopedChunks(int dir)5892 private void executeScopedChunks(int dir) 5893 { 5894 withVisualModeSelection((pos) -> 5895 { 5896 if (pos == null) 5897 { 5898 // No active chunk position; look for the nearest chunk in the given direction 5899 Scope scope = visualMode_.getNearestChunkScope(dir); 5900 if (scope == null) 5901 { 5902 // No suitable chunks found; do nothing (expected if there just aren't 5903 // any previous/next chunks to run) 5904 return; 5905 } 5906 if (dir == TextEditingTargetScopeHelper.FOLLOWING_CHUNKS) 5907 { 5908 // Going down: start at beginning of next chunk 5909 pos = scope.getBodyStart(); 5910 } 5911 else 5912 { 5913 // Going up: start *just beneath* chunk if we can (so chunk itself is included) 5914 5915 // Clone position so we can update it without affecting the chunk scope 5916 pos = Position.create(scope.getEnd().getRow(), scope.getEnd().getColumn()); 5917 if (pos.getRow() < docDisplay_.getRowCount()) 5918 { 5919 pos.setRow(pos.getRow() + 1); 5920 pos.setColumn(1); 5921 } 5922 } 5923 } 5924 executeChunks(pos, dir); 5925 }); 5926 } 5927 executeChunks(Position position, int which)5928 public void executeChunks(Position position, int which) 5929 { 5930 // null implies we should use current cursor position 5931 if (position == null) 5932 position = docDisplay_.getSelectionStart(); 5933 5934 if (docDisplay_.showChunkOutputInline()) 5935 { 5936 executeChunksNotebookMode(position, which); 5937 return; 5938 } 5939 5940 // HACK: This is just to force the entire function tree to be built. 5941 // It's the easiest way to make sure getCurrentScope() returns 5942 // a Scope with an end. 5943 docDisplay_.getScopeTree(); 5944 5945 // execute the chunks 5946 Scope[] previousScopes = scopeHelper_.getSweaveChunks(position, 5947 which); 5948 5949 StringBuilder builder = new StringBuilder(); 5950 for (Scope scope : previousScopes) 5951 { 5952 if (isRChunk(scope) && isExecutableChunk(scope)) 5953 { 5954 builder.append("# " + scope.getLabel() + "\n"); 5955 builder.append(scopeHelper_.getSweaveChunkText(scope)); 5956 builder.append("\n\n"); 5957 } 5958 } 5959 5960 final String code = builder.toString().trim(); 5961 if (fileType_.isRmd()) 5962 { 5963 final Position positionFinal = position; 5964 docUpdateSentinel_.withSavedDoc(new Command() 5965 { 5966 @Override 5967 public void execute() 5968 { 5969 rmarkdownHelper_.prepareForRmdChunkExecution( 5970 docUpdateSentinel_.getId(), 5971 docUpdateSentinel_.getContents(), 5972 new Command() 5973 { 5974 @Override 5975 public void execute() 5976 { 5977 // compute the language for this chunk 5978 String language = "R"; 5979 if (DocumentMode.isPositionInPythonMode(docDisplay_, positionFinal)) 5980 language = "Python"; 5981 5982 events_.fireEvent(new SendToConsoleEvent(code, language, true)); 5983 } 5984 }); 5985 } 5986 }); 5987 } 5988 else 5989 { 5990 events_.fireEvent(new SendToConsoleEvent(code, true)); 5991 } 5992 } 5993 executeChunksNotebookMode(Position position, int which)5994 public void executeChunksNotebookMode(Position position, int which) 5995 { 5996 // HACK: This is just to force the entire function tree to be built. 5997 // It's the easiest way to make sure getCurrentScope() returns 5998 // a Scope with an end. 5999 docDisplay_.getScopeTree(); 6000 6001 // execute the previous chunks 6002 Scope[] previousScopes = scopeHelper_.getSweaveChunks(position, which); 6003 6004 // create job description 6005 String jobDesc = ""; 6006 if (previousScopes.length > 0) 6007 { 6008 if (position != null && 6009 position.getRow() > docDisplay_.getDocumentEnd().getRow()) 6010 jobDesc = "Run All"; 6011 else if (which == TextEditingTargetScopeHelper.PREVIOUS_CHUNKS) 6012 jobDesc = "Run Previous"; 6013 else if (which == TextEditingTargetScopeHelper.FOLLOWING_CHUNKS) 6014 jobDesc = "Run After"; 6015 } 6016 6017 List<ChunkExecUnit> chunks = new ArrayList<>(); 6018 for (Scope scope : previousScopes) 6019 { 6020 if (isExecutableChunk(scope)) 6021 chunks.add( 6022 new ChunkExecUnit(scope, NotebookQueueUnit.EXEC_MODE_BATCH)); 6023 } 6024 6025 if (!chunks.isEmpty()) 6026 notebook_.executeChunks(jobDesc, chunks); 6027 } 6028 6029 @Handler onExecuteSetupChunk()6030 public void onExecuteSetupChunk() 6031 { 6032 prepareForVisualExecution(() -> executeSetupChunk()); 6033 } 6034 executeSetupChunk()6035 private void executeSetupChunk() 6036 { 6037 // attempt to find the setup scope by name 6038 Scope setupScope = null; 6039 if (notebook_ != null) 6040 setupScope = notebook_.getSetupChunkScope(); 6041 6042 // if we didn't find it by name, flatten the scope list and find the 6043 // first chunk 6044 if (setupScope == null) 6045 { 6046 ScopeList scopes = new ScopeList(docDisplay_); 6047 for (Scope scope: scopes) 6048 { 6049 if (scope.isChunk()) 6050 { 6051 setupScope = scope; 6052 break; 6053 } 6054 } 6055 } 6056 6057 // if we found a candidate, run it 6058 if (setupScope != null) 6059 { 6060 executeSweaveChunk(setupScope, NotebookQueueUnit.EXEC_MODE_BATCH, 6061 false); 6062 } 6063 } 6064 renderLatex()6065 public void renderLatex() 6066 { 6067 if (mathjax_ != null) 6068 mathjax_.renderLatex(); 6069 } 6070 renderLatex(Range range, boolean background)6071 public void renderLatex(Range range, boolean background) 6072 { 6073 if (mathjax_ != null) 6074 mathjax_.renderLatex(range, background); 6075 } 6076 getDefaultNamePrefix()6077 public String getDefaultNamePrefix() 6078 { 6079 return null; 6080 } 6081 6082 @Override getCurrentStatus()6083 public String getCurrentStatus() 6084 { 6085 Position pos = docDisplay_.getCursorPosition(); 6086 String scope = statusBar_.getScope().getValue(); 6087 if (StringUtil.isNullOrEmpty(scope)) 6088 scope = "None"; 6089 String name = getName().getValue(); 6090 if (StringUtil.isNullOrEmpty(name)) 6091 name = "No name"; 6092 6093 StringBuilder status = new StringBuilder(); 6094 status.append("Row ").append(pos.getRow() + 1).append(" Column ").append(pos.getColumn() + 1); 6095 status.append(" Scope ").append(scope); 6096 status.append(" File type ").append(fileType_.getLabel()); 6097 status.append(" File name ").append(name); 6098 return status.toString(); 6099 } 6100 isRChunk(Scope scope)6101 private boolean isRChunk(Scope scope) 6102 { 6103 String labelText = docDisplay_.getLine(scope.getPreamble().getRow()); 6104 Pattern reEngine = Pattern.create(".*engine\\s*=\\s*['\"]([^'\"]*)['\"]"); 6105 Match match = reEngine.match(labelText, 0); 6106 if (match == null) 6107 return true; 6108 6109 String engine = match.getGroup(1).toLowerCase(); 6110 6111 // NOTE: We might want to include 'Rscript' but such chunks are typically 6112 // intended to be run in their own process so it might not make sense to 6113 // collect those here. 6114 return engine.equals("r"); 6115 } 6116 isExecutableChunk(final Scope chunk)6117 private boolean isExecutableChunk(final Scope chunk) 6118 { 6119 if (!chunk.isChunk()) 6120 return false; 6121 6122 String headerText = docDisplay_.getLine(chunk.getPreamble().getRow()); 6123 Pattern reEvalFalse = Pattern.create("eval\\s*=\\s*F(?:ALSE)?"); 6124 if (reEvalFalse.test(headerText)) 6125 return false; 6126 6127 return true; 6128 } 6129 executeSweaveChunk(final Scope chunk, final int mode, final boolean scrollNearTop)6130 private void executeSweaveChunk(final Scope chunk, 6131 final int mode, 6132 final boolean scrollNearTop) 6133 { 6134 if (chunk == null) 6135 return; 6136 6137 // command used to execute chunk (we may need to defer it if this 6138 // is an Rmd document as populating params might be necessary) 6139 final Command executeChunk = new Command() { 6140 @Override 6141 public void execute() 6142 { 6143 Range range = scopeHelper_.getSweaveChunkInnerRange(chunk); 6144 if (scrollNearTop) 6145 { 6146 docDisplay_.navigateToPosition( 6147 SourcePosition.create(range.getStart().getRow(), 6148 range.getStart().getColumn()), 6149 true); 6150 } 6151 if (!range.isEmpty()) 6152 { 6153 codeExecution_.setLastExecuted(range.getStart(), range.getEnd()); 6154 } 6155 if (fileType_.isRmd() && 6156 docDisplay_.showChunkOutputInline()) 6157 { 6158 // in notebook mode, an empty chunk can refer to external code, 6159 // so always execute it 6160 notebook_.executeChunk(chunk); 6161 } 6162 else if (!range.isEmpty()) 6163 { 6164 String code = scopeHelper_.getSweaveChunkText(chunk); 6165 6166 // compute the language for this chunk 6167 String language = "R"; 6168 if (DocumentMode.isPositionInPythonMode(docDisplay_, chunk.getBodyStart())) 6169 language = "Python"; 6170 6171 events_.fireEvent(new SendToConsoleEvent(code, language, true)); 6172 } 6173 docDisplay_.collapseSelection(true); 6174 } 6175 }; 6176 6177 // Rmd allows server-side prep for chunk execution 6178 if (fileType_.isRmd() && !docDisplay_.showChunkOutputInline()) 6179 { 6180 // ensure source is synced with server 6181 docUpdateSentinel_.withSavedDoc(new Command() { 6182 @Override 6183 public void execute() 6184 { 6185 // allow server to prepare for chunk execution 6186 // (e.g. by populating 'params' in the global environment) 6187 rmarkdownHelper_.prepareForRmdChunkExecution( 6188 docUpdateSentinel_.getId(), 6189 docUpdateSentinel_.getContents(), 6190 executeChunk); 6191 } 6192 }); 6193 } 6194 else 6195 { 6196 executeChunk.execute(); 6197 } 6198 6199 } 6200 6201 @Handler onJumpTo()6202 void onJumpTo() 6203 { 6204 statusBar_.getScope().click(); 6205 } 6206 6207 @Handler onGoToLine()6208 void onGoToLine() 6209 { 6210 globalDisplay_.promptForInteger( 6211 "Go to Line", 6212 "Enter line number:", 6213 null, 6214 new ProgressOperationWithInput<Integer>() 6215 { 6216 @Override 6217 public void execute(Integer line, ProgressIndicator indicator) 6218 { 6219 indicator.onCompleted(); 6220 6221 line = Math.max(1, line); 6222 line = Math.min(docDisplay_.getRowCount(), line); 6223 6224 docDisplay_.navigateToPosition( 6225 SourcePosition.create(line-1, 0), 6226 true); 6227 } 6228 }, 6229 null); 6230 } 6231 6232 @Handler onCodeCompletion()6233 void onCodeCompletion() 6234 { 6235 withActiveEditor((disp) -> 6236 { 6237 disp.codeCompletion(); 6238 }); 6239 } 6240 6241 @Handler onGoToHelp()6242 void onGoToHelp() 6243 { 6244 withActiveEditor((disp) -> 6245 { 6246 disp.goToHelp(); 6247 }); 6248 } 6249 6250 @Handler onGoToDefinition()6251 void onGoToDefinition() 6252 { 6253 withActiveEditor((disp) -> 6254 { 6255 disp.goToDefinition(); 6256 }); 6257 } 6258 6259 @Handler onFindAll()6260 void onFindAll() 6261 { 6262 docDisplay_.selectAll(docDisplay_.getSelectionValue()); 6263 } 6264 6265 @Handler onFindUsages()6266 void onFindUsages() 6267 { 6268 cppHelper_.findUsages(); 6269 } 6270 6271 @Handler onSetWorkingDirToActiveDoc()6272 public void onSetWorkingDirToActiveDoc() 6273 { 6274 // get path 6275 String activeDocPath = docUpdateSentinel_.getPath(); 6276 if (activeDocPath != null) 6277 { 6278 FileSystemItem wdPath = 6279 FileSystemItem.createFile(activeDocPath).getParentPath(); 6280 consoleDispatcher_.executeSetWd(wdPath, true); 6281 } 6282 else 6283 { 6284 globalDisplay_.showMessage( 6285 MessageDialog.WARNING, 6286 "Source File Not Saved", 6287 "The currently active source file is not saved so doesn't " + 6288 "have a directory to change into."); 6289 } 6290 } 6291 stangle(String sweaveStr)6292 private String stangle(String sweaveStr) 6293 { 6294 ScopeList chunks = new ScopeList(docDisplay_); 6295 chunks.selectAll(ScopeList.CHUNK); 6296 6297 StringBuilder code = new StringBuilder(); 6298 for (Scope chunk : chunks) 6299 { 6300 String text = scopeHelper_.getSweaveChunkText(chunk); 6301 code.append(text); 6302 if (text.length() > 0 && StringUtil.charAt(text, text.length()-1) != '\n') 6303 code.append('\n'); 6304 } 6305 return code.toString(); 6306 } 6307 6308 @Handler onPreviewJS()6309 void onPreviewJS() 6310 { 6311 previewJS(); 6312 } 6313 6314 @Handler onPreviewSql()6315 void onPreviewSql() 6316 { 6317 previewSql(); 6318 } 6319 6320 @Handler onSourceActiveDocument()6321 void onSourceActiveDocument() 6322 { 6323 sourceActiveDocument(false); 6324 } 6325 6326 @Handler onSourceActiveDocumentWithEcho()6327 void onSourceActiveDocumentWithEcho() 6328 { 6329 sourceActiveDocument(true); 6330 } 6331 6332 @Handler onSourceAsJob()6333 void onSourceAsJob() 6334 { 6335 saveThenExecute(null, true, () -> 6336 { 6337 events_.fireEvent(new JobRunScriptEvent(getPath())); 6338 }); 6339 } 6340 6341 @Handler onSourceAsLauncherJob()6342 public void onSourceAsLauncherJob() 6343 { 6344 saveThenExecute(null, true, () -> 6345 { 6346 events_.fireEvent(new LauncherJobRunScriptEvent(getPath())); 6347 }); 6348 } 6349 6350 @Handler onProfileCode()6351 void onProfileCode() 6352 { 6353 dependencyManager_.withProfvis("The profiler", new Command() 6354 { 6355 @Override 6356 public void execute() 6357 { 6358 withVisualModeSelection(() -> 6359 { 6360 codeExecution_.executeSelection(true, true, "profvis::profvis", true); 6361 }); 6362 } 6363 }); 6364 } 6365 sourceActiveDocument(final boolean echo)6366 private void sourceActiveDocument(final boolean echo) 6367 { 6368 if (!isVisualEditorActive()) 6369 { 6370 docDisplay_.focus(); 6371 } 6372 6373 // If this is a Python file, use reticulate. 6374 if (fileType_.isPython()) 6375 { 6376 sourcePython(); 6377 return; 6378 } 6379 6380 if (fileType_.isR()) 6381 { 6382 if (extendedType_.startsWith(SourceDocument.XT_SHINY_PREFIX)) 6383 { 6384 // If the document being sourced is a Shiny file, run the app instead. 6385 runShinyApp(); 6386 return; 6387 } 6388 else if (extendedType_ == SourceDocument.XT_PLUMBER_API) 6389 { 6390 // If the document being sourced in a Plumber file, run the API instead. 6391 runPlumberAPI(); 6392 return; 6393 } 6394 } 6395 6396 // if the document is an R Markdown notebook, run all its chunks instead 6397 if (fileType_.isRmd() && isRmdNotebook()) 6398 { 6399 onExecuteAllCode(); 6400 return; 6401 } 6402 6403 // If the document being sourced is a script then use that codepath 6404 if (fileType_.isScript()) 6405 { 6406 runScript(); 6407 return; 6408 } 6409 6410 // If the document is previewable 6411 if (fileType_.canPreviewFromR()) 6412 { 6413 previewFromR(); 6414 return; 6415 } 6416 6417 String code = docDisplay_.getCode(); 6418 if (code != null && code.trim().length() > 0) 6419 { 6420 boolean sweave = 6421 fileType_.canCompilePDF() || 6422 fileType_.canKnitToHTML() || 6423 fileType_.isRpres(); 6424 6425 RnwWeave rnwWeave = compilePdfHelper_.getActiveRnwWeave(); 6426 final boolean forceEcho = sweave && (rnwWeave != null) ? rnwWeave.forceEchoOnExec() : false; 6427 6428 // NOTE: we always set echo to true for knitr because knitr doesn't 6429 // require print statements so if you don't echo to the console 6430 // then you don't see any of the output 6431 6432 boolean saveWhenSourcing = fileType_.isCpp() || 6433 docDisplay_.hasBreakpoints() || (prefs_.saveBeforeSourcing().getValue() && (getPath() != null) && !sweave); 6434 6435 if ((dirtyState_.getValue() || sweave) && !saveWhenSourcing) 6436 { 6437 server_.saveActiveDocument(code, 6438 sweave, 6439 compilePdfHelper_.getActiveRnwWeaveName(), 6440 new SimpleRequestCallback<Void>() { 6441 @Override 6442 public void onResponseReceived(Void response) 6443 { 6444 consoleDispatcher_.executeSourceCommand( 6445 "~/.active-rstudio-document", 6446 fileType_, 6447 "UTF-8", 6448 activeCodeIsAscii(), 6449 forceEcho ? true : echo, 6450 prefs_.focusConsoleAfterExec().getValue(), 6451 docDisplay_.hasBreakpoints()); 6452 } 6453 }); 6454 } 6455 else 6456 { 6457 Command sourceCommand = new Command() { 6458 @Override 6459 public void execute() 6460 { 6461 executeRSourceCommand(forceEcho ? true : echo, 6462 prefs_.focusConsoleAfterExec().getValue()); 6463 } 6464 }; 6465 6466 if (saveWhenSourcing && (dirtyState_.getValue() || (getPath() == null))) 6467 saveThenExecute(null, true, sourceCommand); 6468 else 6469 sourceCommand.execute(); 6470 } 6471 } 6472 6473 // update pref if necessary 6474 if (prefs_.sourceWithEcho().getValue() != echo) 6475 { 6476 prefs_.sourceWithEcho().setGlobalValue(echo, true); 6477 prefs_.writeUserPrefs(); 6478 } 6479 } 6480 runShinyApp()6481 private void runShinyApp() 6482 { 6483 source_.withSaveFilesBeforeCommand(() -> 6484 { 6485 events_.fireEvent(new LaunchShinyApplicationEvent(getPath(), 6486 prefs_.shinyBackgroundJobs().getValue() ? 6487 ShinyApplication.BACKGROUND_APP : 6488 ShinyApplication.FOREGROUND_APP, getExtendedFileType())); 6489 }, () -> {}, "Run Shiny Application"); 6490 } 6491 runPlumberAPI()6492 private void runPlumberAPI() 6493 { 6494 source_.withSaveFilesBeforeCommand(new Command() { 6495 @Override 6496 public void execute() 6497 { 6498 events_.fireEvent(new LaunchPlumberAPIEvent(getPath())); 6499 } 6500 }, () -> {}, "Run Plumber API"); 6501 } 6502 sourcePython()6503 private void sourcePython() 6504 { 6505 saveThenExecute(null, true, () -> { 6506 dependencyManager_.withReticulate( 6507 "Executing Python", 6508 "Sourcing Python scripts", 6509 () -> { 6510 String command = "reticulate::source_python('" + getPath() + "')"; 6511 events_.fireEvent(new SendToConsoleEvent(command, true)); 6512 }); 6513 }); 6514 } 6515 runScript()6516 private void runScript() 6517 { 6518 saveThenExecute(null, true, new Command() { 6519 @Override 6520 public void execute() 6521 { 6522 String interpreter = fileType_.getScriptInterpreter(); 6523 server_.getScriptRunCommand( 6524 interpreter, 6525 getPath(), 6526 new SimpleRequestCallback<String>() { 6527 @Override 6528 public void onResponseReceived(String cmd) 6529 { 6530 events_.fireEvent(new SendToConsoleEvent(cmd, true)); 6531 } 6532 }); 6533 } 6534 }); 6535 } 6536 previewFromR()6537 private void previewFromR() 6538 { 6539 saveThenExecute(null, true, new Command() { 6540 @Override 6541 public void execute() 6542 { 6543 server_.getMinimalSourcePath( 6544 getPath(), 6545 new SimpleRequestCallback<String>() { 6546 @Override 6547 public void onResponseReceived(String path) 6548 { 6549 String cmd = fileType_.createPreviewCommand(path); 6550 if (cmd != null) 6551 events_.fireEvent(new SendToConsoleEvent(cmd, true)); 6552 } 6553 }); 6554 } 6555 }); 6556 } 6557 activeCodeIsAscii()6558 private boolean activeCodeIsAscii() 6559 { 6560 String code = docDisplay_.getCode(); 6561 for (int i=0; i< code.length(); i++) 6562 { 6563 if (code.charAt(i) > 127) 6564 return false; 6565 } 6566 6567 return true; 6568 } 6569 6570 @Handler onExecuteLastCode()6571 void onExecuteLastCode() 6572 { 6573 withVisualModeSelection(() -> 6574 { 6575 if (!isVisualEditorActive()) 6576 { 6577 docDisplay_.focus(); 6578 } 6579 6580 codeExecution_.executeLastCode(); 6581 }); 6582 } 6583 6584 @Handler onKnitDocument()6585 void onKnitDocument() 6586 { 6587 onPreviewHTML(); 6588 } 6589 6590 @Handler onQuartoRenderDocument()6591 void onQuartoRenderDocument() 6592 { 6593 renderRmd(); 6594 } 6595 6596 @Handler onRunDocumentFromServerDotR()6597 void onRunDocumentFromServerDotR() 6598 { 6599 SourceColumn column = view_.getSourceColumn(); 6600 EditingTarget runTarget = column.shinyRunDocumentEditor(docUpdateSentinel_.getPath()); 6601 if (runTarget != null) 6602 { 6603 Command renderCommand = new Command() 6604 { 6605 @Override 6606 public void execute() 6607 { 6608 rmarkdownHelper_.renderRMarkdown( 6609 runTarget.getPath(), 6610 1, 6611 null, 6612 "UTF-8", 6613 null, 6614 false, 6615 RmdOutput.TYPE_SHINY, 6616 false, 6617 null, 6618 null); 6619 } 6620 }; 6621 6622 final Command saveCommand = new Command() 6623 { 6624 @Override 6625 public void execute() 6626 { 6627 saveThenExecute(null, true, renderCommand); 6628 } 6629 }; 6630 6631 // save before rendering if the document is dirty or has never been saved; 6632 // otherwise render directly 6633 Command command = 6634 docUpdateSentinel_.getPath() == null || dirtyState_.getValue() ? 6635 saveCommand : renderCommand; 6636 command.execute(); 6637 6638 } 6639 } 6640 6641 @Handler onPreviewHTML()6642 void onPreviewHTML() 6643 { 6644 // last ditch extended type detection 6645 String extendedType = extendedType_; 6646 extendedType = rmarkdownHelper_.detectExtendedType(docDisplay_.getCode(), 6647 extendedType, 6648 fileType_); 6649 6650 if (extendedType.startsWith(SourceDocument.XT_RMARKDOWN_PREFIX)) 6651 { 6652 renderRmd(); 6653 } 6654 else if (fileType_.isRd()) 6655 previewRd(); 6656 else if (fileType_.isRpres()) 6657 previewRpresentation(); 6658 else if (fileType_.isR()) 6659 onCompileNotebook(); 6660 else 6661 previewHTML(); 6662 } 6663 previewRpresentation()6664 void previewRpresentation() 6665 { 6666 SessionInfo sessionInfo = session_.getSessionInfo(); 6667 if (!fileTypeCommands_.getHTMLCapabiliites().isRMarkdownSupported()) 6668 { 6669 globalDisplay_.showMessage( 6670 MessageDisplay.MSG_WARNING, 6671 "Unable to Preview", 6672 "R Presentations require the knitr package " + 6673 "(version 1.2 or higher)"); 6674 return; 6675 } 6676 6677 PresentationState state = sessionInfo.getPresentationState(); 6678 6679 // if this presentation is already showing then just activate 6680 if (state.isActive() && 6681 state.getFilePath().equals(docUpdateSentinel_.getPath())) 6682 { 6683 commands_.activatePresentation().execute(); 6684 save(); 6685 } 6686 // otherwise reload 6687 else 6688 { 6689 saveThenExecute(null, true, new Command() { 6690 @Override 6691 public void execute() 6692 { 6693 server_.showPresentationPane(docUpdateSentinel_.getPath(), 6694 new VoidServerRequestCallback()); 6695 } 6696 6697 }); 6698 } 6699 } 6700 6701 previewRd()6702 void previewRd() 6703 { 6704 saveThenExecute(null, true, new Command() { 6705 @Override 6706 public void execute() 6707 { 6708 String previewURL = "help/preview?file="; 6709 previewURL += URL.encodeQueryString(docUpdateSentinel_.getPath()); 6710 events_.fireEvent(new ShowHelpEvent(previewURL)); 6711 } 6712 }); 6713 } 6714 previewJS()6715 void previewJS() 6716 { 6717 verifyD3Prequisites(new Command() { 6718 @Override 6719 public void execute() 6720 { 6721 saveThenExecute(null, true, new Command() { 6722 @Override 6723 public void execute() 6724 { 6725 jsHelper_.previewJS(TextEditingTarget.this); 6726 } 6727 }); 6728 } 6729 }); 6730 } 6731 previewSql()6732 void previewSql() 6733 { 6734 verifySqlPrerequisites(new Command() { 6735 @Override 6736 public void execute() 6737 { 6738 saveThenExecute(null, true, new Command() { 6739 @Override 6740 public void execute() 6741 { 6742 sqlHelper_.previewSql(TextEditingTarget.this); 6743 } 6744 }); 6745 } 6746 }); 6747 } 6748 customSource()6749 boolean customSource() 6750 { 6751 return rHelper_.customSource(TextEditingTarget.this); 6752 } 6753 renderRmd()6754 void renderRmd() 6755 { 6756 renderRmd(null); 6757 } 6758 renderRmd(final String paramsFile)6759 void renderRmd(final String paramsFile) 6760 { 6761 if (extendedType_ != SourceDocument.XT_QUARTO_DOCUMENT) 6762 events_.fireEvent(new RmdRenderPendingEvent(docUpdateSentinel_.getId())); 6763 6764 final int type = isShinyDoc() ? RmdOutput.TYPE_SHINY: 6765 isRmdNotebook() ? RmdOutput.TYPE_NOTEBOOK: 6766 RmdOutput.TYPE_STATIC; 6767 final Command renderCommand = new Command() 6768 { 6769 @Override 6770 public void execute() 6771 { 6772 boolean asTempfile = isPackageDocumentationFile(); 6773 String viewerType = RmdEditorOptions.getString( 6774 getRmdFrontMatter(), RmdEditorOptions.PREVIEW_IN, null); 6775 6776 // if visual mode is active, move the cursor in source mode to 6777 // match its position in visual mode, so that we pass the correct 6778 // line number hint to render below 6779 if (isVisualEditorActive()) 6780 { 6781 visualMode_.syncSourceOutlineLocation(); 6782 } 6783 6784 // Command we can use to do an R Markdown render 6785 Command renderCmd = new Command() { 6786 @Override 6787 public void execute() 6788 { 6789 rmarkdownHelper_.renderRMarkdown( 6790 docUpdateSentinel_.getPath(), 6791 docDisplay_.getCursorPosition().getRow() + 1, 6792 null, 6793 docUpdateSentinel_.getEncoding(), 6794 paramsFile, 6795 asTempfile, 6796 type, 6797 false, 6798 rmarkdownHelper_.getKnitWorkingDir(docUpdateSentinel_), 6799 viewerType); 6800 6801 } 6802 6803 }; 6804 6805 6806 // see if we should be using quarto preview 6807 String quartoFormat = useQuartoPreview(); 6808 if (quartoFormat != null) 6809 { 6810 // quarto preview can reject the preview (e.g. if it turns 6811 // out this file is part of a website or book project) 6812 server_.quartoPreview(docUpdateSentinel_.getPath(), 6813 quartoFormat, 6814 new SimpleRequestCallback<Boolean>() { 6815 @Override 6816 public void onResponseReceived(Boolean previewed) 6817 { 6818 if (!previewed) 6819 { 6820 renderCmd.execute(); 6821 } 6822 } 6823 }); 6824 } 6825 else 6826 { 6827 renderCmd.execute(); 6828 } 6829 } 6830 }; 6831 6832 final Command saveCommand = new Command() 6833 { 6834 @Override 6835 public void execute() 6836 { 6837 saveThenExecute(null, true, renderCommand); 6838 } 6839 }; 6840 6841 // save before rendering if the document is dirty or has never been saved; 6842 // otherwise render directly 6843 Command command = 6844 docUpdateSentinel_.getPath() == null || dirtyState_.getValue() ? 6845 saveCommand : renderCommand; 6846 6847 if (isRmdNotebook()) 6848 dependencyManager_.withRMarkdown("Creating R Notebooks", command); 6849 else 6850 command.execute(); 6851 } 6852 6853 isRmdNotebook()6854 public boolean isRmdNotebook() 6855 { 6856 List<String> outputFormats = getOutputFormats(); 6857 return outputFormats.size() > 0 && 6858 outputFormats.get(0) == RmdOutputFormat.OUTPUT_HTML_NOTEBOOK; 6859 } 6860 hasRmdNotebook()6861 public boolean hasRmdNotebook() 6862 { 6863 List<String> outputFormats = getOutputFormats(); 6864 for (String format: outputFormats) 6865 { 6866 if (format == RmdOutputFormat.OUTPUT_HTML_NOTEBOOK) 6867 return true; 6868 } 6869 return false; 6870 } 6871 isShinyDoc()6872 private boolean isShinyDoc() 6873 { 6874 try 6875 { 6876 String yaml = getRmdFrontMatter(); 6877 if (yaml == null) 6878 return false; 6879 return rmarkdownHelper_.isRuntimeShiny(yaml); 6880 } 6881 catch(Exception e) 6882 { 6883 Debug.log(e.getMessage()); 6884 return false; 6885 } 6886 } 6887 getCustomKnit()6888 private String getCustomKnit() 6889 { 6890 try 6891 { 6892 String yaml = getRmdFrontMatter(); 6893 if (yaml == null) 6894 return ""; 6895 return rmarkdownHelper_.getCustomKnit(yaml); 6896 } 6897 catch(Exception e) 6898 { 6899 Debug.log(e.getMessage()); 6900 return ""; 6901 } 6902 } 6903 6904 useQuartoPreview()6905 private String useQuartoPreview() 6906 { 6907 if (session_.getSessionInfo().getQuartoConfig().installed && 6908 (extendedType_ == SourceDocument.XT_QUARTO_DOCUMENT) && 6909 !isShinyDoc() && !isRmdNotebook() && !isQuartoWebsiteDoc()) 6910 { 6911 List<String> outputFormats = getQuartoOutputFormats(); 6912 if (outputFormats.size() == 0) 6913 { 6914 return "html"; 6915 } 6916 else 6917 { 6918 String format = outputFormats.get(0); 6919 final ArrayList<String> previewFormats = new ArrayList<String>( 6920 Arrays.asList("pdf", "beamer", "html", "revealjs", "slidy")); 6921 return previewFormats.stream() 6922 .filter(fmt -> format.startsWith(fmt)) 6923 .findAny() 6924 .orElse(null); 6925 6926 } 6927 } 6928 else 6929 { 6930 return null; 6931 } 6932 6933 } 6934 isQuartoWebsiteDoc()6935 private boolean isQuartoWebsiteDoc() 6936 { 6937 QuartoConfig config = session_.getSessionInfo().getQuartoConfig(); 6938 return QuartoHelper.isQuartoWebsiteDoc(docUpdateSentinel_.getPath(), config); 6939 } 6940 6941 previewHTML()6942 void previewHTML() 6943 { 6944 // validate pre-reqs 6945 if (!rmarkdownHelper_.verifyPrerequisites(view_, fileType_)) 6946 return; 6947 6948 doHtmlPreview(new Provider<HTMLPreviewParams>() 6949 { 6950 @Override 6951 public HTMLPreviewParams get() 6952 { 6953 return HTMLPreviewParams.create(docUpdateSentinel_.getPath(), 6954 docUpdateSentinel_.getEncoding(), 6955 fileType_.isMarkdown(), 6956 fileType_.requiresKnit(), 6957 false); 6958 } 6959 }); 6960 } 6961 doHtmlPreview(final Provider<HTMLPreviewParams> pParams)6962 private void doHtmlPreview(final Provider<HTMLPreviewParams> pParams) 6963 { 6964 // command to show the preview window 6965 final Command showPreviewWindowCommand = new Command() { 6966 @Override 6967 public void execute() 6968 { 6969 HTMLPreviewParams params = pParams.get(); 6970 events_.fireEvent(new ShowHTMLPreviewEvent(params)); 6971 } 6972 }; 6973 6974 // command to run the preview 6975 final Command runPreviewCommand = new Command() { 6976 @Override 6977 public void execute() 6978 { 6979 final HTMLPreviewParams params = pParams.get(); 6980 server_.previewHTML(params, new SimpleRequestCallback<>()); 6981 } 6982 }; 6983 6984 if (pParams.get().isNotebook()) 6985 { 6986 saveThenExecute(null, true, new Command() 6987 { 6988 @Override 6989 public void execute() 6990 { 6991 generateNotebook(new Command() 6992 { 6993 @Override 6994 public void execute() 6995 { 6996 showPreviewWindowCommand.execute(); 6997 runPreviewCommand.execute(); 6998 } 6999 }); 7000 } 7001 }); 7002 } 7003 // if the document is new and unsaved, then resolve that and then 7004 // show the preview window -- it won't activate in web mode 7005 // due to popup activation rules but at least it will show up 7006 else if (isNewDoc()) 7007 { 7008 saveThenExecute(null, true, CommandUtil.join(showPreviewWindowCommand, 7009 runPreviewCommand)); 7010 } 7011 // otherwise if it's dirty then show the preview window first (to 7012 // beat the popup blockers) then save & run 7013 else if (dirtyState().getValue()) 7014 { 7015 showPreviewWindowCommand.execute(); 7016 saveThenExecute(null, true, runPreviewCommand); 7017 } 7018 // otherwise show the preview window then run the preview 7019 else 7020 { 7021 showPreviewWindowCommand.execute(); 7022 runPreviewCommand.execute(); 7023 } 7024 } 7025 generateNotebook(final Command executeOnSuccess)7026 private void generateNotebook(final Command executeOnSuccess) 7027 { 7028 // default title 7029 String defaultTitle = docUpdateSentinel_.getProperty(NOTEBOOK_TITLE); 7030 if (StringUtil.isNullOrEmpty(defaultTitle)) 7031 defaultTitle = FileSystemItem.getNameFromPath(docUpdateSentinel_.getPath()); 7032 7033 // default author 7034 String defaultAuthor = docUpdateSentinel_.getProperty(NOTEBOOK_AUTHOR); 7035 if (StringUtil.isNullOrEmpty(defaultAuthor)) 7036 { 7037 defaultAuthor = state_.compileRNotebookPrefs().getValue().getAuthor(); 7038 if (StringUtil.isNullOrEmpty(defaultAuthor)) 7039 defaultAuthor = session_.getSessionInfo().getUserIdentity(); 7040 } 7041 7042 // default type 7043 String defaultType = docUpdateSentinel_.getProperty(NOTEBOOK_TYPE); 7044 if (StringUtil.isNullOrEmpty(defaultType)) 7045 { 7046 defaultType = state_.compileRNotebookPrefs().getValue().getType(); 7047 if (StringUtil.isNullOrEmpty(defaultType)) 7048 defaultType = CompileNotebookOptions.TYPE_DEFAULT; 7049 } 7050 7051 CompileNotebookOptionsDialog dialog = new CompileNotebookOptionsDialog( 7052 getId(), 7053 defaultTitle, 7054 defaultAuthor, 7055 defaultType, 7056 new OperationWithInput<CompileNotebookOptions>() 7057 { 7058 @Override 7059 public void execute(CompileNotebookOptions input) 7060 { 7061 server_.createNotebook( 7062 input, 7063 new SimpleRequestCallback<CompileNotebookResult>() 7064 { 7065 @Override 7066 public void onResponseReceived(CompileNotebookResult response) 7067 { 7068 if (response.getSucceeded()) 7069 { 7070 executeOnSuccess.execute(); 7071 } 7072 else 7073 { 7074 globalDisplay_.showErrorMessage( 7075 "Unable to Compile Report", 7076 response.getFailureMessage()); 7077 } 7078 } 7079 }); 7080 7081 // save options for this document 7082 HashMap<String, String> changedProperties = new HashMap<>(); 7083 changedProperties.put(NOTEBOOK_TITLE, input.getNotebookTitle()); 7084 changedProperties.put(NOTEBOOK_AUTHOR, input.getNotebookAuthor()); 7085 changedProperties.put(NOTEBOOK_TYPE, input.getNotebookType()); 7086 docUpdateSentinel_.modifyProperties(changedProperties, null); 7087 7088 // save global prefs 7089 CompileNotebookPrefs prefs = CompileNotebookPrefs.create( 7090 input.getNotebookAuthor(), 7091 input.getNotebookType()); 7092 if (!CompileNotebookPrefs.areEqual( 7093 prefs, 7094 state_.compileRNotebookPrefs().getValue().cast())) 7095 { 7096 state_.compileRNotebookPrefs().setGlobalValue(prefs.cast()); 7097 state_.writeState(); 7098 } 7099 } 7100 } 7101 ); 7102 dialog.showModal(); 7103 } 7104 7105 @Handler onCompileNotebook()7106 void onCompileNotebook() 7107 { 7108 if (session_.getSessionInfo().getRMarkdownPackageAvailable()) 7109 { 7110 saveThenExecute(null, true, new Command() 7111 { 7112 @Override 7113 public void execute() 7114 { 7115 rmarkdownHelper_.renderNotebookv2(docUpdateSentinel_, null); 7116 } 7117 }); 7118 } 7119 else 7120 { 7121 if (!rmarkdownHelper_.verifyPrerequisites("Compile Report", 7122 view_, 7123 FileTypeRegistry.RMARKDOWN)) 7124 { 7125 return; 7126 } 7127 7128 doHtmlPreview(new Provider<HTMLPreviewParams>() 7129 { 7130 @Override 7131 public HTMLPreviewParams get() 7132 { 7133 return HTMLPreviewParams.create(docUpdateSentinel_.getPath(), 7134 docUpdateSentinel_.getEncoding(), 7135 true, 7136 true, 7137 true); 7138 } 7139 }); 7140 } 7141 } 7142 7143 @Handler onCompilePDF()7144 void onCompilePDF() 7145 { 7146 String pdfPreview = prefs_.pdfPreviewer().getValue(); 7147 boolean showPdf = !pdfPreview.equals(UserPrefs.PDF_PREVIEWER_NONE); 7148 boolean useInternalPreview = 7149 pdfPreview.equals(UserPrefs.PDF_PREVIEWER_RSTUDIO); 7150 boolean useDesktopSynctexPreview = 7151 pdfPreview.equals(UserPrefs.PDF_PREVIEWER_DESKTOP_SYNCTEX) && 7152 Desktop.isDesktop(); 7153 7154 String action = ""; 7155 if (showPdf && !useInternalPreview && !useDesktopSynctexPreview) 7156 action = "view_external"; 7157 7158 handlePdfCommand(action, useInternalPreview, null); 7159 } 7160 7161 7162 @Handler onKnitWithParameters()7163 void onKnitWithParameters() 7164 { 7165 saveThenExecute(null, true, new Command() { 7166 @Override 7167 public void execute() 7168 { 7169 rmarkdownHelper_.getRMarkdownParamsFile( 7170 docUpdateSentinel_.getPath(), 7171 docUpdateSentinel_.getEncoding(), 7172 activeCodeIsAscii(), 7173 new CommandWithArg<String>() { 7174 @Override 7175 public void execute(String paramsFile) 7176 { 7177 // null return means user cancelled 7178 if (paramsFile != null) 7179 { 7180 // special "none" value means no parameters 7181 if (paramsFile.equals("none")) 7182 { 7183 new RMarkdownNoParamsDialog().showModal(); 7184 } 7185 else 7186 { 7187 renderRmd(paramsFile); 7188 } 7189 } 7190 } 7191 }); 7192 } 7193 }); 7194 } 7195 7196 @Handler onClearKnitrCache()7197 void onClearKnitrCache() 7198 { 7199 withSavedDoc(new Command() { 7200 @Override 7201 public void execute() 7202 { 7203 // determine the cache path (use relative path if possible) 7204 String path = docUpdateSentinel_.getPath(); 7205 FileSystemItem fsi = FileSystemItem.createFile(path); 7206 path = fsi.getParentPath().completePath(fsi.getStem() + "_cache"); 7207 String relativePath = FileSystemItem.createFile(path).getPathRelativeTo( 7208 workbenchContext_.getCurrentWorkingDir()); 7209 if (relativePath != null) 7210 path = relativePath; 7211 final String docPath = path; 7212 7213 globalDisplay_.showYesNoMessage( 7214 MessageDialog.QUESTION, 7215 "Clear Knitr Cache", 7216 "Clearing the Knitr cache will delete the cache " + 7217 "directory for " + docPath + ". " + 7218 "\n\nAre you sure you want to clear the cache now?", 7219 false, 7220 new Operation() { 7221 @Override 7222 public void execute() 7223 { 7224 String code = "unlink(" + 7225 ConsoleDispatcher.escapedPath(docPath) + 7226 ", recursive = TRUE)"; 7227 events_.fireEvent(new SendToConsoleEvent(code, true)); 7228 } 7229 }, 7230 null, 7231 true); 7232 7233 } 7234 7235 }); 7236 7237 7238 } 7239 7240 7241 @Handler onClearPrerenderedOutput()7242 void onClearPrerenderedOutput() 7243 { 7244 withSavedDoc(new Command() { 7245 @Override 7246 public void execute() 7247 { 7248 // determine the output path (use relative path if possible) 7249 String path = docUpdateSentinel_.getPath(); 7250 String relativePath = FileSystemItem.createFile(path).getPathRelativeTo( 7251 workbenchContext_.getCurrentWorkingDir()); 7252 if (relativePath != null) 7253 path = relativePath; 7254 final String docPath = path; 7255 7256 globalDisplay_.showYesNoMessage( 7257 MessageDialog.QUESTION, 7258 "Clear Prerendered Output", 7259 "This will remove all previously generated output " + 7260 "for " + docPath + " (html, prerendered data, knitr cache, etc.)." + 7261 "\n\nAre you sure you want to clear the output now?", 7262 false, 7263 new Operation() { 7264 @Override 7265 public void execute() 7266 { 7267 String code = "rmarkdown::shiny_prerendered_clean(" + 7268 ConsoleDispatcher.escapedPath(docPath) + 7269 ")"; 7270 events_.fireEvent(new SendToConsoleEvent(code, true)); 7271 } 7272 }, 7273 null, 7274 true); 7275 } 7276 }); 7277 } 7278 7279 7280 @Handler onSynctexSearch()7281 void onSynctexSearch() 7282 { 7283 doSynctexSearch(true); 7284 } 7285 doSynctexSearch(boolean fromClick)7286 private void doSynctexSearch(boolean fromClick) 7287 { 7288 SourceLocation sourceLocation = getSelectionAsSourceLocation(fromClick); 7289 if (sourceLocation == null) 7290 return; 7291 7292 // compute the target pdf 7293 FileSystemItem editorFile = FileSystemItem.createFile( 7294 docUpdateSentinel_.getPath()); 7295 FileSystemItem targetFile = compilePdfHelper_.getTargetFile(editorFile); 7296 String pdfFile = 7297 targetFile.getParentPath().completePath(targetFile.getStem() + ".pdf"); 7298 7299 synctex_.forwardSearch(pdfFile, sourceLocation); 7300 } 7301 7302 getSelectionAsSourceLocation(boolean fromClick)7303 private SourceLocation getSelectionAsSourceLocation(boolean fromClick) 7304 { 7305 // get doc path (bail if the document is unsaved) 7306 String file = docUpdateSentinel_.getPath(); 7307 if (file == null) 7308 return null; 7309 7310 Position selPos = docDisplay_.getSelectionStart(); 7311 int line = selPos.getRow() + 1; 7312 int column = selPos.getColumn() + 1; 7313 return SourceLocation.create(file, line, column, fromClick); 7314 } 7315 7316 @Handler onQuickAddNext()7317 void onQuickAddNext() 7318 { 7319 docDisplay_.quickAddNext(); 7320 } 7321 getFindReplace()7322 private HasFindReplace getFindReplace() 7323 { 7324 if (visualMode_.isActivated()) 7325 return visualMode_.getFindReplace(); 7326 else 7327 return view_; 7328 } 7329 7330 @Handler onFindReplace()7331 void onFindReplace() 7332 { 7333 getFindReplace().showFindReplace(true); 7334 } 7335 7336 @Handler onFindNext()7337 void onFindNext() 7338 { 7339 getFindReplace().findNext(); 7340 } 7341 7342 @Handler onFindPrevious()7343 void onFindPrevious() 7344 { 7345 getFindReplace().findPrevious(); 7346 } 7347 7348 @Handler onFindSelectAll()7349 void onFindSelectAll() 7350 { 7351 view_.findSelectAll(); 7352 } 7353 7354 @Handler onFindFromSelection()7355 void onFindFromSelection() 7356 { 7357 if (visualMode_.isActivated()) { 7358 ensureVisualModeActive(() -> { 7359 visualMode_.getFindReplace().findFromSelection(visualMode_.getSelectedText()); 7360 }); 7361 } else { 7362 withActiveEditor((disp) -> 7363 { 7364 view_.findFromSelection(disp.getSelectionValue()); 7365 disp.focus(); 7366 }); 7367 } 7368 7369 } 7370 7371 @Handler onReplaceAndFind()7372 void onReplaceAndFind() 7373 { 7374 getFindReplace().replaceAndFind(); 7375 } 7376 7377 @Override search(String regex)7378 public Position search(String regex) 7379 { 7380 return search(Position.create(0, 0), regex); 7381 } 7382 7383 @Override search(Position startPos, String regex)7384 public Position search(Position startPos, String regex) 7385 { 7386 InputEditorSelection sel = docDisplay_.search(regex, 7387 false, 7388 false, 7389 false, 7390 false, 7391 startPos, 7392 null, 7393 true); 7394 if (sel != null) 7395 return docDisplay_.selectionToPosition(sel.getStart()); 7396 else 7397 return null; 7398 } 7399 7400 7401 @Handler onFold()7402 void onFold() 7403 { 7404 if (useScopeTreeFolding()) 7405 { 7406 Range range = Range.fromPoints(docDisplay_.getSelectionStart(), 7407 docDisplay_.getSelectionEnd()); 7408 if (range.isEmpty()) 7409 { 7410 // If no selection, fold the innermost non-anonymous scope 7411 Scope scope = docDisplay_.getCurrentScope(); 7412 while (scope != null && scope.isAnon()) 7413 scope = scope.getParentScope(); 7414 7415 if (scope == null || scope.isTopLevel()) 7416 return; 7417 7418 docDisplay_.addFoldFromRow(scope.getFoldStart().getRow()); 7419 } 7420 else 7421 { 7422 // If selection, fold the selection 7423 docDisplay_.addFold(range); 7424 } 7425 } 7426 else 7427 { 7428 int row = docDisplay_.getSelectionStart().getRow(); 7429 docDisplay_.addFoldFromRow(row); 7430 } 7431 } 7432 7433 @Handler onUnfold()7434 void onUnfold() 7435 { 7436 if (useScopeTreeFolding()) 7437 { 7438 Range range = Range.fromPoints(docDisplay_.getSelectionStart(), 7439 docDisplay_.getSelectionEnd()); 7440 if (range.isEmpty()) 7441 { 7442 // If no selection, either: 7443 // 7444 // 1) Unfold a fold containing the cursor, or 7445 // 2) Unfold the closest fold on the current row. 7446 Position pos = docDisplay_.getCursorPosition(); 7447 7448 AceFold containingCandidate = null; 7449 AceFold startCandidate = null; 7450 AceFold endCandidate = null; 7451 7452 for (AceFold f : JsUtil.asIterable(docDisplay_.getFolds())) 7453 { 7454 // Check to see whether this fold contains the cursor position. 7455 if (f.getRange().contains(pos)) 7456 { 7457 if (containingCandidate == null || 7458 containingCandidate.getRange().contains(f.getRange())) 7459 { 7460 containingCandidate = f; 7461 } 7462 } 7463 7464 if (startCandidate == null 7465 && f.getStart().getRow() == pos.getRow() 7466 && f.getStart().getColumn() >= pos.getColumn()) 7467 { 7468 startCandidate = f; 7469 } 7470 7471 if (startCandidate == null && 7472 f.getEnd().getRow() == pos.getRow() && 7473 f.getEnd().getColumn() <= pos.getColumn()) 7474 { 7475 endCandidate = f; 7476 } 7477 } 7478 7479 if (containingCandidate != null) 7480 { 7481 docDisplay_.unfold(containingCandidate); 7482 } 7483 else if (startCandidate == null ^ endCandidate == null) 7484 { 7485 docDisplay_.unfold(startCandidate != null ? startCandidate 7486 : endCandidate); 7487 } 7488 else if (startCandidate != null && endCandidate != null) 7489 { 7490 // Both are candidates; see which is closer 7491 int startDelta = startCandidate.getStart().getColumn() - pos.getColumn(); 7492 int endDelta = pos.getColumn() - endCandidate.getEnd().getColumn(); 7493 docDisplay_.unfold(startDelta <= endDelta? startCandidate 7494 : endCandidate); 7495 } 7496 } 7497 else 7498 { 7499 // If selection, unfold the selection 7500 7501 docDisplay_.unfold(range); 7502 } 7503 } 7504 else 7505 { 7506 int row = docDisplay_.getSelectionStart().getRow(); 7507 docDisplay_.unfold(row); 7508 } 7509 } 7510 7511 @Handler onFoldAll()7512 void onFoldAll() 7513 { 7514 if (useScopeTreeFolding()) 7515 { 7516 // Fold all except anonymous braces 7517 HashSet<Integer> rowsFolded = new HashSet<>(); 7518 for (AceFold f : JsUtil.asIterable(docDisplay_.getFolds())) 7519 rowsFolded.add(f.getStart().getRow()); 7520 7521 ScopeList scopeList = new ScopeList(docDisplay_); 7522 scopeList.removeAll(ScopeList.ANON_BRACE); 7523 for (Scope scope : scopeList) 7524 { 7525 int row = scope.getFoldStart().getRow(); 7526 if (!rowsFolded.contains(row)) 7527 docDisplay_.addFoldFromRow(row); 7528 } 7529 } 7530 else 7531 { 7532 docDisplay_.foldAll(); 7533 } 7534 } 7535 7536 @Handler onUnfoldAll()7537 void onUnfoldAll() 7538 { 7539 if (useScopeTreeFolding()) 7540 { 7541 for (AceFold f : JsUtil.asIterable(docDisplay_.getFolds())) 7542 docDisplay_.unfold(f); 7543 } 7544 else 7545 { 7546 docDisplay_.unfoldAll(); 7547 } 7548 } 7549 7550 @Handler onToggleEditorTokenInfo()7551 void onToggleEditorTokenInfo() 7552 { 7553 docDisplay_.toggleTokenInfo(); 7554 } 7555 useScopeTreeFolding()7556 boolean useScopeTreeFolding() 7557 { 7558 return docDisplay_.hasCodeModelScopeTree(); 7559 } 7560 handlePdfCommand(final String completedAction, final boolean useInternalPreview, final Command onBeforeCompile)7561 void handlePdfCommand(final String completedAction, 7562 final boolean useInternalPreview, 7563 final Command onBeforeCompile) 7564 { 7565 if (fileType_.isRnw() && prefs_.alwaysEnableRnwConcordance().getValue()) 7566 compilePdfHelper_.ensureRnwConcordance(); 7567 7568 // if the document has been previously saved then we should execute 7569 // the onBeforeCompile command immediately 7570 final boolean isNewDoc = isNewDoc(); 7571 if (!isNewDoc && (onBeforeCompile != null)) 7572 onBeforeCompile.execute(); 7573 7574 saveThenExecute(null, true, new Command() 7575 { 7576 public void execute() 7577 { 7578 // if this was a new doc then we still need to execute the 7579 // onBeforeCompile command 7580 if (isNewDoc && (onBeforeCompile != null)) 7581 onBeforeCompile.execute(); 7582 7583 String path = docUpdateSentinel_.getPath(); 7584 if (path != null) 7585 { 7586 String encoding = StringUtil.notNull( 7587 docUpdateSentinel_.getEncoding()); 7588 fireCompilePdfEvent(path, 7589 encoding, 7590 completedAction, 7591 useInternalPreview); 7592 } 7593 } 7594 }); 7595 } 7596 fireCompilePdfEvent(String path, String encoding, String completedAction, boolean useInternalPreview)7597 private void fireCompilePdfEvent(String path, 7598 String encoding, 7599 String completedAction, 7600 boolean useInternalPreview) 7601 { 7602 // first validate the path to make sure it doesn't contain spaces 7603 FileSystemItem file = FileSystemItem.createFile(path); 7604 if (file.getName().indexOf(' ') != -1) 7605 { 7606 globalDisplay_.showErrorMessage( 7607 "Invalid Filename", 7608 "The file '" + file.getName() + "' cannot be compiled to " + 7609 "a PDF because TeX does not understand paths with spaces. " + 7610 "If you rename the file to remove spaces then " + 7611 "PDF compilation will work correctly."); 7612 7613 return; 7614 } 7615 7616 CompilePdfEvent event = new CompilePdfEvent( 7617 compilePdfHelper_.getTargetFile(file), 7618 encoding, 7619 getSelectionAsSourceLocation(false), 7620 completedAction, 7621 useInternalPreview); 7622 events_.fireEvent(event); 7623 } 7624 postSaveCommand()7625 private Command postSaveCommand() 7626 { 7627 return new Command() 7628 { 7629 public void execute() 7630 { 7631 // fire source document saved event 7632 FileSystemItem file = FileSystemItem.createFile( 7633 docUpdateSentinel_.getPath()); 7634 events_.fireEvent(new SourceFileSaveCompletedEvent( 7635 file, 7636 docUpdateSentinel_.getContents(), 7637 docDisplay_.getCursorPosition())); 7638 7639 // check for source on save 7640 if (isSourceOnSaveEnabled() && docUpdateSentinel_.sourceOnSave()) 7641 { 7642 if (fileType_.isRd()) 7643 { 7644 previewRd(); 7645 } 7646 else if (fileType_.isJS()) 7647 { 7648 if (extendedType_ == SourceDocument.XT_JS_PREVIEWABLE) 7649 previewJS(); 7650 } 7651 else if (fileType_.isSql()) 7652 { 7653 if (extendedType_ == SourceDocument.XT_SQL_PREVIEWABLE) 7654 previewSql(); 7655 } 7656 else if (fileType_.canPreviewFromR()) 7657 { 7658 previewFromR(); 7659 } 7660 else if (extendedType_ == SourceDocument.XT_RMARKDOWN_DOCUMENT || 7661 extendedType_ == SourceDocument.XT_QUARTO_DOCUMENT) 7662 { 7663 renderRmd(); 7664 } 7665 else 7666 { 7667 executeRSourceCommand(false, false); 7668 } 7669 } 7670 } 7671 }; 7672 } 7673 executeRSourceCommand(boolean forceEcho, boolean focusAfterExec)7674 private void executeRSourceCommand(boolean forceEcho, boolean focusAfterExec) 7675 { 7676 // Hide breakpoint warning bar if visible (since we will re-evaluate 7677 // breakpoints after source) 7678 if (docDisplay_.hasBreakpoints()) 7679 { 7680 hideBreakpointWarningBar(); 7681 } 7682 7683 if (fileType_.isR() && extendedType_ == SourceDocument.XT_R_CUSTOM_SOURCE) 7684 { 7685 // If this R script looks like it has a custom source 7686 // command, try to execute it; if successful, we're done. 7687 if (customSource()) 7688 return; 7689 } 7690 7691 // Execute the R source() command 7692 consoleDispatcher_.executeSourceCommand( 7693 docUpdateSentinel_.getPath(), 7694 fileType_, 7695 docUpdateSentinel_.getEncoding(), 7696 activeCodeIsAscii(), 7697 forceEcho, 7698 focusAfterExec, 7699 docDisplay_.hasBreakpoints()); 7700 } 7701 checkForExternalEdit()7702 public void checkForExternalEdit() 7703 { 7704 if (!externalEditCheckInterval_.hasElapsed()) 7705 return; 7706 externalEditCheckInterval_.reset(); 7707 7708 externalEditCheckInvalidation_.invalidate(); 7709 7710 // If the doc has never been saved, don't even bother checking 7711 if (getPath() == null) 7712 return; 7713 7714 // If we're already waiting for the user to respond to an edit event, bail 7715 if (isWaitingForUserResponseToExternalEdit_) 7716 return; 7717 7718 final Invalidation.Token token = externalEditCheckInvalidation_.getInvalidationToken(); 7719 7720 server_.checkForExternalEdit( 7721 id_, 7722 new ServerRequestCallback<CheckForExternalEditResult>() 7723 { 7724 @Override 7725 public void onResponseReceived(CheckForExternalEditResult response) 7726 { 7727 if (token.isInvalid()) 7728 return; 7729 7730 if (response.isDeleted()) 7731 { 7732 if (ignoreDeletes_) 7733 return; 7734 7735 isWaitingForUserResponseToExternalEdit_ = true; 7736 globalDisplay_.showYesNoMessage( 7737 GlobalDisplay.MSG_WARNING, 7738 "File Deleted", 7739 "The file " + 7740 StringUtil.notNull(docUpdateSentinel_.getPath()) + 7741 " has been deleted or moved. " + 7742 "Do you want to close this file now?", 7743 false, 7744 new Operation() 7745 { 7746 public void execute() 7747 { 7748 isWaitingForUserResponseToExternalEdit_ = false; 7749 CloseEvent.fire(TextEditingTarget.this, null); 7750 } 7751 }, 7752 new Operation() 7753 { 7754 public void execute() 7755 { 7756 isWaitingForUserResponseToExternalEdit_ = false; 7757 externalEditCheckInterval_.reset(); 7758 ignoreDeletes_ = true; 7759 // Make sure it stays dirty 7760 dirtyState_.markDirty(false); 7761 } 7762 }, 7763 true 7764 ); 7765 } 7766 else if (response.isModified()) 7767 { 7768 // If we're in a collaborative session, we need to let it 7769 // reconcile the modification 7770 if (docDisplay_ != null && 7771 docDisplay_.hasActiveCollabSession() && 7772 response.getItem() != null) 7773 { 7774 events_.fireEvent(new CollabExternalEditEvent( 7775 getId(), getPath(), 7776 response.getItem().getLastModifiedNative())); 7777 return; 7778 } 7779 7780 ignoreDeletes_ = false; // Now we know it exists 7781 7782 // Use StringUtil.formatDate(response.getLastModified())? 7783 7784 if (!dirtyState_.getValue()) 7785 { 7786 revertEdits(); 7787 } 7788 else 7789 { 7790 externalEditCheckInterval_.reset(); 7791 isWaitingForUserResponseToExternalEdit_ = true; 7792 globalDisplay_.showYesNoMessage( 7793 GlobalDisplay.MSG_WARNING, 7794 "File Changed", 7795 "The file " + name_.getValue() + " has changed " + 7796 "on disk. Do you want to reload the file from " + 7797 "disk and discard your unsaved changes?", 7798 false, 7799 new Operation() 7800 { 7801 public void execute() 7802 { 7803 isWaitingForUserResponseToExternalEdit_ = false; 7804 revertEdits(); 7805 } 7806 }, 7807 new Operation() 7808 { 7809 public void execute() 7810 { 7811 isWaitingForUserResponseToExternalEdit_ = false; 7812 externalEditCheckInterval_.reset(); 7813 docUpdateSentinel_.ignoreExternalEdit(); 7814 // Make sure it stays dirty 7815 dirtyState_.markDirty(false); 7816 } 7817 }, 7818 true 7819 ); 7820 } 7821 } 7822 } 7823 7824 @Override 7825 public void onError(ServerError error) 7826 { 7827 Debug.logError(error); 7828 } 7829 }); 7830 } 7831 checkForExternalEdit(int delayMs)7832 public void checkForExternalEdit(int delayMs) 7833 { 7834 Scheduler.get().scheduleFixedDelay(new RepeatingCommand() 7835 { 7836 public boolean execute() 7837 { 7838 if (view_.isAttached()) 7839 checkForExternalEdit(); 7840 return false; 7841 } 7842 }, delayMs); 7843 } 7844 revertEdits()7845 private void revertEdits() 7846 { 7847 docUpdateSentinel_.revert(() -> { 7848 visualMode_.syncFromEditorIfActivated(); 7849 }, false); 7850 } 7851 toSourcePosition(Scope func)7852 private SourcePosition toSourcePosition(Scope func) 7853 { 7854 Position pos = func.getPreamble(); 7855 return SourcePosition.create(pos.getRow(), pos.getColumn()); 7856 } 7857 isCursorInTexMode(DocDisplay display)7858 private boolean isCursorInTexMode(DocDisplay display) 7859 { 7860 if (fileType_ instanceof TexFileType) 7861 return true; 7862 7863 if (fileType_.canCompilePDF()) 7864 { 7865 if (fileType_.isRnw()) 7866 { 7867 return SweaveFileType.TEX_LANG_MODE.equals( 7868 display.getLanguageMode(display.getCursorPosition())); 7869 } 7870 else 7871 { 7872 return true; 7873 } 7874 } 7875 else 7876 { 7877 return false; 7878 } 7879 } 7880 isCursorInRMode(DocDisplay display)7881 private boolean isCursorInRMode(DocDisplay display) 7882 { 7883 TextFileType type = display.getFileType(); 7884 if (type != null && type instanceof TexFileType) 7885 return false; 7886 7887 String mode = display.getLanguageMode(display.getCursorPosition()); 7888 if (mode == null) 7889 return true; 7890 7891 if (mode.equals(TextFileType.R_LANG_MODE)) 7892 return true; 7893 7894 return false; 7895 } 7896 isCursorInYamlMode(DocDisplay display)7897 private boolean isCursorInYamlMode(DocDisplay display) 7898 { 7899 String mode = display.getLanguageMode(display.getCursorPosition()); 7900 if (mode == null) 7901 return false; 7902 7903 if (mode.equals("YAML")) 7904 return true; 7905 7906 return false; 7907 } 7908 isNewDoc()7909 private boolean isNewDoc() 7910 { 7911 return docUpdateSentinel_.getPath() == null; 7912 } 7913 shouldEnforceHardTabs(FileSystemItem item)7914 public static boolean shouldEnforceHardTabs(FileSystemItem item) 7915 { 7916 if (item == null) 7917 return false; 7918 7919 String[] requiresHardTabs = new String[] { 7920 "Makefile", "Makefile.in", "Makefile.win", 7921 "Makevars", "Makevars.in", "Makevars.win" 7922 }; 7923 7924 for (String file : requiresHardTabs) 7925 if (file.equals(item.getName())) 7926 return true; 7927 7928 if (".tsv".equals(item.getExtension())) 7929 return true; 7930 7931 return false; 7932 } 7933 7934 private final CppCompletionContext cppCompletionContext_ = new CppCompletionContext() { 7935 @Override 7936 public boolean isCompletionEnabled() 7937 { 7938 return session_.getSessionInfo().getClangAvailable() && 7939 (docUpdateSentinel_.getPath() != null) && 7940 fileType_.isC(); 7941 } 7942 7943 @Override 7944 public void withUpdatedDoc(final CommandWith2Args<String, String> onUpdated) 7945 { 7946 docUpdateSentinel_.withSavedDoc(new Command() { 7947 @Override 7948 public void execute() 7949 { 7950 onUpdated.execute(docUpdateSentinel_.getPath(), 7951 docUpdateSentinel_.getId()); 7952 } 7953 }); 7954 7955 } 7956 7957 @Override 7958 public void cppCompletionOperation(final CppCompletionOperation operation) 7959 { 7960 if (isCompletionEnabled()) 7961 { 7962 withUpdatedDoc(new CommandWith2Args<String, String>() { 7963 @Override 7964 public void execute(String docPath, String docId) 7965 { 7966 Position pos = docDisplay_.getSelectionStart(); 7967 7968 operation.execute(docPath, 7969 pos.getRow() + 1, 7970 pos.getColumn() + 1); 7971 } 7972 }); 7973 } 7974 7975 } 7976 7977 @Override 7978 public String getDocPath() 7979 { 7980 if (docUpdateSentinel_ == null) 7981 return ""; 7982 7983 return docUpdateSentinel_.getPath(); 7984 } 7985 }; 7986 7987 private final CompletionContext rContext_ = new CompletionContext() { 7988 7989 @Override 7990 public String getPath() 7991 { 7992 if (docUpdateSentinel_ == null) 7993 return null; 7994 else 7995 return docUpdateSentinel_.getPath(); 7996 } 7997 7998 @Override 7999 public String getId() 8000 { 8001 if (docUpdateSentinel_ == null) 8002 return null; 8003 else 8004 return docUpdateSentinel_.getId(); 8005 } 8006 }; 8007 getRCompletionContext()8008 public CompletionContext getRCompletionContext() 8009 { 8010 return rContext_; 8011 } 8012 getCppCompletionContext()8013 public CppCompletionContext getCppCompletionContext() 8014 { 8015 return cppCompletionContext_; 8016 } 8017 getRnwCompletionContext()8018 public RnwCompletionContext getRnwCompletionContext() 8019 { 8020 return compilePdfHelper_; 8021 } 8022 syncFontSize( ArrayList<HandlerRegistration> releaseOnDismiss, EventBus events, final TextDisplay view, FontSizeManager fontSizeManager)8023 public static void syncFontSize( 8024 ArrayList<HandlerRegistration> releaseOnDismiss, 8025 EventBus events, 8026 final TextDisplay view, 8027 FontSizeManager fontSizeManager) 8028 { 8029 releaseOnDismiss.add(events.addHandler(ChangeFontSizeEvent.TYPE, changeFontSizeEvent -> 8030 { 8031 view.setFontSize(changeFontSizeEvent.getFontSize()); 8032 })); 8033 view.setFontSize(fontSizeManager.getSize()); 8034 8035 } 8036 onPrintSourceDoc(final DocDisplay docDisplay)8037 public static void onPrintSourceDoc(final DocDisplay docDisplay) 8038 { 8039 Scheduler.get().scheduleDeferred(new ScheduledCommand() 8040 { 8041 public void execute() 8042 { 8043 docDisplay.print(); 8044 } 8045 }); 8046 } 8047 addRecordNavigationPositionHandler( ArrayList<HandlerRegistration> releaseOnDismiss, final DocDisplay docDisplay, final EventBus events, final EditingTarget target)8048 public static void addRecordNavigationPositionHandler( 8049 ArrayList<HandlerRegistration> releaseOnDismiss, 8050 final DocDisplay docDisplay, 8051 final EventBus events, 8052 final EditingTarget target) 8053 { 8054 releaseOnDismiss.add(docDisplay.addRecordNavigationPositionHandler( 8055 new RecordNavigationPositionEvent.Handler() { 8056 @Override 8057 public void onRecordNavigationPosition( 8058 RecordNavigationPositionEvent event) 8059 { 8060 SourcePosition pos = SourcePosition.create( 8061 target.getContext(), 8062 event.getPosition().getRow(), 8063 event.getPosition().getColumn(), 8064 docDisplay.getScrollTop()); 8065 events.fireEvent(new SourceNavigationEvent( 8066 SourceNavigation.create( 8067 target.getId(), 8068 target.getPath(), 8069 pos))); 8070 } 8071 })); 8072 } 8073 screenCoordinatesToDocumentPosition(int pageX, int pageY)8074 public Position screenCoordinatesToDocumentPosition(int pageX, int pageY) 8075 { 8076 return docDisplay_.screenCoordinatesToDocumentPosition(pageX, pageY); 8077 } 8078 getDocDisplay()8079 public DocDisplay getDocDisplay() 8080 { 8081 return docDisplay_; 8082 } 8083 addAdditionalResourceFiles(ArrayList<String> additionalFiles)8084 private void addAdditionalResourceFiles(ArrayList<String> additionalFiles) 8085 { 8086 // it does--get the YAML front matter and modify it to include 8087 // the additional files named in the deployment 8088 String yaml = getRmdFrontMatter(); 8089 if (yaml == null) 8090 return; 8091 rmarkdownHelper_.addAdditionalResourceFiles(yaml, 8092 additionalFiles, 8093 new CommandWithArg<String>() 8094 { 8095 @Override 8096 public void execute(String yamlOut) 8097 { 8098 if (yamlOut != null) 8099 { 8100 applyRmdFrontMatter(yamlOut); 8101 } 8102 } 8103 }); 8104 } 8105 syncPublishPath(String path)8106 private void syncPublishPath(String path) 8107 { 8108 // if we have a view, a type, and a path, sync the view's content publish 8109 // path to the new content path--note that we need to do this even if the 8110 // document isn't currently of a publishable type, since it may become 8111 // publishable once saved. 8112 if (view_ != null && path != null) 8113 { 8114 view_.setPublishPath(extendedType_, path); 8115 } 8116 } 8117 setPreferredOutlineWidgetSize(double size)8118 public void setPreferredOutlineWidgetSize(double size) 8119 { 8120 state_.documentOutlineWidth().setGlobalValue((int) size); 8121 state_.writeState(); 8122 docUpdateSentinel_.setProperty(DOC_OUTLINE_SIZE, size + ""); 8123 } 8124 getPreferredOutlineWidgetSize()8125 public double getPreferredOutlineWidgetSize() 8126 { 8127 String property = docUpdateSentinel_.getProperty(DOC_OUTLINE_SIZE); 8128 if (StringUtil.isNullOrEmpty(property)) 8129 return state_.documentOutlineWidth().getGlobalValue(); 8130 8131 try { 8132 double value = Double.parseDouble(property); 8133 8134 // Don't allow too-small widget sizes. This helps to protect against 8135 // a user who might drag the outline width to just a few pixels, and 8136 // then toggle its visibility by clicking on the 'toggle outline' 8137 // button. It's unlikely that, realistically, any user would desire an 8138 // outline width less than ~30 pixels; at minimum we just need to 8139 // ensure they will be able to see + drag the widget to a larger 8140 // size if desired. 8141 if (value < 30) 8142 return 30; 8143 8144 return value; 8145 } catch (Exception e) { 8146 return state_.documentOutlineWidth().getGlobalValue(); 8147 } 8148 } 8149 setPreferredOutlineWidgetVisibility(boolean visible)8150 public void setPreferredOutlineWidgetVisibility(boolean visible) 8151 { 8152 docUpdateSentinel_.setProperty(DOC_OUTLINE_VISIBLE, visible ? "1" : "0"); 8153 } 8154 getPreferredOutlineWidgetVisibility()8155 public boolean getPreferredOutlineWidgetVisibility() 8156 { 8157 return getPreferredOutlineWidgetVisibility(prefs_.showDocOutlineRmd().getGlobalValue()); 8158 } 8159 getPreferredOutlineWidgetVisibility(boolean defaultValue)8160 public boolean getPreferredOutlineWidgetVisibility(boolean defaultValue) 8161 { 8162 String property = docUpdateSentinel_.getProperty(DOC_OUTLINE_VISIBLE); 8163 return StringUtil.isNullOrEmpty(property) 8164 ? (getTextFileType().isRmd() && defaultValue) 8165 : Integer.parseInt(property) > 0; 8166 } 8167 8168 // similar to get but will write the default value if it's used establishPreferredOutlineWidgetVisibility(boolean defaultValue)8169 public boolean establishPreferredOutlineWidgetVisibility(boolean defaultValue) 8170 { 8171 String property = docUpdateSentinel_.getProperty(DOC_OUTLINE_VISIBLE); 8172 if (!StringUtil.isNullOrEmpty(property)) 8173 { 8174 return Integer.parseInt(property) > 0; 8175 } 8176 else 8177 { 8178 boolean visible = getTextFileType().isRmd() && defaultValue; 8179 setPreferredOutlineWidgetVisibility(visible); 8180 return visible; 8181 } 8182 } 8183 isActiveDocument()8184 public boolean isActiveDocument() 8185 { 8186 return commandHandlerReg_ != null; 8187 } 8188 getStatusBar()8189 public StatusBar getStatusBar() 8190 { 8191 return statusBar_; 8192 } 8193 getNotebook()8194 public TextEditingTargetNotebook getNotebook() 8195 { 8196 return notebook_; 8197 } 8198 getVisualMode()8199 public VisualMode getVisualMode() 8200 { 8201 return visualMode_; 8202 } 8203 getCodeExecutor()8204 public EditingTargetCodeExecution getCodeExecutor() 8205 { 8206 return codeExecution_; 8207 } 8208 8209 /** 8210 * Updates the path of the file loaded in the editor, as though the user 8211 * had just saved the file at the new path. 8212 * 8213 * @param path New path for the editor 8214 */ setPath(FileSystemItem path)8215 public void setPath(FileSystemItem path) 8216 { 8217 // Find the new type 8218 TextFileType type = fileTypeRegistry_.getTextTypeForFile(path); 8219 8220 // Simulate a completed save of the new path 8221 new SaveProgressIndicator(path, type, false, null).onCompleted(); 8222 } 8223 setRMarkdownBehaviorEnabled(boolean enabled)8224 private void setRMarkdownBehaviorEnabled(boolean enabled) 8225 { 8226 // register idle monitor; automatically creates/refreshes previews 8227 // of images and LaTeX equations during idle 8228 if (bgIdleMonitor_ == null && enabled) 8229 bgIdleMonitor_ = new TextEditingTargetIdleMonitor(this, 8230 docUpdateSentinel_); 8231 else if (bgIdleMonitor_ != null) 8232 { 8233 if (enabled) 8234 bgIdleMonitor_.beginMonitoring(); 8235 else 8236 bgIdleMonitor_.endMonitoring(); 8237 } 8238 8239 // set up mathjax 8240 if (mathjax_ == null && enabled) 8241 mathjax_ = new MathJax(docDisplay_, docUpdateSentinel_, prefs_); 8242 8243 if (enabled) 8244 { 8245 // auto preview images and equations 8246 if (inlinePreviewer_ == null) 8247 inlinePreviewer_ = new InlinePreviewer( 8248 this, docUpdateSentinel_, prefs_); 8249 inlinePreviewer_.preview(); 8250 8251 // sync the notebook's output mode (enable/disable inline output) 8252 if (notebook_ != null) 8253 notebook_.syncOutputMode(); 8254 } 8255 else 8256 { 8257 // clean up previewers 8258 if (inlinePreviewer_ != null) 8259 inlinePreviewer_.onDismiss(); 8260 8261 // clean up line widgets 8262 if (notebook_ != null) 8263 notebook_.onNotebookClearAllOutput(); 8264 docDisplay_.removeAllLineWidgets(); 8265 } 8266 } 8267 setIntendedAsReadOnly(List<String> alternatives)8268 public void setIntendedAsReadOnly(List<String> alternatives) 8269 { 8270 view_.showReadOnlyWarning(alternatives); 8271 } 8272 installShinyTestDependencies(final Command success)8273 void installShinyTestDependencies(final Command success) { 8274 server_.installShinyTestDependencies(new ServerRequestCallback<ConsoleProcess>() { 8275 @Override 8276 public void onResponseReceived(ConsoleProcess process) 8277 { 8278 final ConsoleProgressDialog dialog = new ConsoleProgressDialog(process, server_); 8279 dialog.showModal(); 8280 8281 process.addProcessExitHandler(new ProcessExitEvent.Handler() 8282 { 8283 @Override 8284 public void onProcessExit(ProcessExitEvent event) 8285 { 8286 if (event.getExitCode() == 0) 8287 { 8288 success.execute(); 8289 dialog.closeDialog(); 8290 } 8291 } 8292 }); 8293 } 8294 8295 @Override 8296 public void onError(ServerError error) 8297 { 8298 Debug.logError(error); 8299 globalDisplay_.showErrorMessage("Failed to install additional dependencies", error.getUserMessage()); 8300 } 8301 }); 8302 } 8303 checkTestPackageDependencies(final Command success, boolean isTestThat)8304 void checkTestPackageDependencies(final Command success, boolean isTestThat) { 8305 dependencyManager_.withTestPackage( 8306 new Command() 8307 { 8308 @Override 8309 public void execute() 8310 { 8311 if (isTestThat) 8312 success.execute(); 8313 else { 8314 server_.hasShinyTestDependenciesInstalled(new ServerRequestCallback<Boolean>() { 8315 @Override 8316 public void onResponseReceived(Boolean hasPackageDependencies) 8317 { 8318 if (hasPackageDependencies) 8319 success.execute(); 8320 else { 8321 globalDisplay_.showYesNoMessage( 8322 GlobalDisplay.MSG_WARNING, 8323 "Install Shinytest Dependencies", 8324 "The package shinytest requires additional components to run.\n\n" + 8325 "Install additional components?", 8326 new Operation() 8327 { 8328 public void execute() 8329 { 8330 installShinyTestDependencies(success); 8331 } 8332 }, 8333 false); 8334 } 8335 } 8336 8337 @Override 8338 public void onError(ServerError error) 8339 { 8340 Debug.logError(error); 8341 globalDisplay_.showErrorMessage("Failed to check for additional dependencies", error.getMessage()); 8342 } 8343 }); 8344 } 8345 } 8346 }, 8347 isTestThat 8348 ); 8349 } 8350 8351 @Handler onTestTestthatFile()8352 void onTestTestthatFile() 8353 { 8354 final String buildCommand = "test-file"; 8355 8356 checkTestPackageDependencies( 8357 new Command() 8358 { 8359 @Override 8360 public void execute() 8361 { 8362 save(new Command() 8363 { 8364 @Override 8365 public void execute() 8366 { 8367 server_.startBuild(buildCommand, docUpdateSentinel_.getPath(), 8368 new SimpleRequestCallback<Boolean>() { 8369 @Override 8370 public void onResponseReceived(Boolean response) 8371 { 8372 8373 } 8374 8375 @Override 8376 public void onError(ServerError error) 8377 { 8378 super.onError(error); 8379 } 8380 }); 8381 } 8382 }); 8383 } 8384 }, 8385 true 8386 ); 8387 } 8388 8389 @Handler onTestShinytestFile()8390 void onTestShinytestFile() 8391 { 8392 final String buildCommand = "test-shiny-file"; 8393 8394 checkTestPackageDependencies( 8395 new Command() 8396 { 8397 @Override 8398 public void execute() 8399 { 8400 save(new Command() 8401 { 8402 @Override 8403 public void execute() 8404 { 8405 server_.startBuild(buildCommand, docUpdateSentinel_.getPath(), 8406 new SimpleRequestCallback<Boolean>() { 8407 @Override 8408 public void onResponseReceived(Boolean response) 8409 { 8410 8411 } 8412 8413 @Override 8414 public void onError(ServerError error) 8415 { 8416 super.onError(error); 8417 } 8418 }); 8419 } 8420 }); 8421 } 8422 }, 8423 false 8424 ); 8425 } 8426 8427 @Handler onShinyRecordTest()8428 void onShinyRecordTest() 8429 { 8430 checkTestPackageDependencies( 8431 new Command() 8432 { 8433 @Override 8434 public void execute() 8435 { 8436 String shinyAppPath = FilePathUtils.dirFromFile(docUpdateSentinel_.getPath()); 8437 8438 if (fileType_.canKnitToHTML()) 8439 { 8440 shinyAppPath = docUpdateSentinel_.getPath(); 8441 } 8442 8443 String code = "shinytest::recordTest(\"" + shinyAppPath.replace("\"", "\\\"") + "\")"; 8444 events_.fireEvent(new SendToConsoleEvent(code, true)); 8445 } 8446 }, 8447 false 8448 ); 8449 } 8450 8451 @Handler onShinyRunAllTests()8452 void onShinyRunAllTests() 8453 { 8454 checkTestPackageDependencies( 8455 new Command() 8456 { 8457 @Override 8458 public void execute() 8459 { 8460 server_.startBuild("test-shiny", FilePathUtils.dirFromFile(docUpdateSentinel_.getPath()), 8461 new SimpleRequestCallback<Boolean>() { 8462 @Override 8463 public void onResponseReceived(Boolean response) 8464 { 8465 8466 } 8467 8468 @Override 8469 public void onError(ServerError error) 8470 { 8471 super.onError(error); 8472 } 8473 }); 8474 } 8475 }, 8476 false 8477 ); 8478 } 8479 8480 @Handler onShinyCompareTest()8481 void onShinyCompareTest() 8482 { 8483 final String testFile = docUpdateSentinel_.getPath(); 8484 server_.hasShinyTestResults(testFile, new ServerRequestCallback<ShinyTestResults>() { 8485 @Override 8486 public void onResponseReceived(ShinyTestResults results) 8487 { 8488 if (!results.testDirExists) 8489 { 8490 globalDisplay_.showMessage( 8491 GlobalDisplay.MSG_INFO, 8492 "No Failed Results", 8493 "There are no failed tests to compare." 8494 ); 8495 } 8496 else 8497 { 8498 checkTestPackageDependencies(() -> 8499 { 8500 String testName = FilePathUtils.fileNameSansExtension(testFile); 8501 String code = "shinytest::viewTestDiff(\"" + 8502 results.appDir + "\", \"" + testName + "\")"; 8503 events_.fireEvent(new SendToConsoleEvent(code, true)); 8504 }, false); 8505 } 8506 } 8507 8508 @Override 8509 public void onError(ServerError error) 8510 { 8511 Debug.logError(error); 8512 globalDisplay_.showErrorMessage("Failed to check if results are available", error.getUserMessage()); 8513 } 8514 }); 8515 } 8516 getSpellingTarget()8517 public TextEditingTargetSpelling getSpellingTarget() { return this.spelling_; } 8518 nudgeAutosave()8519 private void nudgeAutosave() 8520 { 8521 // Cancel any existing autosave timer 8522 autoSaveTimer_.cancel(); 8523 8524 // Bail if not enabled 8525 if (prefs_.autoSaveOnIdle().getValue() != UserPrefs.AUTO_SAVE_ON_IDLE_COMMIT) 8526 return; 8527 8528 // OK, schedule autosave 8529 autoSaveTimer_.schedule(prefs_.autoSaveMs()); 8530 } 8531 8532 // logical state (may not be physically activated yet due to async loading) isVisualModeActivated()8533 public boolean isVisualModeActivated() 8534 { 8535 return docUpdateSentinel_.getBoolProperty(RMD_VISUAL_MODE, false); 8536 } 8537 8538 // physical state (guaranteed to be loaded and addressable) isVisualEditorActive()8539 public boolean isVisualEditorActive() 8540 { 8541 return visualMode_ != null && visualMode_.isVisualEditorActive(); 8542 } 8543 8544 /** 8545 * Prepares to execute code when visual mode is active; ensures that the 8546 * underlying editor has a complete copy of the code and scope tree. 8547 * 8548 * @param onComplete Command to run when sync is complete. 8549 */ prepareForVisualExecution(Command onComplete)8550 public void prepareForVisualExecution(Command onComplete) 8551 { 8552 if (isVisualEditorActive()) 8553 { 8554 visualMode_.syncToEditor(SyncType.SyncTypeExecution, onComplete); 8555 } 8556 else 8557 { 8558 onComplete.execute(); 8559 } 8560 } 8561 8562 /** 8563 * Executes a command with the active Ace instance. If there is no active 8564 * instance (e.g. in visual mode when focus is not in an editor), then the 8565 * command is not executed. 8566 * 8567 * @param cmd The command to execute. 8568 */ withActiveEditor(CommandWithArg<DocDisplay> cmd)8569 private void withActiveEditor(CommandWithArg<DocDisplay> cmd) 8570 { 8571 if (isVisualEditorActive()) 8572 { 8573 DocDisplay activeEditor = visualMode_.getActiveEditor(); 8574 if (activeEditor != null) 8575 { 8576 cmd.execute(activeEditor); 8577 } 8578 } 8579 else 8580 { 8581 cmd.execute(docDisplay_); 8582 } 8583 } 8584 8585 // user is switching to visual mode onUserSwitchingToVisualMode()8586 void onUserSwitchingToVisualMode() 8587 { 8588 visualMode_.onUserSwitchingToVisualMode(); 8589 } 8590 getEditorContext()8591 public void getEditorContext() 8592 { 8593 if (visualMode_.isActivated()) 8594 { 8595 ensureVisualModeActive(() -> 8596 { 8597 AceEditor activeEditor = AceEditor.getLastFocusedEditor(); 8598 if (activeEditor == null) 8599 { 8600 GetEditorContextEvent.SelectionData data = 8601 GetEditorContextEvent.SelectionData.create( 8602 StringUtil.notNull(getId()), 8603 StringUtil.notNull(getPath()), 8604 "", 8605 JavaScriptObject.createArray().cast()); 8606 8607 server_.getEditorContextCompleted(data, new VoidServerRequestCallback()); 8608 return; 8609 } 8610 8611 SourceColumnManager.getEditorContext( 8612 getId(), 8613 getPath(), 8614 activeEditor, 8615 server_); 8616 }); 8617 } 8618 else 8619 { 8620 ensureTextEditorActive(() -> 8621 { 8622 SourceColumnManager.getEditorContext( 8623 getId(), 8624 getPath(), 8625 getDocDisplay(), 8626 server_); 8627 }); 8628 } 8629 } 8630 withEditorSelection(final CommandWithArg<String> callback)8631 public void withEditorSelection(final CommandWithArg<String> callback) 8632 { 8633 if (visualMode_.isActivated()) 8634 { 8635 ensureVisualModeActive(new Command() 8636 { 8637 @Override 8638 public void execute() 8639 { 8640 callback.execute(visualMode_.getSelectedText()); 8641 } 8642 }); 8643 } 8644 else 8645 { 8646 ensureTextEditorActive(new Command() 8647 { 8648 @Override 8649 public void execute() 8650 { 8651 callback.execute(docDisplay_.getSelectionValue()); 8652 } 8653 }); 8654 } 8655 } 8656 continueSpecialCommentOnNewline(NativeEvent event)8657 private boolean continueSpecialCommentOnNewline(NativeEvent event) 8658 { 8659 // don't do anything if we have a completion popup showing 8660 if (docDisplay_.isPopupVisible()) 8661 return false; 8662 8663 // only handle plain Enter insertions 8664 if (event.getKeyCode() != KeyCodes.KEY_ENTER) 8665 return false; 8666 8667 int modifier = KeyboardShortcut.getModifierValue(event); 8668 if (modifier != KeyboardShortcut.NONE) 8669 return false; 8670 8671 String line = docDisplay_.getCurrentLineUpToCursor(); 8672 8673 // validate that this line begins with a comment character 8674 // (necessary to check token type for e.g. Markdown documents) 8675 // https://github.com/rstudio/rstudio/issues/6421 8676 // 8677 // note that we don't check all tokens here since we provide 8678 // special token styling within some comments (e.g. roxygen) 8679 JsArray<Token> tokens = 8680 docDisplay_.getTokens(docDisplay_.getCursorPosition().getRow()); 8681 8682 for (int i = 0, n = tokens.length(); i < n; i++) 8683 { 8684 Token token = tokens.get(i); 8685 8686 // skip initial whitespace tokens if any 8687 String value = token.getValue(); 8688 if (value.trim().isEmpty()) 8689 continue; 8690 8691 // check that we have a comment 8692 if (token.hasType("comment")) 8693 break; 8694 8695 // the token isn't a comment; we shouldn't take action here 8696 return false; 8697 } 8698 8699 // if this is an R Markdown chunk metadata comment, and this 8700 // line is blank other than the comment prefix, remove that 8701 // prefix and insert a newline (terminating the block) 8702 { 8703 Pattern pattern = Pattern.create("^\\s*#[|]\\s*$", ""); 8704 Match match = pattern.match(line, 0); 8705 if (match != null) 8706 { 8707 Position cursorPos = docDisplay_.getCursorPosition(); 8708 Range range = Range.create( 8709 cursorPos.getRow(), 0, 8710 cursorPos.getRow() + 1, 0); 8711 8712 event.stopPropagation(); 8713 event.preventDefault(); 8714 docDisplay_.replaceRange(range, "\n\n"); 8715 docDisplay_.moveCursorBackward(); 8716 docDisplay_.ensureCursorVisible(); 8717 return true; 8718 } 8719 } 8720 8721 // NOTE: we are generous with our pattern definition here 8722 // as we've already validated this is a comment token above 8723 Pattern pattern = Pattern.create("^(\\s*(?:#+|%+|//+)['*+>|]\\s*)"); 8724 Match match = pattern.match(line, 0); 8725 if (match == null) 8726 return false; 8727 8728 event.preventDefault(); 8729 event.stopPropagation(); 8730 docDisplay_.insertCode("\n" + match.getGroup(1)); 8731 docDisplay_.ensureCursorVisible(); 8732 8733 return true; 8734 } 8735 8736 private StatusBar statusBar_; 8737 private final DocDisplay docDisplay_; 8738 private final UserPrefs prefs_; 8739 private final UserState state_; 8740 private Display view_; 8741 private final Commands commands_; 8742 private final SourceServerOperations server_; 8743 private final EventBus events_; 8744 private final GlobalDisplay globalDisplay_; 8745 private final FileDialogs fileDialogs_; 8746 private final FileTypeRegistry fileTypeRegistry_; 8747 private final FileTypeCommands fileTypeCommands_; 8748 private final ConsoleDispatcher consoleDispatcher_; 8749 private final WorkbenchContext workbenchContext_; 8750 private final Session session_; 8751 private final Synctex synctex_; 8752 private final FontSizeManager fontSizeManager_; 8753 private final Source source_; 8754 private final DependencyManager dependencyManager_; 8755 private DocUpdateSentinel docUpdateSentinel_; 8756 private final Value<String> name_ = new Value<>(null); 8757 private TextFileType fileType_; 8758 private String id_; 8759 private HandlerRegistration commandHandlerReg_; 8760 private final ArrayList<HandlerRegistration> releaseOnDismiss_ = new ArrayList<>(); 8761 private final DirtyState dirtyState_; 8762 private final HandlerManager handlers_ = new HandlerManager(this); 8763 private FileSystemContext fileContext_; 8764 private final TextEditingTargetCompilePdfHelper compilePdfHelper_; 8765 private final TextEditingTargetRMarkdownHelper rmarkdownHelper_; 8766 private final TextEditingTargetCppHelper cppHelper_; 8767 private final TextEditingTargetJSHelper jsHelper_; 8768 private final TextEditingTargetSqlHelper sqlHelper_; 8769 private final TextEditingTargetPresentationHelper presentationHelper_; 8770 private final TextEditingTargetRHelper rHelper_; 8771 private VisualMode visualMode_; 8772 private final TextEditingTargetQuartoHelper quartoHelper_; 8773 private TextEditingTargetIdleMonitor bgIdleMonitor_; 8774 private TextEditingTargetThemeHelper themeHelper_; 8775 private boolean ignoreDeletes_; 8776 private boolean forceSaveCommandActive_ = false; 8777 private final TextEditingTargetScopeHelper scopeHelper_; 8778 private TextEditingTargetPackageDependencyHelper packageDependencyHelper_; 8779 private TextEditingTargetSpelling spelling_; 8780 private TextEditingTargetNotebook notebook_; 8781 private TextEditingTargetChunks chunks_; 8782 private final BreakpointManager breakpointManager_; 8783 private final LintManager lintManager_; 8784 private CollabEditStartParams queuedCollabParams_; 8785 private MathJax mathjax_; 8786 private InlinePreviewer inlinePreviewer_; 8787 private ProjectConfig projConfig_; 8788 8789 // Allows external edit checks to supercede one another 8790 private final Invalidation externalEditCheckInvalidation_ = 8791 new Invalidation(); 8792 // Prevents external edit checks from happening too soon after each other 8793 private final IntervalTracker externalEditCheckInterval_ = 8794 new IntervalTracker(1000, true); 8795 private boolean isWaitingForUserResponseToExternalEdit_ = false; 8796 private EditingTargetCodeExecution codeExecution_; 8797 8798 // Timer for autosave 8799 private final Timer autoSaveTimer_ = new Timer() 8800 { 8801 @Override 8802 public void run() 8803 { 8804 // It's unlikely, but if we attempt to autosave while running a 8805 // previous autosave, just nudge the timer so we try again. 8806 if (saving_ != 0) 8807 { 8808 // If we've been trying to save for more than 5 seconds, we won't 8809 // nudge (just fall through and we'll attempt again below) 8810 if (System.currentTimeMillis() - saving_ < 5000) 8811 { 8812 nudgeAutosave(); 8813 return; 8814 } 8815 } 8816 8817 if (getPath() == null) 8818 { 8819 // This editor isn't file-backed yet, so there's no save to do. 8820 return; 8821 } 8822 8823 if (docDisplay_.hasActiveCollabSession()) 8824 { 8825 // Everyone's autosave gets turned off during a collab session -- 8826 // otherwise the autosaves all fire at once and fight 8827 return; 8828 } 8829 8830 // Save (and keep track of when we initiated it) 8831 saving_ = System.currentTimeMillis(); 8832 try 8833 { 8834 autoSave( 8835 () -> 8836 { 8837 saving_ = 0; 8838 }, 8839 () -> 8840 { 8841 // if this autosave operation silently fails, we want to automatically restart it 8842 saving_ = 0; 8843 nudgeAutosave(); 8844 }); 8845 } 8846 catch(Exception e) 8847 { 8848 // Autosave exceptions are logged rather than displayed 8849 saving_ = 0; 8850 Debug.logException(e); 8851 } 8852 } 8853 8854 // The time at which we attempted the current autosave operation, or zero 8855 // if no autosave operation is in progress. 8856 private long saving_ = 0; 8857 }; 8858 8859 private SourcePosition debugStartPos_ = null; 8860 private SourcePosition debugEndPos_ = null; 8861 private boolean isDebugWarningVisible_ = false; 8862 private boolean isBreakpointWarningVisible_ = false; 8863 private String extendedType_; 8864 8865 // prevent multiple manual saves from queuing up 8866 private boolean isSaving_ = false; 8867 8868 private abstract class RefactorServerRequestCallback 8869 extends ServerRequestCallback<JsArrayString> 8870 { 8871 private final String refactoringName_; 8872 RefactorServerRequestCallback(String refactoringName)8873 public RefactorServerRequestCallback(String refactoringName) 8874 { 8875 refactoringName_ = refactoringName; 8876 } 8877 8878 @Override onResponseReceived(final JsArrayString response)8879 public void onResponseReceived(final JsArrayString response) 8880 { 8881 doExtract(response); 8882 } 8883 8884 @Override onError(ServerError error)8885 public void onError(ServerError error) 8886 { 8887 globalDisplay_.showYesNoMessage( 8888 GlobalDisplay.MSG_WARNING, 8889 refactoringName_, 8890 "The selected code could not be " + 8891 "parsed.\n\n" + 8892 "Are you sure you want to continue?", 8893 new Operation() 8894 { 8895 public void execute() 8896 { 8897 doExtract(null); 8898 } 8899 }, 8900 false); 8901 } 8902 doExtract(final JsArrayString response)8903 abstract void doExtract(final JsArrayString response); 8904 } 8905 8906 private static final String PROPERTY_CURSOR_POSITION = "cursorPosition"; 8907 private static final String PROPERTY_SCROLL_LINE = "scrollLine"; 8908 } 8909