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 + "&nbsp;&nbsp;";
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