Permalink
Cannot retrieve contributors at this time
rstudio/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/TextEditingTarget.java /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
7353 lines (6507 sloc)
242 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * TextEditingTarget.java | |
| * | |
| * Copyright (C) 2009-18 by RStudio, Inc. | |
| * | |
| * Unless you have received this program directly from RStudio pursuant | |
| * to the terms of a commercial license agreement with RStudio, then | |
| * this program is licensed to you under the terms of version 3 of the | |
| * GNU Affero General Public License. This program is distributed WITHOUT | |
| * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, | |
| * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the | |
| * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. | |
| * | |
| */ | |
| package org.rstudio.studio.client.workbench.views.source.editors.text; | |
| import com.google.gwt.core.client.GWT; | |
| import com.google.gwt.core.client.JavaScriptObject; | |
| import com.google.gwt.core.client.JsArray; | |
| import com.google.gwt.core.client.JsArrayString; | |
| import com.google.gwt.core.client.Scheduler; | |
| import com.google.gwt.core.client.Scheduler.RepeatingCommand; | |
| import com.google.gwt.core.client.Scheduler.ScheduledCommand; | |
| import com.google.gwt.dom.client.Element; | |
| import com.google.gwt.dom.client.NativeEvent; | |
| import com.google.gwt.dom.client.Style.FontWeight; | |
| import com.google.gwt.event.dom.client.*; | |
| import com.google.gwt.event.logical.shared.*; | |
| import com.google.gwt.event.shared.GwtEvent; | |
| import com.google.gwt.event.shared.HandlerManager; | |
| import com.google.gwt.event.shared.HandlerRegistration; | |
| import com.google.gwt.http.client.URL; | |
| import com.google.gwt.resources.client.ImageResource; | |
| import com.google.gwt.safehtml.shared.SafeHtmlBuilder; | |
| import com.google.gwt.user.client.Command; | |
| import com.google.gwt.user.client.Event; | |
| import com.google.gwt.user.client.Event.NativePreviewEvent; | |
| import com.google.gwt.user.client.Timer; | |
| import com.google.gwt.user.client.ui.HasValue; | |
| import com.google.gwt.user.client.ui.MenuItem; | |
| import com.google.gwt.user.client.ui.UIObject; | |
| import com.google.gwt.user.client.ui.Widget; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import org.rstudio.core.client.*; | |
| import org.rstudio.core.client.command.AppCommand; | |
| import org.rstudio.core.client.command.CommandBinder; | |
| import org.rstudio.core.client.command.Handler; | |
| import org.rstudio.core.client.command.KeyboardShortcut; | |
| import org.rstudio.core.client.events.EnsureHeightHandler; | |
| import org.rstudio.core.client.events.EnsureVisibleHandler; | |
| import org.rstudio.core.client.events.HasEnsureHeightHandlers; | |
| import org.rstudio.core.client.events.HasEnsureVisibleHandlers; | |
| import org.rstudio.core.client.files.FileSystemContext; | |
| import org.rstudio.core.client.files.FileSystemItem; | |
| import org.rstudio.core.client.js.JsMap; | |
| import org.rstudio.core.client.js.JsUtil; | |
| import org.rstudio.core.client.regex.Match; | |
| import org.rstudio.core.client.regex.Pattern; | |
| import org.rstudio.core.client.widget.*; | |
| import org.rstudio.studio.client.RStudioGinjector; | |
| import org.rstudio.studio.client.application.Desktop; | |
| import org.rstudio.studio.client.application.events.ChangeFontSizeEvent; | |
| import org.rstudio.studio.client.application.events.ChangeFontSizeHandler; | |
| import org.rstudio.studio.client.application.events.EventBus; | |
| import org.rstudio.studio.client.application.events.ResetEditorCommandsEvent; | |
| import org.rstudio.studio.client.application.events.SetEditorCommandBindingsEvent; | |
| import org.rstudio.studio.client.common.*; | |
| import org.rstudio.studio.client.common.console.ConsoleProcess; | |
| import org.rstudio.studio.client.common.console.ProcessExitEvent; | |
| import org.rstudio.studio.client.common.debugging.BreakpointManager; | |
| import org.rstudio.studio.client.common.debugging.events.BreakpointsSavedEvent; | |
| import org.rstudio.studio.client.common.debugging.model.Breakpoint; | |
| import org.rstudio.studio.client.common.dependencies.DependencyManager; | |
| import org.rstudio.studio.client.common.filetypes.DocumentMode; | |
| import org.rstudio.studio.client.common.filetypes.FileType; | |
| import org.rstudio.studio.client.common.filetypes.FileTypeCommands; | |
| import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; | |
| import org.rstudio.studio.client.common.filetypes.SweaveFileType; | |
| import org.rstudio.studio.client.common.filetypes.TextFileType; | |
| import org.rstudio.studio.client.common.filetypes.events.RenameSourceFileEvent; | |
| import org.rstudio.studio.client.common.mathjax.MathJax; | |
| import org.rstudio.studio.client.common.r.roxygen.RoxygenHelper; | |
| import org.rstudio.studio.client.common.rnw.RnwWeave; | |
| import org.rstudio.studio.client.common.synctex.Synctex; | |
| import org.rstudio.studio.client.common.synctex.SynctexUtils; | |
| import org.rstudio.studio.client.common.synctex.model.SourceLocation; | |
| import org.rstudio.studio.client.htmlpreview.events.ShowHTMLPreviewEvent; | |
| import org.rstudio.studio.client.htmlpreview.model.HTMLPreviewParams; | |
| import org.rstudio.studio.client.notebook.CompileNotebookOptions; | |
| import org.rstudio.studio.client.notebook.CompileNotebookOptionsDialog; | |
| import org.rstudio.studio.client.notebook.CompileNotebookPrefs; | |
| import org.rstudio.studio.client.notebook.CompileNotebookResult; | |
| import org.rstudio.studio.client.plumber.events.LaunchPlumberAPIEvent; | |
| import org.rstudio.studio.client.plumber.events.PlumberAPIStatusEvent; | |
| import org.rstudio.studio.client.plumber.model.PlumberAPIParams; | |
| import org.rstudio.studio.client.plumber.model.PlumberViewerType; | |
| import org.rstudio.studio.client.rmarkdown.RmdOutput; | |
| import org.rstudio.studio.client.rmarkdown.events.ConvertToShinyDocEvent; | |
| import org.rstudio.studio.client.rmarkdown.events.RmdOutputFormatChangedEvent; | |
| import org.rstudio.studio.client.rmarkdown.events.RmdRenderPendingEvent; | |
| import org.rstudio.studio.client.rmarkdown.model.NotebookQueueUnit; | |
| import org.rstudio.studio.client.rmarkdown.model.RMarkdownContext; | |
| import org.rstudio.studio.client.rmarkdown.model.RmdEditorOptions; | |
| import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatter; | |
| import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatterOutputOptions; | |
| import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat; | |
| import org.rstudio.studio.client.rmarkdown.model.RmdTemplateFormat; | |
| import org.rstudio.studio.client.rmarkdown.model.RmdYamlData; | |
| import org.rstudio.studio.client.rmarkdown.model.YamlFrontMatter; | |
| import org.rstudio.studio.client.rmarkdown.ui.RmdTemplateOptionsDialog; | |
| import org.rstudio.studio.client.rsconnect.events.RSConnectActionEvent; | |
| import org.rstudio.studio.client.rsconnect.events.RSConnectDeployInitiatedEvent; | |
| import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSettings; | |
| import org.rstudio.studio.client.server.ServerError; | |
| import org.rstudio.studio.client.server.ServerRequestCallback; | |
| import org.rstudio.studio.client.server.Void; | |
| import org.rstudio.studio.client.server.VoidServerRequestCallback; | |
| import org.rstudio.studio.client.shiny.events.LaunchShinyApplicationEvent; | |
| import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent; | |
| import org.rstudio.studio.client.shiny.model.ShinyApplicationParams; | |
| import org.rstudio.studio.client.shiny.model.ShinyViewerType; | |
| import org.rstudio.studio.client.workbench.WorkbenchContext; | |
| import org.rstudio.studio.client.workbench.commands.Commands; | |
| import org.rstudio.studio.client.workbench.model.Session; | |
| import org.rstudio.studio.client.workbench.model.SessionInfo; | |
| import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; | |
| import org.rstudio.studio.client.workbench.prefs.model.UIPrefsAccessor; | |
| import org.rstudio.studio.client.workbench.ui.FontSizeManager; | |
| import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; | |
| import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorPosition; | |
| import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection; | |
| import org.rstudio.studio.client.workbench.views.files.events.FileChangeEvent; | |
| import org.rstudio.studio.client.workbench.views.files.events.FileChangeHandler; | |
| import org.rstudio.studio.client.workbench.views.files.model.FileChange; | |
| import org.rstudio.studio.client.workbench.views.help.events.ShowHelpEvent; | |
| import org.rstudio.studio.client.workbench.views.jobs.events.JobRunScriptEvent; | |
| import org.rstudio.studio.client.workbench.views.output.compilepdf.events.CompilePdfEvent; | |
| import org.rstudio.studio.client.workbench.views.output.lint.LintManager; | |
| import org.rstudio.studio.client.workbench.views.presentation.events.SourceFileSaveCompletedEvent; | |
| import org.rstudio.studio.client.workbench.views.presentation.model.PresentationState; | |
| import org.rstudio.studio.client.workbench.views.source.SourceBuildHelper; | |
| import org.rstudio.studio.client.workbench.views.source.SourceWindowManager; | |
| import org.rstudio.studio.client.workbench.views.source.editors.EditingTarget; | |
| import org.rstudio.studio.client.workbench.views.source.editors.EditingTargetCodeExecution; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTargetRMarkdownHelper.RmdSelectedTemplate; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceAfterCommandExecutedEvent; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceFold; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Mode.InsertChunkInfo; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ace.VimMarks; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.cpp.CppCompletionContext; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.cpp.CppCompletionOperation; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.events.*; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.ChunkExecUnit; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.TextEditingTargetNotebook; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.events.InterruptChunkEvent; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBar; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBar.HideMessageHandler; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBarPopupMenu; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBarPopupRequest; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ui.ChooseEncodingDialog; | |
| import org.rstudio.studio.client.workbench.views.source.editors.text.ui.RMarkdownNoParamsDialog; | |
| import org.rstudio.studio.client.workbench.views.source.events.CollabEditStartParams; | |
| import org.rstudio.studio.client.workbench.views.source.events.CollabExternalEditEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.DocFocusedEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.DocTabDragStateChangedEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.DocWindowChangedEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.PopoutDocEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.RecordNavigationPositionEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.RecordNavigationPositionHandler; | |
| import org.rstudio.studio.client.workbench.views.source.events.SourceFileSavedEvent; | |
| import org.rstudio.studio.client.workbench.views.source.events.SourceNavigationEvent; | |
| import org.rstudio.studio.client.workbench.views.source.model.*; | |
| import org.rstudio.studio.client.workbench.views.vcs.common.ConsoleProgressDialog; | |
| import org.rstudio.studio.client.workbench.views.vcs.common.events.ShowVcsDiffEvent; | |
| import org.rstudio.studio.client.workbench.views.vcs.common.events.ShowVcsHistoryEvent; | |
| import org.rstudio.studio.client.workbench.views.vcs.common.events.VcsRevertFileEvent; | |
| import org.rstudio.studio.client.workbench.views.vcs.common.events.VcsViewOnGitHubEvent; | |
| import org.rstudio.studio.client.workbench.views.vcs.common.model.GitHubViewRequest; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| public class TextEditingTarget implements | |
| EditingTarget, | |
| EditingTargetCodeExecution.CodeExtractor | |
| { | |
| interface MyCommandBinder | |
| extends CommandBinder<Commands, TextEditingTarget> | |
| { | |
| } | |
| private static final String NOTEBOOK_TITLE = "notebook_title"; | |
| private static final String NOTEBOOK_AUTHOR = "notebook_author"; | |
| private static final String NOTEBOOK_TYPE = "notebook_type"; | |
| public final static String DOC_OUTLINE_SIZE = "docOutlineSize"; | |
| public final static String DOC_OUTLINE_VISIBLE = "docOutlineVisible"; | |
| private static final MyCommandBinder commandBinder = | |
| GWT.create(MyCommandBinder.class); | |
| public interface Display extends TextDisplay, | |
| WarningBarDisplay, | |
| HasEnsureVisibleHandlers, | |
| HasEnsureHeightHandlers, | |
| HasResizeHandlers | |
| { | |
| HasValue<Boolean> getSourceOnSave(); | |
| void ensureVisible(); | |
| void showFindReplace(boolean defaultForward); | |
| void findNext(); | |
| void findPrevious(); | |
| void findSelectAll(); | |
| void findFromSelection(); | |
| void replaceAndFind(); | |
| StatusBar getStatusBar(); | |
| boolean isAttached(); | |
| void adaptToExtendedFileType(String extendedType); | |
| void onShinyApplicationStateChanged(String state); | |
| void onPlumberAPIStateChanged(String state); | |
| void debug_dumpContents(); | |
| void debug_importDump(); | |
| void setIsShinyFormat(boolean showOutputOptions, | |
| boolean isPresentation, | |
| boolean isShinyPrerendered); | |
| void setIsNotShinyFormat(); | |
| void setIsNotebookFormat(); | |
| void setFormatOptions(TextFileType fileType, | |
| boolean showRmdFormatMenu, | |
| boolean canEditFormatOptions, | |
| List<String> options, | |
| List<String> values, | |
| List<String> extensions, | |
| String selected); | |
| HandlerRegistration addRmdFormatChangedHandler( | |
| RmdOutputFormatChangedEvent.Handler handler); | |
| void setPublishPath(String type, String publishPath); | |
| void invokePublish(); | |
| void initWidgetSize(); | |
| void toggleDocumentOutline(); | |
| void setNotebookUIVisible(boolean visible); | |
| } | |
| private class SaveProgressIndicator implements ProgressIndicator | |
| { | |
| public SaveProgressIndicator(FileSystemItem file, | |
| TextFileType fileType, | |
| Command executeOnSuccess) | |
| { | |
| file_ = file; | |
| newFileType_ = fileType; | |
| executeOnSuccess_ = executeOnSuccess; | |
| } | |
| public void onProgress(String message) | |
| { | |
| onProgress(message, null); | |
| } | |
| public void onProgress(String message, Operation onCancel) | |
| { | |
| } | |
| public void clearProgress() | |
| { | |
| } | |
| public void onCompleted() | |
| { | |
| // don't need to check again soon because we just saved | |
| // (without this and when file monitoring is active we'd | |
| // end up immediately checking for external edits) | |
| externalEditCheckInterval_.reset(250); | |
| if (newFileType_ != null) | |
| fileType_ = newFileType_; | |
| if (file_ != null) | |
| { | |
| ignoreDeletes_ = false; | |
| forceSaveCommandActive_ = false; | |
| commands_.reopenSourceDocWithEncoding().setEnabled(true); | |
| name_.setValue(file_.getName(), true); | |
| // Make sure tooltip gets updated, even if name hasn't changed | |
| name_.fireChangeEvent(); | |
| // If we were dirty prior to saving, clean up the debug state so | |
| // we don't continue highlighting after saving. (There are cases | |
| // in which we want to restore highlighting after the dirty state | |
| // is marked clean--i.e. when unwinding the undo stack.) | |
| if (dirtyState_.getValue()) | |
| endDebugHighlighting(); | |
| dirtyState_.markClean(); | |
| } | |
| if (newFileType_ != null) | |
| { | |
| // Make sure the icon gets updated, even if name hasn't changed | |
| name_.fireChangeEvent(); | |
| updateStatusBarLanguage(); | |
| view_.adaptToFileType(newFileType_); | |
| // turn R Markdown behavior (inline execution, previews, etc.) | |
| // based on whether we just became an R Markdown type | |
| setRMarkdownBehaviorEnabled(newFileType_.isRmd()); | |
| events_.fireEvent(new FileTypeChangedEvent()); | |
| if (!fileType_.canSourceOnSave() && docUpdateSentinel_.sourceOnSave()) | |
| { | |
| view_.getSourceOnSave().setValue(false, true); | |
| } | |
| } | |
| if (executeOnSuccess_ != null) | |
| executeOnSuccess_.execute(); | |
| } | |
| public void onError(final String message) | |
| { | |
| // in case the error occured saving a document that wasn't | |
| // in the foreground | |
| view_.ensureVisible(); | |
| // command to show the error | |
| final Command showErrorCommand = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| globalDisplay_.showErrorMessage("Error Saving File", | |
| message); | |
| } | |
| }; | |
| // check whether the file exists and isn't writeable | |
| if (file_ != null) | |
| { | |
| server_.isReadOnlyFile(file_.getPath(), | |
| new ServerRequestCallback<Boolean>() { | |
| @Override | |
| public void onResponseReceived(Boolean isReadOnly) | |
| { | |
| if (isReadOnly) | |
| { | |
| String message = "This source file is read-only " + | |
| "so changes cannot be saved"; | |
| view_.showWarningBar(message); | |
| String saveAsPath = file_.getParentPath().completePath( | |
| file_.getStem() + "-copy" + file_.getExtension()); | |
| saveNewFile( | |
| saveAsPath, | |
| null, | |
| CommandUtil.join(postSaveCommand(), new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| view_.hideWarningBar(); | |
| } | |
| })); | |
| } | |
| else | |
| { | |
| showErrorCommand.execute(); | |
| } | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| Debug.logError(error); | |
| showErrorCommand.execute(); | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| showErrorCommand.execute(); | |
| } | |
| } | |
| private final FileSystemItem file_; | |
| private final TextFileType newFileType_; | |
| private final Command executeOnSuccess_; | |
| } | |
| @Inject | |
| public 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, | |
| UIPrefs prefs, | |
| BreakpointManager breakpointManager, | |
| SourceBuildHelper sourceBuildHelper, | |
| DependencyManager dependencyManager) | |
| { | |
| commands_ = commands; | |
| server_ = server; | |
| events_ = events; | |
| globalDisplay_ = globalDisplay; | |
| fileDialogs_ = fileDialogs; | |
| fileTypeRegistry_ = fileTypeRegistry; | |
| fileTypeCommands_ = fileTypeCommands; | |
| consoleDispatcher_ = consoleDispatcher; | |
| workbenchContext_ = workbenchContext; | |
| session_ = session; | |
| synctex_ = synctex; | |
| fontSizeManager_ = fontSizeManager; | |
| breakpointManager_ = breakpointManager; | |
| sourceBuildHelper_ = sourceBuildHelper; | |
| dependencyManager_ = dependencyManager; | |
| docDisplay_ = docDisplay; | |
| dirtyState_ = new DirtyState(docDisplay_, false); | |
| lintManager_ = new LintManager(this, cppCompletionContext_); | |
| prefs_ = prefs; | |
| compilePdfHelper_ = new TextEditingTargetCompilePdfHelper(docDisplay_); | |
| rmarkdownHelper_ = new TextEditingTargetRMarkdownHelper(); | |
| cppHelper_ = new TextEditingTargetCppHelper(cppCompletionContext_, | |
| docDisplay_); | |
| jsHelper_ = new TextEditingTargetJSHelper(docDisplay_); | |
| sqlHelper_ = new TextEditingTargetSqlHelper(docDisplay_); | |
| presentationHelper_ = new TextEditingTargetPresentationHelper( | |
| docDisplay_); | |
| reformatHelper_ = new TextEditingTargetReformatHelper(docDisplay_); | |
| renameHelper_ = new TextEditingTargetRenameHelper(docDisplay_); | |
| rHelper_ = new TextEditingTargetRHelper(docDisplay_); | |
| docDisplay_.setRnwCompletionContext(compilePdfHelper_); | |
| docDisplay_.setCppCompletionContext(cppCompletionContext_); | |
| docDisplay_.setRCompletionContext(rContext_); | |
| scopeHelper_ = new TextEditingTargetScopeHelper(docDisplay_); | |
| addRecordNavigationPositionHandler(releaseOnDismiss_, | |
| docDisplay_, | |
| events_, | |
| this); | |
| docDisplay_.addKeyDownHandler(new KeyDownHandler() | |
| { | |
| public void onKeyDown(KeyDownEvent event) | |
| { | |
| NativeEvent ne = event.getNativeEvent(); | |
| int mod = KeyboardShortcut.getModifierValue(ne); | |
| if ((mod == KeyboardShortcut.META || ( | |
| mod == KeyboardShortcut.CTRL && | |
| !BrowseCap.hasMetaKey() && | |
| !docDisplay_.isEmacsModeOn() && | |
| (!docDisplay_.isVimModeOn() || docDisplay_.isVimInInsertMode()))) | |
| && ne.getKeyCode() == 'F') | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| commands_.findReplace().execute(); | |
| } | |
| else if (BrowseCap.hasMetaKey() && | |
| (mod == KeyboardShortcut.META) && | |
| (ne.getKeyCode() == 'E')) | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| commands_.findFromSelection().execute(); | |
| } | |
| else if (mod == KeyboardShortcut.CTRL | |
| && ne.getKeyCode() == KeyCodes.KEY_UP | |
| && fileType_ == FileTypeRegistry.R) | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| jumpToPreviousFunction(); | |
| } | |
| else if (mod == KeyboardShortcut.CTRL | |
| && ne.getKeyCode() == KeyCodes.KEY_DOWN | |
| && fileType_ == FileTypeRegistry.R) | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| jumpToNextFunction(); | |
| } | |
| else if ((ne.getKeyCode() == KeyCodes.KEY_ESCAPE) && | |
| !prefs_.useVimMode().getValue()) | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| // Don't send an interrupt if a popup is visible | |
| if (docDisplay_.isPopupVisible()) | |
| return; | |
| // Don't send an interrupt if we're in a source window | |
| if (!SourceWindowManager.isMainSourceWindow()) | |
| return; | |
| if (commands_.interruptR().isEnabled()) | |
| commands_.interruptR().execute(); | |
| } | |
| else if ( | |
| prefs_.continueCommentsOnNewline().getValue() && | |
| !docDisplay_.isPopupVisible() && | |
| ne.getKeyCode() == KeyCodes.KEY_ENTER && mod == 0 && | |
| (fileType_.isC() || isCursorInRMode() || isCursorInTexMode())) | |
| { | |
| String line = docDisplay_.getCurrentLineUpToCursor(); | |
| Pattern pattern = null; | |
| if (isCursorInRMode()) | |
| pattern = Pattern.create("^(\\s*#+'?\\s*)"); | |
| else if (isCursorInTexMode()) | |
| pattern = Pattern.create("^(\\s*%+'?\\s*)"); | |
| else if (fileType_.isC()) | |
| { | |
| // bail on attributes | |
| if (!line.matches("^\\s*//\\s*\\[\\[.*\\]\\].*")) | |
| pattern = Pattern.create("^(\\s*//'?\\s*)"); | |
| } | |
| if (pattern != null) | |
| { | |
| Match match = pattern.match(line, 0); | |
| if (match != null) | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| docDisplay_.insertCode("\n" + match.getGroup(1)); | |
| docDisplay_.ensureCursorVisible(); | |
| } | |
| } | |
| } | |
| else if ( | |
| prefs_.continueCommentsOnNewline().getValue() && | |
| !docDisplay_.isPopupVisible() && | |
| ne.getKeyCode() == KeyCodes.KEY_ENTER && | |
| mod == KeyboardShortcut.SHIFT) | |
| { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| String indent = docDisplay_.getNextLineIndent(); | |
| docDisplay_.insertCode("\n" + indent); | |
| } | |
| } | |
| }); | |
| docDisplay_.addCommandClickHandler(new CommandClickEvent.Handler() | |
| { | |
| @Override | |
| public void onCommandClick(CommandClickEvent event) | |
| { | |
| // bail if the target is a link marker (implies already handled) | |
| NativeEvent nativeEvent = event.getNativeEvent(); | |
| Element target = nativeEvent.getEventTarget().cast(); | |
| if (target != null && target.hasClassName("ace_marker")) | |
| { | |
| nativeEvent.stopPropagation(); | |
| nativeEvent.preventDefault(); | |
| return; | |
| } | |
| // force cursor position | |
| Position position = event.getEvent().getDocumentPosition(); | |
| docDisplay_.setCursorPosition(position); | |
| // delegate to handlers | |
| if (fileType_.canCompilePDF() && | |
| commands_.synctexSearch().isEnabled()) | |
| { | |
| // warn firefox users that this doesn't really work in Firefox | |
| if (BrowseCap.isFirefox() && !BrowseCap.isMacintosh()) | |
| SynctexUtils.maybeShowFirefoxWarning("PDF preview"); | |
| doSynctexSearch(true); | |
| } | |
| else | |
| { | |
| docDisplay_.goToDefinition(); | |
| } | |
| } | |
| }); | |
| docDisplay_.addFindRequestedHandler(new FindRequestedEvent.Handler() { | |
| @Override | |
| public void onFindRequested(FindRequestedEvent event) | |
| { | |
| view_.showFindReplace(event.getDefaultForward()); | |
| } | |
| }); | |
| docDisplay_.addScopeTreeReadyHandler(new ScopeTreeReadyEvent.Handler() | |
| { | |
| @Override | |
| public void onScopeTreeReady(ScopeTreeReadyEvent event) | |
| { | |
| updateCurrentScope(); | |
| } | |
| }); | |
| events_.addHandler( | |
| ShinyApplicationStatusEvent.TYPE, | |
| new ShinyApplicationStatusEvent.Handler() | |
| { | |
| @Override | |
| public void onShinyApplicationStatus( | |
| ShinyApplicationStatusEvent event) | |
| { | |
| // If the document appears to be inside the directory | |
| // associated with the event, update the view to match the | |
| // new state. | |
| if (getPath() != null && | |
| getPath().startsWith(event.getParams().getPath())) | |
| { | |
| String state = event.getParams().getState(); | |
| if (event.getParams().getViewerType() != | |
| ShinyViewerType.SHINY_VIEWER_PANE && | |
| event.getParams().getViewerType() != | |
| ShinyViewerType.SHINY_VIEWER_WINDOW) | |
| { | |
| // we can't control the state when it's not in an | |
| // RStudio-owned window, so treat the app as stopped | |
| state = ShinyApplicationParams.STATE_STOPPED; | |
| } | |
| view_.onShinyApplicationStateChanged(state); | |
| } | |
| } | |
| }); | |
| events_.addHandler( | |
| PlumberAPIStatusEvent.TYPE, | |
| new PlumberAPIStatusEvent.Handler() | |
| { | |
| @Override | |
| public void onPlumberAPIStatus(PlumberAPIStatusEvent event) | |
| { | |
| // If the document appears to be inside the directory | |
| // associated with the event, update the view to match the | |
| // new state. | |
| if (getPath() != null && | |
| getPath().startsWith(event.getParams().getPath())) | |
| { | |
| String state = event.getParams().getState(); | |
| if (event.getParams().getViewerType() != | |
| PlumberViewerType.PLUMBER_VIEWER_PANE && | |
| event.getParams().getViewerType() != | |
| PlumberViewerType.PLUMBER_VIEWER_WINDOW) | |
| { | |
| // we can't control the state when it's not in an | |
| // RStudio-owned window, so treat the app as stopped | |
| state = PlumberAPIParams.STATE_STOPPED; | |
| } | |
| view_.onPlumberAPIStateChanged(state); | |
| } | |
| } | |
| }); | |
| events_.addHandler( | |
| BreakpointsSavedEvent.TYPE, | |
| new BreakpointsSavedEvent.Handler() | |
| { | |
| @Override | |
| public void onBreakpointsSaved(BreakpointsSavedEvent event) | |
| { | |
| // if this document isn't ready for breakpoints, stop now | |
| if (docUpdateSentinel_ == null) | |
| { | |
| return; | |
| } | |
| for (Breakpoint breakpoint: event.breakpoints()) | |
| { | |
| // discard the breakpoint if it's not related to the file this | |
| // editor instance is concerned with | |
| if (!breakpoint.isInFile(getPath())) | |
| { | |
| continue; | |
| } | |
| // if the breakpoint was saved successfully, enable it on the | |
| // editor surface; otherwise, just remove it. | |
| if (event.successful()) | |
| { | |
| docDisplay_.addOrUpdateBreakpoint(breakpoint); | |
| } | |
| else | |
| { | |
| // Show a warning for breakpoints that didn't get set (unless | |
| // the reason the breakpoint wasn't set was that it's being | |
| // removed) | |
| if (breakpoint.getState() != Breakpoint.STATE_REMOVING) | |
| { | |
| view_.showWarningBar("Breakpoints can only be set inside "+ | |
| "the body of a function. "); | |
| } | |
| docDisplay_.removeBreakpoint(breakpoint); | |
| } | |
| } | |
| updateBreakpointWarningBar(); | |
| } | |
| }); | |
| events_.addHandler(ConvertToShinyDocEvent.TYPE, | |
| new ConvertToShinyDocEvent.Handler() | |
| { | |
| @Override | |
| public void onConvertToShinyDoc(ConvertToShinyDocEvent event) | |
| { | |
| if (getPath() != null && | |
| getPath().equals(event.getPath())) | |
| { | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return; | |
| String newYaml = rmarkdownHelper_.convertYamlToShinyDoc(yaml); | |
| applyRmdFrontMatter(newYaml); | |
| renderRmd(); | |
| } | |
| } | |
| }); | |
| events_.addHandler(RSConnectDeployInitiatedEvent.TYPE, | |
| new RSConnectDeployInitiatedEvent.Handler() | |
| { | |
| @Override | |
| public void onRSConnectDeployInitiated( | |
| RSConnectDeployInitiatedEvent event) | |
| { | |
| // no need to process this event if this target doesn't have a | |
| // path, or if the event's contents don't include additional | |
| // files. | |
| if (getPath() == null) | |
| return; | |
| // see if the event corresponds to a deployment of this file | |
| if (!getPath().equals(event.getSource().getSourceFile())) | |
| return; | |
| RSConnectPublishSettings settings = event.getSettings(); | |
| if (settings == null) | |
| return; | |
| // ignore deployments of static content generated from this | |
| // file | |
| if (settings.getAsStatic()) | |
| return; | |
| if (settings.getAdditionalFiles() != null && | |
| settings.getAdditionalFiles().size() > 0) | |
| { | |
| addAdditionalResourceFiles(settings.getAdditionalFiles()); | |
| } | |
| } | |
| }); | |
| events_.addHandler( | |
| SetEditorCommandBindingsEvent.TYPE, | |
| new SetEditorCommandBindingsEvent.Handler() | |
| { | |
| @Override | |
| public void onSetEditorCommandBindings(SetEditorCommandBindingsEvent event) | |
| { | |
| getDocDisplay().setEditorCommandBinding( | |
| event.getId(), | |
| event.getKeySequences()); | |
| } | |
| }); | |
| events_.addHandler( | |
| ResetEditorCommandsEvent.TYPE, | |
| new ResetEditorCommandsEvent.Handler() | |
| { | |
| @Override | |
| public void onResetEditorCommands(ResetEditorCommandsEvent event) | |
| { | |
| getDocDisplay().resetCommands(); | |
| } | |
| }); | |
| events_.addHandler(DocTabDragStateChangedEvent.TYPE, | |
| new DocTabDragStateChangedEvent.Handler() | |
| { | |
| @Override | |
| public void onDocTabDragStateChanged( | |
| DocTabDragStateChangedEvent e) | |
| { | |
| // enable text drag/drop only while we're not dragging tabs | |
| boolean enabled = e.getState() == | |
| DocTabDragStateChangedEvent.STATE_NONE; | |
| // disable drag/drop if disabled in preferences | |
| if (enabled) | |
| enabled = prefs.enableTextDrag().getValue(); | |
| // update editor surface | |
| docDisplay_.setDragEnabled(enabled); | |
| } | |
| }); | |
| events_.addHandler( | |
| AceAfterCommandExecutedEvent.TYPE, | |
| new AceAfterCommandExecutedEvent.Handler() | |
| { | |
| @Override | |
| public void onAceAfterCommandExecuted(AceAfterCommandExecutedEvent event) | |
| { | |
| JavaScriptObject data = event.getCommandData(); | |
| if (isIncrementalSearchCommand(data)) | |
| { | |
| String message = getIncrementalSearchMessage(); | |
| if (StringUtil.isNullOrEmpty(message)) | |
| { | |
| view_.getStatusBar().hideMessage(); | |
| } | |
| else | |
| { | |
| view_.getStatusBar().showMessage( | |
| getIncrementalSearchMessage(), | |
| 2000); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| static { | |
| initializeIncrementalSearch(); | |
| } | |
| private static final native String initializeIncrementalSearch() /*-{ | |
| var IncrementalSearch = $wnd.require("ace/incremental_search").IncrementalSearch; | |
| (function() { | |
| this.message = $entry(function(msg) { | |
| @org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTarget::setIncrementalSearchMessage(Ljava/lang/String;)(msg); | |
| }); | |
| }).call(IncrementalSearch.prototype); | |
| }-*/; | |
| private static final native boolean isIncrementalSearchCommand(JavaScriptObject data) /*-{ | |
| var command = data.command; | |
| if (command == null) | |
| return false; | |
| var result = | |
| command.name === "iSearch" || | |
| command.name === "iSearchBackwards" || | |
| command.isIncrementalSearchCommand === true; | |
| return result; | |
| }-*/; | |
| private static String sIncrementalSearchMessage_ = null; | |
| private static final void setIncrementalSearchMessage(String message) | |
| { | |
| sIncrementalSearchMessage_ = message; | |
| } | |
| private static final String getIncrementalSearchMessage() | |
| { | |
| return sIncrementalSearchMessage_; | |
| } | |
| private boolean moveCursorToNextSectionOrChunk(boolean includeSections) | |
| { | |
| Scope current = docDisplay_.getCurrentScope(); | |
| ScopeList scopes = new ScopeList(docDisplay_); | |
| Position cursorPos = docDisplay_.getCursorPosition(); | |
| int n = scopes.size(); | |
| for (int i = 0; i < n; i++) | |
| { | |
| Scope scope = scopes.get(i); | |
| if (!(scope.isChunk() || (scope.isSection() && includeSections))) | |
| continue; | |
| if (scope.equals(current)) | |
| continue; | |
| if (scope.getPreamble().isAfter(cursorPos)) | |
| { | |
| moveCursorToNextPrevSection(scope.getPreamble()); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private boolean moveCursorToPreviousSectionOrChunk(boolean includeSections) | |
| { | |
| ScopeList scopes = new ScopeList(docDisplay_); | |
| Position cursorPos = docDisplay_.getCursorPosition(); | |
| int n = scopes.size(); | |
| for (int i = n - 1; i >= 0; i--) | |
| { | |
| Scope scope = scopes.get(i); | |
| if (!(scope.isChunk() || (includeSections && scope.isSection()))) | |
| continue; | |
| if (scope.getPreamble().isBefore(cursorPos)) | |
| { | |
| moveCursorToNextPrevSection(scope.getPreamble()); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private void moveCursorToNextPrevSection(Position pos) | |
| { | |
| docDisplay_.setCursorPosition(pos); | |
| docDisplay_.moveCursorNearTop(5); | |
| } | |
| @Handler | |
| void onSwitchFocusSourceConsole() | |
| { | |
| if (docDisplay_.isFocused()) | |
| commands_.activateConsole().execute(); | |
| else | |
| commands_.activateSource().execute(); | |
| } | |
| @Handler | |
| void onGoToStartOfCurrentScope() | |
| { | |
| docDisplay_.focus(); | |
| Scope scope = docDisplay_.getCurrentScope(); | |
| if (scope != null) | |
| { | |
| Position position = Position.create( | |
| scope.getBodyStart().getRow(), | |
| scope.getBodyStart().getColumn() + 1); | |
| docDisplay_.setCursorPosition(position); | |
| } | |
| } | |
| @Handler | |
| void onGoToEndOfCurrentScope() | |
| { | |
| docDisplay_.focus(); | |
| Scope scope = docDisplay_.getCurrentScope(); | |
| if (scope != null) | |
| { | |
| Position end = scope.getEnd(); | |
| if (end != null) | |
| { | |
| Position position = Position.create( | |
| end.getRow(), | |
| Math.max(0, end.getColumn() - 1)); | |
| docDisplay_.setCursorPosition(position); | |
| } | |
| } | |
| } | |
| @Handler | |
| void onGoToNextSection() | |
| { | |
| if (docDisplay_.getFileType().canGoNextPrevSection()) | |
| { | |
| if (!moveCursorToNextSectionOrChunk(true)) | |
| docDisplay_.gotoPageDown(); | |
| } | |
| else | |
| { | |
| docDisplay_.gotoPageDown(); | |
| } | |
| } | |
| @Handler | |
| void onGoToPrevSection() | |
| { | |
| if (docDisplay_.getFileType().canGoNextPrevSection()) | |
| { | |
| if (!moveCursorToPreviousSectionOrChunk(true)) | |
| docDisplay_.gotoPageUp(); | |
| } | |
| else | |
| { | |
| docDisplay_.gotoPageUp(); | |
| } | |
| } | |
| @Handler | |
| void onGoToNextChunk() | |
| { | |
| moveCursorToNextSectionOrChunk(false); | |
| } | |
| @Handler | |
| void onGoToPrevChunk() | |
| { | |
| moveCursorToPreviousSectionOrChunk(false); | |
| } | |
| @Override | |
| public void recordCurrentNavigationPosition() | |
| { | |
| docDisplay_.recordCurrentNavigationPosition(); | |
| } | |
| @Override | |
| public void navigateToPosition(SourcePosition position, | |
| boolean recordCurrent) | |
| { | |
| docDisplay_.navigateToPosition(position, recordCurrent); | |
| } | |
| @Override | |
| public void navigateToPosition(SourcePosition position, | |
| boolean recordCurrent, | |
| boolean highlightLine) | |
| { | |
| docDisplay_.navigateToPosition(position, recordCurrent, highlightLine); | |
| } | |
| @Override | |
| public void restorePosition(SourcePosition position) | |
| { | |
| docDisplay_.restorePosition(position); | |
| } | |
| @Override | |
| public SourcePosition currentPosition() | |
| { | |
| Position cursor = docDisplay_.getCursorPosition(); | |
| if (docDisplay_.hasLineWidgets()) | |
| { | |
| // if we have line widgets, they create an non-reproducible scroll | |
| // position, so use the cursor position only | |
| return SourcePosition.create(cursor.getRow(), cursor.getColumn()); | |
| } | |
| return SourcePosition.create(getContext(), cursor.getRow(), | |
| cursor.getColumn(), docDisplay_.getScrollTop()); | |
| } | |
| @Override | |
| public boolean isAtSourceRow(SourcePosition position) | |
| { | |
| return docDisplay_.isAtSourceRow(position); | |
| } | |
| @Override | |
| public void setCursorPosition(Position position) | |
| { | |
| docDisplay_.setCursorPosition(position); | |
| } | |
| @Override | |
| public void ensureCursorVisible() | |
| { | |
| docDisplay_.ensureCursorVisible(); | |
| } | |
| @Override | |
| public void forceLineHighlighting() | |
| { | |
| docDisplay_.setHighlightSelectedLine(true); | |
| } | |
| @Override | |
| public void setSourceOnSave(boolean sourceOnSave) | |
| { | |
| view_.getSourceOnSave().setValue(sourceOnSave, true); | |
| } | |
| @Override | |
| public void highlightDebugLocation( | |
| SourcePosition startPos, | |
| SourcePosition endPos, | |
| boolean executing) | |
| { | |
| debugStartPos_ = startPos; | |
| debugEndPos_ = endPos; | |
| docDisplay_.highlightDebugLocation(startPos, endPos, executing); | |
| updateDebugWarningBar(); | |
| } | |
| @Override | |
| public void endDebugHighlighting() | |
| { | |
| docDisplay_.endDebugHighlighting(); | |
| debugStartPos_ = null; | |
| debugEndPos_ = null; | |
| updateDebugWarningBar(); | |
| } | |
| @Override | |
| public void beginCollabSession(CollabEditStartParams params) | |
| { | |
| // the server may notify us of a collab session we're already | |
| // participating in; this is okay | |
| if (docDisplay_.hasActiveCollabSession()) | |
| { | |
| return; | |
| } | |
| // were we waiting to process another set of params when these arrived? | |
| boolean paramQueueClear = queuedCollabParams_ == null; | |
| // save params | |
| queuedCollabParams_ = params; | |
| // if we're not waiting for another set of params to resolve, and we're | |
| // the active doc, process these params immediately | |
| if (paramQueueClear && isActiveDocument()) | |
| { | |
| beginQueuedCollabSession(); | |
| } | |
| } | |
| @Override | |
| public void endCollabSession() | |
| { | |
| if (docDisplay_.hasActiveCollabSession()) | |
| docDisplay_.endCollabSession(); | |
| // a collaboration session may have come and gone while the tab was not | |
| // focused | |
| queuedCollabParams_ = null; | |
| } | |
| private void beginQueuedCollabSession() | |
| { | |
| // do nothing if we don't have an active path | |
| if (docUpdateSentinel_ == null || docUpdateSentinel_.getPath() == null) | |
| return; | |
| // do nothing if we don't have queued params | |
| final CollabEditStartParams params = queuedCollabParams_; | |
| if (params == null) | |
| return; | |
| // if we have local changes, and we're not the master copy nor rejoining a | |
| // previous edit session, we need to prompt the user | |
| if (dirtyState().getValue() && !params.isMaster() && | |
| !params.isRejoining()) | |
| { | |
| String filename = | |
| FilePathUtils.friendlyFileName(docUpdateSentinel_.getPath()); | |
| globalDisplay_.showYesNoMessage( | |
| GlobalDisplay.MSG_QUESTION, | |
| "Join Edit Session", | |
| "You have unsaved changes to " + filename + ", but another " + | |
| "user is editing the file. Do you want to discard your " + | |
| "changes and join their edit session, or make your own copy " + | |
| "of the file to work on?", | |
| false, // includeCancel | |
| new Operation() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| docDisplay_.beginCollabSession(params, dirtyState_); | |
| queuedCollabParams_ = null; | |
| } | |
| }, | |
| new Operation() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| // open a new tab for the user's local changes | |
| events_.fireEvent(new NewWorkingCopyEvent(fileType_, | |
| docUpdateSentinel_.getPath(), | |
| docUpdateSentinel_.getContents())); | |
| // let the collab session initiate in this tab | |
| docDisplay_.beginCollabSession(params, dirtyState_); | |
| queuedCollabParams_ = null; | |
| } | |
| }, | |
| null, // cancelOperation, | |
| "Discard and Join", | |
| "Work on a Copy", | |
| true // yesIsDefault | |
| ); | |
| } | |
| else | |
| { | |
| // just begin the session right away | |
| docDisplay_.beginCollabSession(params, dirtyState_); | |
| queuedCollabParams_ = null; | |
| } | |
| } | |
| private void updateDebugWarningBar() | |
| { | |
| // show the warning bar if we're debugging and the document is dirty | |
| if (debugStartPos_ != null && | |
| dirtyState().getValue() && | |
| !isDebugWarningVisible_) | |
| { | |
| view_.showWarningBar("Debug lines may not match because the file contains unsaved changes."); | |
| isDebugWarningVisible_ = true; | |
| } | |
| // hide the warning bar if the dirty state or debug state change | |
| else if (isDebugWarningVisible_ && | |
| (debugStartPos_ == null || dirtyState().getValue() == false)) | |
| { | |
| view_.hideWarningBar(); | |
| // if we're still debugging, start highlighting the line again | |
| if (debugStartPos_ != null) | |
| { | |
| docDisplay_.highlightDebugLocation( | |
| debugStartPos_, | |
| debugEndPos_, false); | |
| } | |
| isDebugWarningVisible_ = false; | |
| } | |
| } | |
| public void showWarningMessage(String message) | |
| { | |
| view_.showWarningBar(message); | |
| } | |
| public void showRequiredPackagesMissingWarning(List<String> packages) | |
| { | |
| view_.showRequiredPackagesMissingWarning(packages); | |
| } | |
| private void jumpToPreviousFunction() | |
| { | |
| Scope jumpTo = scopeHelper_.getPreviousFunction( | |
| docDisplay_.getCursorPosition()); | |
| if (jumpTo != null) | |
| docDisplay_.navigateToPosition(toSourcePosition(jumpTo), true); | |
| } | |
| private void jumpToNextFunction() | |
| { | |
| Scope jumpTo = scopeHelper_.getNextFunction( | |
| docDisplay_.getCursorPosition()); | |
| if (jumpTo != null) | |
| docDisplay_.navigateToPosition(toSourcePosition(jumpTo), true); | |
| } | |
| public void initialize(final SourceDocument document, | |
| FileSystemContext fileContext, | |
| FileType type, | |
| Provider<String> defaultNameProvider) | |
| { | |
| id_ = document.getId(); | |
| fileContext_ = fileContext; | |
| fileType_ = (TextFileType) type; | |
| codeExecution_ = new EditingTargetCodeExecution(this, docDisplay_, getId(), | |
| this); | |
| extendedType_ = document.getExtendedType(); | |
| extendedType_ = rmarkdownHelper_.detectExtendedType(document.getContents(), | |
| extendedType_, | |
| fileType_); | |
| themeHelper_ = new TextEditingTargetThemeHelper(this, events_); | |
| docUpdateSentinel_ = new DocUpdateSentinel( | |
| server_, | |
| docDisplay_, | |
| document, | |
| globalDisplay_.getProgressIndicator("Save File"), | |
| dirtyState_, | |
| events_); | |
| view_ = new TextEditingTargetWidget(this, | |
| docUpdateSentinel_, | |
| commands_, | |
| prefs_, | |
| fileTypeRegistry_, | |
| docDisplay_, | |
| fileType_, | |
| extendedType_, | |
| events_, | |
| session_, | |
| server_); | |
| roxygenHelper_ = new RoxygenHelper(docDisplay_, view_); | |
| packageDependencyHelper_ = new TextEditingTargetPackageDependencyHelper(this, docUpdateSentinel_, docDisplay_); | |
| // create notebook and forward resize events | |
| chunks_ = new TextEditingTargetChunks(this); | |
| notebook_ = new TextEditingTargetNotebook(this, chunks_, view_, | |
| docDisplay_, dirtyState_, docUpdateSentinel_, document, | |
| releaseOnDismiss_, dependencyManager_); | |
| view_.addResizeHandler(notebook_); | |
| // apply project properties | |
| projConfig_ = document.getProjectConfig(); | |
| if (projConfig_ != null) | |
| { | |
| docDisplay_.setUseSoftTabs(projConfig_.useSoftTabs()); | |
| docDisplay_.setTabSize(projConfig_.getTabSize()); | |
| } | |
| // ensure that Makefile and Makevars always use tabs | |
| name_.addValueChangeHandler(new ValueChangeHandler<String>() { | |
| @Override | |
| public void onValueChange(ValueChangeEvent<String> event) | |
| { | |
| FileSystemItem item = FileSystemItem.createFile(event.getValue()); | |
| if (shouldEnforceHardTabs(item)) | |
| docDisplay_.setUseSoftTabs(false); | |
| } | |
| }); | |
| name_.setValue(getNameFromDocument(document, defaultNameProvider), true); | |
| String contents = document.getContents(); | |
| docDisplay_.setCode(contents, false); | |
| // Discover dependencies on file first open. | |
| packageDependencyHelper_.discoverPackageDependencies(); | |
| // Load and apply folds. | |
| final ArrayList<Fold> folds = Fold.decode(document.getFoldSpec()); | |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| for (Fold fold : folds) | |
| docDisplay_.addFold(fold.getRange()); | |
| } | |
| }); | |
| // Load and apply Vim marks (if they exist). | |
| if (document.getProperties().hasKey("marks")) | |
| { | |
| final String marksSpec = document.getProperties().getString("marks"); | |
| final JsMap<Position> marks = VimMarks.decode(marksSpec); | |
| // Time out the marks setting just to avoid conflict with other | |
| // mutations of the editor. | |
| new Timer() | |
| { | |
| @Override | |
| public void run() | |
| { | |
| docDisplay_.setMarks(marks); | |
| } | |
| }.schedule(100); | |
| } | |
| registerPrefs(releaseOnDismiss_, prefs_, projConfig_, docDisplay_, document); | |
| // Initialize sourceOnSave, and keep it in sync | |
| view_.getSourceOnSave().setValue(document.sourceOnSave(), false); | |
| view_.getSourceOnSave().addValueChangeHandler(new ValueChangeHandler<Boolean>() | |
| { | |
| public void onValueChange(ValueChangeEvent<Boolean> event) | |
| { | |
| docUpdateSentinel_.setSourceOnSave( | |
| event.getValue(), | |
| globalDisplay_.getProgressIndicator("Error Saving Setting")); | |
| } | |
| }); | |
| if (document.isDirty()) | |
| dirtyState_.markDirty(false); | |
| else | |
| dirtyState_.markClean(); | |
| docDisplay_.addValueChangeHandler(new ValueChangeHandler<Void>() | |
| { | |
| public void onValueChange(ValueChangeEvent<Void> event) | |
| { | |
| dirtyState_.markDirty(true); | |
| docDisplay_.clearSelectionHistory(); | |
| } | |
| }); | |
| docDisplay_.addFocusHandler(new FocusHandler() | |
| { | |
| public void onFocus(FocusEvent event) | |
| { | |
| // let anyone listening know this doc just got focus | |
| events_.fireEvent(new DocFocusedEvent(getPath(), getId())); | |
| if (queuedCollabParams_ != null) | |
| { | |
| // join an in-progress collab session if we aren't already part | |
| // of one | |
| if (docDisplay_ != null && !docDisplay_.hasActiveCollabSession()) | |
| { | |
| beginQueuedCollabSession(); | |
| } | |
| } | |
| // check to see if the file's been saved externally--we do this even | |
| // in a collaborative editing session so we can get delete | |
| // notifications | |
| Scheduler.get().scheduleFixedDelay(new RepeatingCommand() | |
| { | |
| public boolean execute() | |
| { | |
| if (view_.isAttached()) | |
| checkForExternalEdit(); | |
| return false; | |
| } | |
| }, 500); | |
| } | |
| }); | |
| if (fileType_.isR()) | |
| { | |
| docDisplay_.addBreakpointSetHandler(new BreakpointSetEvent.Handler() | |
| { | |
| @Override | |
| public void onBreakpointSet(BreakpointSetEvent event) | |
| { | |
| if (event.isSet()) | |
| { | |
| Breakpoint breakpoint = null; | |
| // don't set breakpoints in Plumber documents | |
| if (SourceDocument.isPlumberFile(extendedType_)) | |
| { | |
| view_.showWarningBar("Breakpoints not supported in Plumber API files."); | |
| return; | |
| } | |
| // don't try to set breakpoints in unsaved code | |
| if (isNewDoc()) | |
| { | |
| view_.showWarningBar("Breakpoints cannot be set until " + | |
| "the file is saved."); | |
| return; | |
| } | |
| // don't try to set breakpoints if the R version is too old | |
| if (!session_.getSessionInfo().getHaveSrcrefAttribute()) | |
| { | |
| view_.showWarningBar("Editor breakpoints require R 2.14 " + | |
| "or newer."); | |
| return; | |
| } | |
| Position breakpointPosition = | |
| Position.create(event.getLineNumber() - 1, 1); | |
| // if we're not in function scope, or this is a Shiny file, | |
| // set a top-level (aka. Shiny-deferred) breakpoint | |
| ScopeFunction innerFunction = null; | |
| if (extendedType_ == null || | |
| !extendedType_.startsWith(SourceDocument.XT_SHINY_PREFIX)) | |
| innerFunction = docDisplay_.getFunctionAtPosition( | |
| breakpointPosition, false); | |
| if (innerFunction == null || !innerFunction.isFunction() || | |
| StringUtil.isNullOrEmpty(innerFunction.getFunctionName())) | |
| { | |
| breakpoint = breakpointManager_.setTopLevelBreakpoint( | |
| getPath(), | |
| event.getLineNumber()); | |
| } | |
| // the scope tree will find nested functions, but in R these | |
| // are addressable only as substeps of the parent function. | |
| // keep walking up the scope tree until we've reached the top | |
| // level function. | |
| else | |
| { | |
| while (innerFunction.getParentScope() != null && | |
| innerFunction.getParentScope().isFunction()) | |
| { | |
| innerFunction = (ScopeFunction) innerFunction.getParentScope(); | |
| } | |
| String functionName = innerFunction.getFunctionName(); | |
| breakpoint = breakpointManager_.setBreakpoint( | |
| getPath(), | |
| functionName, | |
| event.getLineNumber(), | |
| dirtyState().getValue() == false); | |
| } | |
| docDisplay_.addOrUpdateBreakpoint(breakpoint); | |
| } | |
| else | |
| { | |
| breakpointManager_.removeBreakpoint(event.getBreakpointId()); | |
| } | |
| updateBreakpointWarningBar(); | |
| } | |
| }); | |
| docDisplay_.addBreakpointMoveHandler(new BreakpointMoveEvent.Handler() | |
| { | |
| @Override | |
| public void onBreakpointMove(BreakpointMoveEvent event) | |
| { | |
| breakpointManager_.moveBreakpoint(event.getBreakpointId()); | |
| } | |
| }); | |
| } | |
| // validate required components (e.g. Tex, knitr, C++ etc.) | |
| checkCompilePdfDependencies(); | |
| rmarkdownHelper_.verifyPrerequisites(view_, fileType_); | |
| syncFontSize(releaseOnDismiss_, events_, view_, fontSizeManager_); | |
| final String rTypeId = FileTypeRegistry.R.getTypeId(); | |
| releaseOnDismiss_.add(prefs_.softWrapRFiles().addValueChangeHandler( | |
| new ValueChangeHandler<Boolean>() | |
| { | |
| public void onValueChange(ValueChangeEvent<Boolean> evt) | |
| { | |
| if (fileType_.getTypeId().equals(rTypeId)) | |
| view_.adaptToFileType(fileType_); | |
| } | |
| } | |
| )); | |
| releaseOnDismiss_.add(events_.addHandler(FileChangeEvent.TYPE, | |
| new FileChangeHandler() { | |
| @Override | |
| public void onFileChange(FileChangeEvent event) | |
| { | |
| // screen out adds and events that aren't for our path | |
| FileChange fileChange = event.getFileChange(); | |
| if (fileChange.getType() == FileChange.ADD) | |
| return; | |
| else if (!fileChange.getFile().getPath().equals(getPath())) | |
| return; | |
| // always check for changes if this is the active editor | |
| if (isActiveDocument()) | |
| checkForExternalEdit(); | |
| // also check for changes on modifications if we are not dirty | |
| // note that we don't check for changes on removed files because | |
| // this will show a confirmation dialog | |
| else if (event.getFileChange().getType() == FileChange.MODIFIED && | |
| dirtyState().getValue() == false) | |
| { | |
| checkForExternalEdit(); | |
| } | |
| } | |
| })); | |
| spelling_ = new TextEditingTargetSpelling(docDisplay_, | |
| docUpdateSentinel_); | |
| // show/hide the debug toolbar when the dirty state changes. (note: | |
| // this doesn't yet handle the case where the user saves the document, | |
| // in which case we should still show some sort of warning.) | |
| dirtyState().addValueChangeHandler(new ValueChangeHandler<Boolean>() | |
| { | |
| public void onValueChange(ValueChangeEvent<Boolean> evt) | |
| { | |
| updateDebugWarningBar(); | |
| } | |
| } | |
| ); | |
| // find all of the debug breakpoints set in this document and replay them | |
| // onto the edit surface | |
| ArrayList<Breakpoint> breakpoints = | |
| breakpointManager_.getBreakpointsInFile(getPath()); | |
| for (Breakpoint breakpoint: breakpoints) | |
| { | |
| docDisplay_.addOrUpdateBreakpoint(breakpoint); | |
| } | |
| if (extendedType_.equals(SourceDocument.XT_RMARKDOWN)) | |
| { | |
| // populate the popup menu with a list of available formats | |
| updateRmdFormatList(); | |
| setRMarkdownBehaviorEnabled(true); | |
| } | |
| view_.addRmdFormatChangedHandler(new RmdOutputFormatChangedEvent.Handler() | |
| { | |
| @Override | |
| public void onRmdOutputFormatChanged(RmdOutputFormatChangedEvent event) | |
| { | |
| setRmdFormat(event.getFormat()); | |
| } | |
| }); | |
| docDisplay_.addCursorChangedHandler(new CursorChangedHandler() | |
| { | |
| Timer timer_ = new Timer() | |
| { | |
| @Override | |
| public void run() | |
| { | |
| HashMap<String, String> properties = new HashMap<String, String>(); | |
| properties.put( | |
| PROPERTY_CURSOR_POSITION, | |
| Position.serialize(docDisplay_.getCursorPosition())); | |
| properties.put( | |
| PROPERTY_SCROLL_LINE, | |
| String.valueOf(docDisplay_.getFirstFullyVisibleRow())); | |
| docUpdateSentinel_.modifyProperties(properties); | |
| } | |
| }; | |
| @Override | |
| public void onCursorChanged(CursorChangedEvent event) | |
| { | |
| if (prefs_.restoreSourceDocumentCursorPosition().getValue()) | |
| timer_.schedule(1000); | |
| } | |
| }); | |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| if (!prefs_.restoreSourceDocumentCursorPosition().getValue()) | |
| return; | |
| String cursorPosition = docUpdateSentinel_.getProperty( | |
| PROPERTY_CURSOR_POSITION, | |
| ""); | |
| if (StringUtil.isNullOrEmpty(cursorPosition)) | |
| return; | |
| int scrollLine = StringUtil.parseInt( | |
| docUpdateSentinel_.getProperty(PROPERTY_SCROLL_LINE, "0"), | |
| 0); | |
| Position position = Position.deserialize(cursorPosition); | |
| docDisplay_.setCursorPosition(position); | |
| docDisplay_.scrollToLine(scrollLine, false); | |
| docDisplay_.setScrollLeft(0); | |
| } | |
| }); | |
| syncPublishPath(document.getPath()); | |
| initStatusBar(); | |
| } | |
| private void updateBreakpointWarningBar() | |
| { | |
| // check to see if there are any inactive breakpoints in this file | |
| boolean hasInactiveBreakpoints = false; | |
| boolean hasDebugPendingBreakpoints = false; | |
| boolean hasPackagePendingBreakpoints = false; | |
| String pendingPackageName = ""; | |
| ArrayList<Breakpoint> breakpoints = | |
| breakpointManager_.getBreakpointsInFile(getPath()); | |
| for (Breakpoint breakpoint: breakpoints) | |
| { | |
| if (breakpoint.getState() == Breakpoint.STATE_INACTIVE) | |
| { | |
| if (breakpoint.isPendingDebugCompletion()) | |
| { | |
| hasDebugPendingBreakpoints = true; | |
| } | |
| else if (breakpoint.isPackageBreakpoint()) | |
| { | |
| hasPackagePendingBreakpoints = true; | |
| pendingPackageName = breakpoint.getPackageName(); | |
| } | |
| else | |
| { | |
| hasInactiveBreakpoints = true; | |
| } | |
| break; | |
| } | |
| } | |
| boolean showWarning = hasDebugPendingBreakpoints || | |
| hasInactiveBreakpoints || | |
| hasPackagePendingBreakpoints; | |
| if (showWarning && !isBreakpointWarningVisible_) | |
| { | |
| String message = ""; | |
| if (hasDebugPendingBreakpoints) | |
| { | |
| message = "Breakpoints will be activated when the file or " + | |
| "function is finished executing."; | |
| } | |
| else if (isPackageFile()) | |
| { | |
| message = "Breakpoints will be activated when the package is " + | |
| "built and reloaded."; | |
| } | |
| else if (hasPackagePendingBreakpoints) | |
| { | |
| message = "Breakpoints will be activated when an updated version " + | |
| "of the " + pendingPackageName + " package is loaded"; | |
| } | |
| else | |
| { | |
| message = "Breakpoints will be activated when this file is " + | |
| "sourced."; | |
| } | |
| view_.showWarningBar(message); | |
| isBreakpointWarningVisible_ = true; | |
| } | |
| else if (!showWarning && isBreakpointWarningVisible_) | |
| { | |
| hideBreakpointWarningBar(); | |
| } | |
| } | |
| private void hideBreakpointWarningBar() | |
| { | |
| if (isBreakpointWarningVisible_) | |
| { | |
| view_.hideWarningBar(); | |
| isBreakpointWarningVisible_ = false; | |
| } | |
| } | |
| private boolean isPackageFile() | |
| { | |
| // not a package file if we're not in package development mode | |
| String type = session_.getSessionInfo().getBuildToolsType(); | |
| if (!type.equals(SessionInfo.BUILD_TOOLS_PACKAGE)) | |
| { | |
| return false; | |
| } | |
| // get the directory associated with the project and see if the file is | |
| // inside that directory | |
| FileSystemItem projectDir = session_.getSessionInfo() | |
| .getActiveProjectDir(); | |
| return getPath().startsWith(projectDir.getPath() + "/R"); | |
| } | |
| private boolean isPackageDocumentationFile() | |
| { | |
| if (getPath() == null) | |
| { | |
| return false; | |
| } | |
| String type = session_.getSessionInfo().getBuildToolsType(); | |
| if (!type.equals(SessionInfo.BUILD_TOOLS_PACKAGE)) | |
| { | |
| return false; | |
| } | |
| FileSystemItem srcFile = FileSystemItem.createFile(getPath()); | |
| FileSystemItem projectDir = session_.getSessionInfo() | |
| .getActiveProjectDir(); | |
| if (srcFile.getPath().startsWith(projectDir.getPath() + "/vignettes")) | |
| return true; | |
| else if (srcFile.getParentPathString().equals(projectDir.getPath()) && | |
| srcFile.getExtension().toLowerCase().equals(".md")) | |
| return true; | |
| else | |
| return false; | |
| } | |
| private void checkCompilePdfDependencies() | |
| { | |
| compilePdfHelper_.checkCompilers(view_, fileType_); | |
| } | |
| private void initStatusBar() | |
| { | |
| statusBar_ = view_.getStatusBar(); | |
| docDisplay_.addCursorChangedHandler(new CursorChangedHandler() | |
| { | |
| public void onCursorChanged(CursorChangedEvent event) | |
| { | |
| updateStatusBarPosition(); | |
| if (docDisplay_.isScopeTreeReady(event.getPosition().getRow())) | |
| updateCurrentScope(); | |
| } | |
| }); | |
| updateStatusBarPosition(); | |
| updateStatusBarLanguage(); | |
| // build file type menu dynamically (so it can change according | |
| // to whether e.g. knitr is installed) | |
| statusBar_.getLanguage().addMouseDownHandler(new MouseDownHandler() { | |
| @Override | |
| public void onMouseDown(MouseDownEvent event) | |
| { | |
| // build menu with all file types - also track whether we need | |
| // to add the current type (may be the case for types which we | |
| // support but don't want to expose on the menu -- e.g. Rmd | |
| // files when knitr isn't installed) | |
| boolean addCurrentType = true; | |
| final StatusBarPopupMenu menu = new StatusBarPopupMenu(); | |
| TextFileType[] fileTypes = fileTypeCommands_.statusBarFileTypes(); | |
| for (TextFileType type : fileTypes) | |
| { | |
| menu.addItem(createMenuItemForType(type)); | |
| if (addCurrentType && type.equals(fileType_)) | |
| addCurrentType = false; | |
| } | |
| // add the current type if isn't on the menu | |
| if (addCurrentType) | |
| menu.addItem(createMenuItemForType(fileType_)); | |
| // show the menu | |
| menu.showRelativeToUpward((UIObject) statusBar_.getLanguage(), | |
| true); | |
| } | |
| }); | |
| statusBar_.getScope().addMouseDownHandler(new MouseDownHandler() | |
| { | |
| public void onMouseDown(MouseDownEvent event) | |
| { | |
| // Unlike the other status bar elements, the function outliner | |
| // needs its menu built on demand | |
| JsArray<Scope> tree = docDisplay_.getScopeTree(); | |
| final StatusBarPopupMenu menu = new StatusBarPopupMenu(); | |
| MenuItem defaultItem = null; | |
| if (fileType_.isRpres()) | |
| { | |
| String path = docUpdateSentinel_.getPath(); | |
| if (path != null) | |
| { | |
| presentationHelper_.buildSlideMenu( | |
| docUpdateSentinel_.getPath(), | |
| dirtyState_.getValue(), | |
| TextEditingTarget.this, | |
| new CommandWithArg<StatusBarPopupRequest>() { | |
| @Override | |
| public void execute(StatusBarPopupRequest request) | |
| { | |
| showStatusBarPopupMenu(request); | |
| } | |
| }); | |
| } | |
| } | |
| else | |
| { | |
| defaultItem = addFunctionsToMenu( | |
| menu, tree, "", docDisplay_.getCurrentScope(), true); | |
| showStatusBarPopupMenu(new StatusBarPopupRequest(menu, | |
| defaultItem)); | |
| } | |
| } | |
| }); | |
| } | |
| private void showStatusBarPopupMenu(StatusBarPopupRequest popupRequest) | |
| { | |
| final StatusBarPopupMenu menu = popupRequest.getMenu(); | |
| MenuItem defaultItem = popupRequest.getDefaultMenuItem(); | |
| if (defaultItem != null) | |
| { | |
| menu.selectItem(defaultItem); | |
| Scheduler.get().scheduleFinally(new RepeatingCommand() | |
| { | |
| public boolean execute() | |
| { | |
| menu.ensureSelectedIsVisible(); | |
| return false; | |
| } | |
| }); | |
| } | |
| menu.showRelativeToUpward((UIObject) statusBar_.getScope(), false); | |
| } | |
| private MenuItem createMenuItemForType(final TextFileType type) | |
| { | |
| SafeHtmlBuilder labelBuilder = new SafeHtmlBuilder(); | |
| labelBuilder.appendEscaped(type.getLabel()); | |
| MenuItem menuItem = new MenuItem( | |
| labelBuilder.toSafeHtml(), | |
| new Command() | |
| { | |
| public void execute() | |
| { | |
| docUpdateSentinel_.changeFileType( | |
| type.getTypeId(), | |
| new SaveProgressIndicator(null, type, null)); | |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() { | |
| @Override | |
| public void execute() | |
| { | |
| focus(); | |
| } | |
| }); | |
| } | |
| }); | |
| return menuItem; | |
| } | |
| private void addScopeStyle(MenuItem item, Scope scope) | |
| { | |
| if (scope.isSection()) | |
| item.getElement().getStyle().setFontWeight(FontWeight.BOLD); | |
| } | |
| private MenuItem addFunctionsToMenu(StatusBarPopupMenu menu, | |
| final JsArray<Scope> funcs, | |
| String indent, | |
| Scope defaultFunction, | |
| boolean includeNoFunctionsMessage) | |
| { | |
| MenuItem defaultMenuItem = null; | |
| if (funcs.length() == 0 && includeNoFunctionsMessage) | |
| { | |
| String type = fileType_.canExecuteChunks() ? "chunks" : "functions"; | |
| MenuItem noFunctions = new MenuItem("(No " + type + " defined)", | |
| false, | |
| (Command) null); | |
| noFunctions.setEnabled(false); | |
| noFunctions.getElement().addClassName("disabled"); | |
| menu.addItem(noFunctions); | |
| } | |
| for (int i = 0; i < funcs.length(); i++) | |
| { | |
| final Scope func = funcs.get(i); | |
| String childIndent = indent; | |
| if (!StringUtil.isNullOrEmpty(func.getLabel())) | |
| { | |
| SafeHtmlBuilder labelBuilder = new SafeHtmlBuilder(); | |
| labelBuilder.appendHtmlConstant(indent); | |
| labelBuilder.appendEscaped(func.getLabel()); | |
| final MenuItem menuItem = new MenuItem( | |
| labelBuilder.toSafeHtml(), | |
| new Command() | |
| { | |
| public void execute() | |
| { | |
| docDisplay_.navigateToPosition(toSourcePosition(func), | |
| true); | |
| } | |
| }); | |
| addScopeStyle(menuItem, func); | |
| menu.addItem(menuItem); | |
| childIndent = indent + " "; | |
| if (defaultFunction != null && defaultMenuItem == null && | |
| func.getLabel() == defaultFunction.getLabel() && | |
| func.getPreamble().getRow() == defaultFunction.getPreamble().getRow() && | |
| func.getPreamble().getColumn() == defaultFunction.getPreamble().getColumn()) | |
| { | |
| defaultMenuItem = menuItem; | |
| } | |
| } | |
| MenuItem childDefaultMenuItem = addFunctionsToMenu( | |
| menu, | |
| func.getChildren(), | |
| childIndent, | |
| defaultMenuItem == null ? defaultFunction : null, | |
| false); | |
| if (childDefaultMenuItem != null) | |
| defaultMenuItem = childDefaultMenuItem; | |
| } | |
| return defaultMenuItem; | |
| } | |
| private void updateStatusBarLanguage() | |
| { | |
| statusBar_.getLanguage().setValue(fileType_.getLabel()); | |
| boolean canShowScope = fileType_.canShowScopeTree(); | |
| statusBar_.setScopeVisible(canShowScope); | |
| } | |
| private void updateStatusBarPosition() | |
| { | |
| Position pos = docDisplay_.getCursorPosition(); | |
| statusBar_.getPosition().setValue((pos.getRow() + 1) + ":" + | |
| (pos.getColumn() + 1)); | |
| } | |
| private void updateCurrentScope() | |
| { | |
| if (fileType_ == null || !fileType_.canShowScopeTree()) | |
| return; | |
| // special handing for presentations since we extract | |
| // the slide structure in a different manner than | |
| // the editor scope trees | |
| if (fileType_.isRpres()) | |
| { | |
| statusBar_.getScope().setValue( | |
| presentationHelper_.getCurrentSlide()); | |
| statusBar_.setScopeType(StatusBar.SCOPE_SLIDE); | |
| } | |
| else | |
| { | |
| Scope scope = docDisplay_.getCurrentScope(); | |
| String label = scope != null | |
| ? scope.getLabel() | |
| : null; | |
| statusBar_.getScope().setValue(label); | |
| if (scope != null) | |
| { | |
| boolean useChunk = | |
| scope.isChunk() || | |
| (fileType_.isRnw() && scope.isTopLevel()); | |
| if (useChunk) | |
| statusBar_.setScopeType(StatusBar.SCOPE_CHUNK); | |
| else if (scope.isNamespace()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_NAMESPACE); | |
| else if (scope.isClass()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_CLASS); | |
| else if (scope.isSection()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_SECTION); | |
| else if (scope.isTopLevel()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_TOP_LEVEL); | |
| else if (scope.isFunction()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_FUNCTION); | |
| else if (scope.isLambda()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_LAMBDA); | |
| else if (scope.isAnon()) | |
| statusBar_.setScopeType(StatusBar.SCOPE_ANON); | |
| } | |
| } | |
| } | |
| private String getNameFromDocument(SourceDocument document, | |
| Provider<String> defaultNameProvider) | |
| { | |
| if (document.getPath() != null) | |
| return FileSystemItem.getNameFromPath(document.getPath()); | |
| String name = document.getProperties().getString("tempName"); | |
| if (!StringUtil.isNullOrEmpty(name)) | |
| return name; | |
| String defaultName = defaultNameProvider.get(); | |
| docUpdateSentinel_.setProperty("tempName", defaultName, null); | |
| return defaultName; | |
| } | |
| public long getFileSizeLimit() | |
| { | |
| return 5 * 1024 * 1024; | |
| } | |
| public long getLargeFileSize() | |
| { | |
| return 2 * 1024 * 1024; | |
| } | |
| public void insertCode(String source, boolean blockMode) | |
| { | |
| docDisplay_.insertCode(source, blockMode); | |
| } | |
| public HashSet<AppCommand> getSupportedCommands() | |
| { | |
| // start with the set of commands supported by the file type | |
| HashSet<AppCommand> commands = fileType_.getSupportedCommands(commands_); | |
| // if the file has a path, it can also be renamed | |
| if (getPath() != null) | |
| { | |
| commands.add(commands_.renameSourceDoc()); | |
| } | |
| return commands; | |
| } | |
| @Override | |
| public void manageCommands() | |
| { | |
| if (fileType_.isRmd()) | |
| notebook_.manageCommands(); | |
| } | |
| @Override | |
| public boolean canCompilePdf() | |
| { | |
| return fileType_.canCompilePDF(); | |
| } | |
| @Override | |
| public void verifyCppPrerequisites() | |
| { | |
| // NOTE: will be a no-op for non-c/c++ file types | |
| cppHelper_.checkBuildCppDependencies(this, view_, fileType_); | |
| } | |
| @Override | |
| public void verifyPythonPrerequisites() | |
| { | |
| // TODO: ensure 'reticulate' installed | |
| } | |
| @Override | |
| public void verifyD3Prerequisites() | |
| { | |
| verifyD3Prequisites(null); | |
| } | |
| private void verifyD3Prequisites(final Command command) | |
| { | |
| dependencyManager_.withR2D3("Previewing D3 scripts", new Command() { | |
| @Override | |
| public void execute() { | |
| if (command != null) | |
| command.execute(); | |
| } | |
| }); | |
| } | |
| @Override | |
| public void verifySqlPrerequisites() | |
| { | |
| verifySqlPrequisites(null); | |
| } | |
| private void verifySqlPrequisites(final Command command) | |
| { | |
| dependencyManager_.withDBI("Previewing SQL scripts", new Command() { | |
| @Override | |
| public void execute() { | |
| if (command != null) | |
| command.execute(); | |
| } | |
| }); | |
| } | |
| public void focus() | |
| { | |
| docDisplay_.focus(); | |
| } | |
| public String getSelectedText() | |
| { | |
| if (docDisplay_.hasSelection()) | |
| return docDisplay_.getSelectionValue(); | |
| else | |
| return ""; | |
| } | |
| public HandlerRegistration addEnsureVisibleHandler(EnsureVisibleHandler handler) | |
| { | |
| return view_.addEnsureVisibleHandler(handler); | |
| } | |
| public HandlerRegistration addEnsureHeightHandler(EnsureHeightHandler handler) | |
| { | |
| return view_.addEnsureHeightHandler(handler); | |
| } | |
| public HandlerRegistration addCloseHandler(CloseHandler<java.lang.Void> handler) | |
| { | |
| return handlers_.addHandler(CloseEvent.getType(), handler); | |
| } | |
| public HandlerRegistration addEditorThemeStyleChangedHandler( | |
| EditorThemeStyleChangedEvent.Handler handler) | |
| { | |
| return themeHelper_.addEditorThemeStyleChangedHandler(handler); | |
| } | |
| public HandlerRegistration addInterruptChunkHandler(InterruptChunkEvent.Handler handler) | |
| { | |
| return handlers_.addHandler(InterruptChunkEvent.TYPE, handler); | |
| } | |
| public void fireEvent(GwtEvent<?> event) | |
| { | |
| handlers_.fireEvent(event); | |
| } | |
| public void onActivate() | |
| { | |
| // IMPORTANT NOTE: most of this logic is duplicated in | |
| // CodeBrowserEditingTarget (no straightforward way to create a | |
| // re-usable implementation) so changes here need to be synced | |
| // If we're already hooked up for some reason, unhook. | |
| // This shouldn't happen though. | |
| if (commandHandlerReg_ != null) | |
| { | |
| Debug.log("Warning: onActivate called twice without intervening onDeactivate"); | |
| commandHandlerReg_.removeHandler(); | |
| commandHandlerReg_ = null; | |
| } | |
| commandHandlerReg_ = commandBinder.bind(commands_, this); | |
| // show outline if not yet rendered (deferred so that widget itself can | |
| // be sized first) | |
| if (!docDisplay_.isRendered()) | |
| { | |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| view_.initWidgetSize(); | |
| } | |
| }); | |
| } | |
| Scheduler.get().scheduleFinally(new ScheduledCommand() | |
| { | |
| public void execute() | |
| { | |
| // This has to be executed in a scheduleFinally because | |
| // Source.manageCommands gets called after this.onActivate, | |
| // and if we're going from a non-editor (like data view) to | |
| // an editor, setEnabled(true) will be called on the command | |
| // in manageCommands. | |
| commands_.reopenSourceDocWithEncoding().setEnabled( | |
| docUpdateSentinel_.getPath() != null); | |
| } | |
| }); | |
| // notify notebook of activation if necessary | |
| if (notebook_ != null) | |
| notebook_.onActivate(); | |
| view_.onActivate(); | |
| } | |
| public void onDeactivate() | |
| { | |
| // IMPORTANT NOTE: most of this logic is duplicated in | |
| // CodeBrowserEditingTarget (no straightforward way to create a | |
| // re-usable implementation) so changes here need to be synced | |
| externalEditCheckInvalidation_.invalidate(); | |
| commandHandlerReg_.removeHandler(); | |
| commandHandlerReg_ = null; | |
| // switching tabs is a navigation action | |
| try | |
| { | |
| docDisplay_.recordCurrentNavigationPosition(); | |
| } | |
| catch(Exception e) | |
| { | |
| Debug.log("Exception recording nav position: " + e.toString()); | |
| } | |
| } | |
| @Override | |
| public void onInitiallyLoaded() | |
| { | |
| checkForExternalEdit(); | |
| } | |
| public boolean onBeforeDismiss() | |
| { | |
| final Command closeCommand = new Command() | |
| { | |
| public void execute() | |
| { | |
| CloseEvent.fire(TextEditingTarget.this, null); | |
| } | |
| }; | |
| final Command promptCommand = new Command() | |
| { | |
| public void execute() | |
| { | |
| if (dirtyState_.getValue()) | |
| saveWithPrompt(closeCommand, null); | |
| else | |
| closeCommand.execute(); | |
| } | |
| }; | |
| if (docDisplay_.hasFollowingCollabSession()) | |
| { | |
| globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_WARNING, | |
| getName().getValue() + " - Active Following Session", | |
| "You're actively following another user's cursor " + | |
| "in '" + getName().getValue() + "'.\n\n" + | |
| "If you close this file, you won't see their " + | |
| "cursor until they edit another file.", | |
| false, | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| promptCommand.execute(); | |
| } | |
| }, | |
| null, | |
| null, | |
| "Close Anyway", | |
| "Cancel", | |
| false); | |
| } | |
| else | |
| { | |
| promptCommand.execute(); | |
| } | |
| return false; | |
| } | |
| public void save() | |
| { | |
| save(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| }}); | |
| } | |
| public void save(Command onCompleted) | |
| { | |
| saveThenExecute(null, CommandUtil.join(postSaveCommand(), | |
| onCompleted)); | |
| } | |
| public void saveWithPrompt(final Command command, final Command onCancelled) | |
| { | |
| view_.ensureVisible(); | |
| globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_WARNING, | |
| getName().getValue() + " - Unsaved Changes", | |
| "The document '" + getName().getValue() + | |
| "' has unsaved changes.\n\n" + | |
| "Do you want to save these changes?", | |
| true, | |
| new Operation() { | |
| public void execute() { saveThenExecute(null, command); } | |
| }, | |
| new Operation() { | |
| public void execute() { command.execute(); } | |
| }, | |
| new Operation() { | |
| public void execute() { | |
| if (onCancelled != null) | |
| onCancelled.execute(); | |
| } | |
| }, | |
| "Save", | |
| "Don't Save", | |
| true); | |
| } | |
| public void revertChanges(Command onCompleted) | |
| { | |
| docUpdateSentinel_.revert(onCompleted, ignoreDeletes_); | |
| } | |
| public void saveThenExecute(String encodingOverride, final Command command) | |
| { | |
| checkCompilePdfDependencies(); | |
| final String path = docUpdateSentinel_.getPath(); | |
| if (path == null) | |
| { | |
| saveNewFile(null, encodingOverride, command); | |
| return; | |
| } | |
| withEncodingRequiredUnlessAscii( | |
| encodingOverride, | |
| new CommandWithArg<String>() | |
| { | |
| public void execute(String encoding) | |
| { | |
| fixupCodeBeforeSaving(); | |
| docUpdateSentinel_.save(path, | |
| null, | |
| encoding, | |
| new SaveProgressIndicator( | |
| FileSystemItem.createFile(path), | |
| null, | |
| command | |
| )); | |
| } | |
| }); | |
| } | |
| private void saveNewFile(final String suggestedPath, | |
| String encodingOverride, | |
| final Command executeOnSuccess) | |
| { | |
| withEncodingRequiredUnlessAscii( | |
| encodingOverride, | |
| new CommandWithArg<String>() | |
| { | |
| public void execute(String encoding) | |
| { | |
| saveNewFileWithEncoding(suggestedPath, | |
| encoding, | |
| executeOnSuccess); | |
| } | |
| }); | |
| } | |
| private void withEncodingRequiredUnlessAscii( | |
| final String encodingOverride, | |
| final CommandWithArg<String> command) | |
| { | |
| final String encoding = StringUtil.firstNotNullOrEmpty(new String[] { | |
| encodingOverride, | |
| docUpdateSentinel_.getEncoding(), | |
| prefs_.defaultEncoding().getValue() | |
| }); | |
| if (StringUtil.isNullOrEmpty(encoding)) | |
| { | |
| if (docUpdateSentinel_.isAscii()) | |
| { | |
| // Don't bother asking when it's just ASCII | |
| command.execute(null); | |
| } | |
| else | |
| { | |
| withChooseEncoding(session_.getSessionInfo().getSystemEncoding(), | |
| new CommandWithArg<String>() | |
| { | |
| public void execute(String newEncoding) | |
| { | |
| command.execute(newEncoding); | |
| } | |
| }); | |
| } | |
| } | |
| else | |
| { | |
| command.execute(encoding); | |
| } | |
| } | |
| private void withChooseEncoding(final String defaultEncoding, | |
| final CommandWithArg<String> command) | |
| { | |
| view_.ensureVisible();; | |
| server_.iconvlist(new SimpleRequestCallback<IconvListResult>() | |
| { | |
| @Override | |
| public void onResponseReceived(IconvListResult response) | |
| { | |
| // Stupid compiler. Use this Value shim to make the dialog available | |
| // in its own handler. | |
| final HasValue<ChooseEncodingDialog> d = new Value<ChooseEncodingDialog>(null); | |
| d.setValue(new ChooseEncodingDialog( | |
| response.getCommon(), | |
| response.getAll(), | |
| defaultEncoding, | |
| false, | |
| true, | |
| new OperationWithInput<String>() | |
| { | |
| public void execute(String newEncoding) | |
| { | |
| if (newEncoding == null) | |
| return; | |
| if (d.getValue().isSaveAsDefault()) | |
| { | |
| prefs_.defaultEncoding().setGlobalValue(newEncoding); | |
| prefs_.writeUIPrefs(); | |
| } | |
| command.execute(newEncoding); | |
| } | |
| })); | |
| d.getValue().showModal(); | |
| } | |
| }); | |
| } | |
| private void saveNewFileWithEncoding(String suggestedPath, | |
| final String encoding, | |
| final Command executeOnSuccess) | |
| { | |
| view_.ensureVisible(); | |
| FileSystemItem fsi; | |
| if (suggestedPath != null) | |
| fsi = FileSystemItem.createFile(suggestedPath); | |
| else | |
| fsi = getSaveFileDefaultDir(); | |
| fileDialogs_.saveFile( | |
| "Save File - " + getName().getValue(), | |
| fileContext_, | |
| fsi, | |
| fileType_.getDefaultExtension(), | |
| false, | |
| new ProgressOperationWithInput<FileSystemItem>() | |
| { | |
| public void execute(final FileSystemItem saveItem, | |
| ProgressIndicator indicator) | |
| { | |
| if (saveItem == null) | |
| return; | |
| try | |
| { | |
| workbenchContext_.setDefaultFileDialogDir( | |
| saveItem.getParentPath()); | |
| final TextFileType fileType = | |
| fileTypeRegistry_.getTextTypeForFile(saveItem); | |
| final Command saveCommand = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| if (getPath() != null && | |
| !getPath().equals(saveItem.getPath())) | |
| { | |
| // breakpoints are file-specific, so when saving | |
| // as a different file, clear the display of | |
| // breakpoints from the old file name | |
| docDisplay_.removeAllBreakpoints(); | |
| // update publish settings | |
| syncPublishPath(saveItem.getPath()); | |
| } | |
| fixupCodeBeforeSaving(); | |
| docUpdateSentinel_.save( | |
| saveItem.getPath(), | |
| fileType.getTypeId(), | |
| encoding, | |
| new SaveProgressIndicator(saveItem, | |
| fileType, | |
| executeOnSuccess)); | |
| events_.fireEvent( | |
| new SourceFileSavedEvent(getId(), | |
| saveItem.getPath())); | |
| } | |
| }; | |
| // if we are switching from an R file type | |
| // to a non-R file type then confirm | |
| if (fileType_.isR() && !fileType.isR()) | |
| { | |
| globalDisplay_.showYesNoMessage( | |
| MessageDialog.WARNING, | |
| "Confirm Change File Type", | |
| "This file was created as an R script however " + | |
| "the file extension you specified will change " + | |
| "it into another file type that will no longer " + | |
| "open as an R script.\n\n" + | |
| "Are you sure you want to change the type of " + | |
| "the file so that it is no longer an R script?", | |
| new Operation() { | |
| @Override | |
| public void execute() | |
| { | |
| saveCommand.execute(); | |
| } | |
| }, | |
| false); | |
| } | |
| else | |
| { | |
| saveCommand.execute(); | |
| } | |
| } | |
| catch (Exception e) | |
| { | |
| indicator.onError(e.toString()); | |
| return; | |
| } | |
| indicator.onCompleted(); | |
| } | |
| }); | |
| } | |
| private void fixupCodeBeforeSaving() | |
| { | |
| int lineCount = docDisplay_.getRowCount(); | |
| if (lineCount < 1) | |
| return; | |
| if (docDisplay_.hasActiveCollabSession()) | |
| { | |
| // mutating the code (especially as below where the entire document | |
| // contents are changed) during a save operation inside a collaborative | |
| // editing session would require some nuanced orchestration so for now | |
| // these preferences don't apply to shared editing sessions | |
| return; | |
| } | |
| boolean stripTrailingWhitespace = (projConfig_ == null) | |
| ? prefs_.stripTrailingWhitespace().getValue() | |
| : projConfig_.stripTrailingWhitespace(); | |
| if (stripTrailingWhitespace && | |
| !fileType_.isMarkdown() && | |
| !name_.getValue().equals("DESCRIPTION")) | |
| { | |
| String code = docDisplay_.getCode(); | |
| Pattern pattern = Pattern.create("[ \t]+$"); | |
| String strippedCode = pattern.replaceAll(code, ""); | |
| if (!strippedCode.equals(code)) | |
| { | |
| // Calling 'setCode' can remove folds in the document; cache the folds | |
| // and reapply them after document mutation. | |
| JsArray<AceFold> folds = docDisplay_.getFolds(); | |
| docDisplay_.setCode(strippedCode, true); | |
| for (AceFold fold : JsUtil.asIterable(folds)) | |
| docDisplay_.addFold(fold.getRange()); | |
| } | |
| } | |
| boolean autoAppendNewline = (projConfig_ == null) | |
| ? prefs_.autoAppendNewline().getValue() | |
| : projConfig_.ensureTrailingNewline(); | |
| // auto-append newlines for commonly-used R startup files | |
| String path = StringUtil.notNull(docUpdateSentinel_.getPath()); | |
| boolean isStartupFile = | |
| path.endsWith("/.Rprofile") || | |
| path.endsWith("/.Rprofile.site") || | |
| path.endsWith("/.Renviron") || | |
| path.endsWith("/.Renviron.site"); | |
| if (autoAppendNewline || isStartupFile || fileType_.isPython()) | |
| { | |
| String lastLine = docDisplay_.getLine(lineCount - 1); | |
| if (lastLine.length() != 0) | |
| docDisplay_.insertCode(docDisplay_.getEnd().getEnd(), "\n"); | |
| } | |
| } | |
| private FileSystemItem getSaveFileDefaultDir() | |
| { | |
| FileSystemItem fsi = null; | |
| SessionInfo si = session_.getSessionInfo(); | |
| if (si.getBuildToolsType() == SessionInfo.BUILD_TOOLS_PACKAGE) | |
| { | |
| FileSystemItem pkg = FileSystemItem.createDir(si.getBuildTargetDir()); | |
| if (fileType_.isR()) | |
| { | |
| fsi = FileSystemItem.createDir(pkg.completePath("R")); | |
| } | |
| else if (fileType_.isC() && si.getHasPackageSrcDir()) | |
| { | |
| fsi = FileSystemItem.createDir(pkg.completePath("src")); | |
| } | |
| else if (fileType_.isRd()) | |
| { | |
| fsi = FileSystemItem.createDir(pkg.completePath("man")); | |
| } | |
| else if ((fileType_.isRnw() || fileType_.isRmd()) && | |
| si.getHasPackageVignetteDir()) | |
| { | |
| fsi = FileSystemItem.createDir(pkg.completePath("vignettes")); | |
| } | |
| } | |
| if (fsi == null) | |
| fsi = workbenchContext_.getDefaultFileDialogDir(); | |
| return fsi; | |
| } | |
| public void onDismiss(int dismissType) | |
| { | |
| docUpdateSentinel_.stop(); | |
| if (spelling_ != null) | |
| spelling_.onDismiss(); | |
| while (releaseOnDismiss_.size() > 0) | |
| releaseOnDismiss_.remove(0).removeHandler(); | |
| docDisplay_.endCollabSession(); | |
| codeExecution_.detachLastExecuted(); | |
| if (notebook_ != null) | |
| notebook_.onDismiss(); | |
| if (inlinePreviewer_ != null) | |
| inlinePreviewer_.onDismiss(); | |
| } | |
| public ReadOnlyValue<Boolean> dirtyState() | |
| { | |
| return dirtyState_; | |
| } | |
| @Override | |
| public boolean isSaveCommandActive() | |
| { | |
| return | |
| // force active? | |
| forceSaveCommandActive_ || | |
| // standard check of dirty state | |
| (dirtyState().getValue() == true) || | |
| // empty untitled document (allow for immediate save) | |
| ((getPath() == null) && docDisplay_.getCode().isEmpty()) || | |
| // source on save is active | |
| (fileType_.canSourceOnSave() && docUpdateSentinel_.sourceOnSave()); | |
| } | |
| @Override | |
| public void forceSaveCommandActive() | |
| { | |
| forceSaveCommandActive_ = true; | |
| } | |
| public Widget asWidget() | |
| { | |
| return (Widget) view_; | |
| } | |
| public String getId() | |
| { | |
| return id_; | |
| } | |
| @Override | |
| public void adaptToExtendedFileType(String extendedType) | |
| { | |
| view_.adaptToExtendedFileType(extendedType); | |
| if (extendedType == SourceDocument.XT_RMARKDOWN) | |
| updateRmdFormatList(); | |
| extendedType_ = extendedType; | |
| // extended type can affect publish options | |
| syncPublishPath(docUpdateSentinel_.getPath()); | |
| } | |
| @Override | |
| public String getExtendedFileType() | |
| { | |
| return extendedType_; | |
| } | |
| public HasValue<String> getName() | |
| { | |
| return name_; | |
| } | |
| public String getTitle() | |
| { | |
| return getName().getValue(); | |
| } | |
| public String getPath() | |
| { | |
| if (docUpdateSentinel_ == null) | |
| return null; | |
| return docUpdateSentinel_.getPath(); | |
| } | |
| public String getContext() | |
| { | |
| return null; | |
| } | |
| public ImageResource getIcon() | |
| { | |
| return fileType_.getDefaultIcon(); | |
| } | |
| public String getTabTooltip() | |
| { | |
| return getPath(); | |
| } | |
| @Override | |
| public FileType getFileType() | |
| { | |
| return fileType_; | |
| } | |
| @Override | |
| public TextFileType getTextFileType() | |
| { | |
| return fileType_; | |
| } | |
| @Handler | |
| void onToggleDocumentOutline() | |
| { | |
| view_.toggleDocumentOutline(); | |
| } | |
| @Handler | |
| void onReformatCode() | |
| { | |
| // Only allow if entire selection in R mode for now | |
| if (!DocumentMode.isSelectionInRMode(docDisplay_)) | |
| { | |
| showRModeWarning("Reformat Code"); | |
| return; | |
| } | |
| reformatHelper_.insertPrettyNewlines(); | |
| } | |
| @Handler | |
| void onRenameInScope() | |
| { | |
| docDisplay_.focus(); | |
| // Save folds (we need to remove them temporarily for the rename helper) | |
| final JsArray<AceFold> folds = docDisplay_.getFolds(); | |
| docDisplay_.unfoldAll(); | |
| int matches = renameHelper_.renameInScope(); | |
| if (matches <= 0) | |
| { | |
| if (!docDisplay_.getSelectionValue().isEmpty()) | |
| { | |
| String message = "No matches for '" + docDisplay_.getSelectionValue() + "'"; | |
| view_.getStatusBar().showMessage(message, 1000); | |
| } | |
| for (AceFold fold : JsUtil.asIterable(folds)) | |
| docDisplay_.addFold(fold.getRange()); | |
| return; | |
| } | |
| String message = "Found " + matches; | |
| if (matches == 1) | |
| message += " match"; | |
| else | |
| message += " matches"; | |
| String selectedItem = docDisplay_.getSelectionValue(); | |
| message += " for " + selectedItem + "."; | |
| docDisplay_.disableSearchHighlight(); | |
| view_.getStatusBar().showMessage(message, new HideMessageHandler() | |
| { | |
| private boolean onRenameFinished(boolean value) | |
| { | |
| for (AceFold fold : JsUtil.asIterable(folds)) | |
| docDisplay_.addFold(fold.getRange()); | |
| return value; | |
| } | |
| @Override | |
| public boolean onNativePreviewEvent(NativePreviewEvent preview) | |
| { | |
| int type = preview.getTypeInt(); | |
| if (docDisplay_.isPopupVisible()) | |
| return false; | |
| // End if the user clicks somewhere | |
| if (type == Event.ONCLICK) | |
| { | |
| docDisplay_.exitMultiSelectMode(); | |
| docDisplay_.clearSelection(); | |
| docDisplay_.enableSearchHighlight(); | |
| return onRenameFinished(true); | |
| } | |
| // Otherwise, handle key events | |
| else if (type == Event.ONKEYDOWN) | |
| { | |
| switch (preview.getNativeEvent().getKeyCode()) | |
| { | |
| case KeyCodes.KEY_ENTER: | |
| preview.cancel(); | |
| case KeyCodes.KEY_UP: | |
| case KeyCodes.KEY_DOWN: | |
| case KeyCodes.KEY_ESCAPE: | |
| docDisplay_.exitMultiSelectMode(); | |
| docDisplay_.clearSelection(); | |
| docDisplay_.enableSearchHighlight(); | |
| return onRenameFinished(true); | |
| } | |
| } | |
| return false; | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onInsertRoxygenSkeleton() | |
| { | |
| roxygenHelper_.insertRoxygenSkeleton(); | |
| } | |
| @Handler | |
| void onExpandSelection() | |
| { | |
| docDisplay_.expandSelection(); | |
| } | |
| @Handler | |
| void onShrinkSelection() | |
| { | |
| docDisplay_.shrinkSelection(); | |
| } | |
| @Handler | |
| void onExpandRaggedSelection() | |
| { | |
| docDisplay_.expandRaggedSelection(); | |
| } | |
| @Handler | |
| void onShowDiagnosticsActiveDocument() | |
| { | |
| lintManager_.lint(true, true, false); | |
| } | |
| public void withSavedDoc(Command onsaved) | |
| { | |
| docUpdateSentinel_.withSavedDoc(onsaved); | |
| } | |
| @Handler | |
| void onCheckSpelling() | |
| { | |
| spelling_.checkSpelling(); | |
| } | |
| @Handler | |
| void onDebugDumpContents() | |
| { | |
| view_.debug_dumpContents(); | |
| } | |
| @Handler | |
| void onDebugImportDump() | |
| { | |
| view_.debug_importDump(); | |
| } | |
| @Handler | |
| void onReopenSourceDocWithEncoding() | |
| { | |
| withChooseEncoding( | |
| docUpdateSentinel_.getEncoding(), | |
| new CommandWithArg<String>() | |
| { | |
| public void execute(String encoding) | |
| { | |
| docUpdateSentinel_.reopenWithEncoding(encoding); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onSaveSourceDoc() | |
| { | |
| saveThenExecute(null, postSaveCommand()); | |
| } | |
| @Handler | |
| void onSaveSourceDocAs() | |
| { | |
| saveNewFile(docUpdateSentinel_.getPath(), | |
| null, | |
| postSaveCommand()); | |
| } | |
| @Handler | |
| void onRenameSourceDoc() | |
| { | |
| events_.fireEvent(new RenameSourceFileEvent(docUpdateSentinel_.getPath())); | |
| } | |
| @Handler | |
| void onSaveSourceDocWithEncoding() | |
| { | |
| withChooseEncoding( | |
| StringUtil.firstNotNullOrEmpty(new String[] { | |
| docUpdateSentinel_.getEncoding(), | |
| prefs_.defaultEncoding().getValue(), | |
| session_.getSessionInfo().getSystemEncoding() | |
| }), | |
| new CommandWithArg<String>() | |
| { | |
| public void execute(String encoding) | |
| { | |
| saveThenExecute(encoding, postSaveCommand()); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onPrintSourceDoc() | |
| { | |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() | |
| { | |
| public void execute() | |
| { | |
| docDisplay_.print(); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onVcsFileDiff() | |
| { | |
| Command showDiffCommand = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| events_.fireEvent(new ShowVcsDiffEvent( | |
| FileSystemItem.createFile(docUpdateSentinel_.getPath()))); | |
| } | |
| }; | |
| if (dirtyState_.getValue()) | |
| saveWithPrompt(showDiffCommand, null); | |
| else | |
| showDiffCommand.execute(); | |
| } | |
| @Handler | |
| void onVcsFileLog() | |
| { | |
| events_.fireEvent(new ShowVcsHistoryEvent( | |
| FileSystemItem.createFile(docUpdateSentinel_.getPath()))); | |
| } | |
| @Handler | |
| void onVcsFileRevert() | |
| { | |
| events_.fireEvent(new VcsRevertFileEvent( | |
| FileSystemItem.createFile(docUpdateSentinel_.getPath()))); | |
| } | |
| @Handler | |
| void onVcsViewOnGitHub() | |
| { | |
| fireVcsViewOnGithubEvent(GitHubViewRequest.VCS_VIEW); | |
| } | |
| @Handler | |
| void onVcsBlameOnGitHub() | |
| { | |
| fireVcsViewOnGithubEvent(GitHubViewRequest.VCS_BLAME); | |
| } | |
| private void fireVcsViewOnGithubEvent(int type) | |
| { | |
| FileSystemItem file = | |
| FileSystemItem.createFile(docUpdateSentinel_.getPath()); | |
| if (docDisplay_.getSelectionValue().length() > 0) | |
| { | |
| int start = docDisplay_.getSelectionStart().getRow() + 1; | |
| int end = docDisplay_.getSelectionEnd().getRow() + 1; | |
| events_.fireEvent(new VcsViewOnGitHubEvent( | |
| new GitHubViewRequest(file, type, start, end))); | |
| } | |
| else | |
| { | |
| events_.fireEvent(new VcsViewOnGitHubEvent( | |
| new GitHubViewRequest(file, type))); | |
| } | |
| } | |
| @Handler | |
| void onExtractLocalVariable() | |
| { | |
| if (!isCursorInRMode()) | |
| { | |
| showRModeWarning("Extract Variable"); | |
| return; | |
| } | |
| docDisplay_.focus(); | |
| String initialSelection = docDisplay_.getSelectionValue(); | |
| final String refactoringName = "Extract local variable"; | |
| final String pleaseSelectCodeMessage = "Please select the code to " + | |
| "extract into a variable."; | |
| if (checkSelectionAndAlert(refactoringName, | |
| pleaseSelectCodeMessage, | |
| initialSelection)) return; | |
| docDisplay_.fitSelectionToLines(false); | |
| final String code = docDisplay_.getSelectionValue(); | |
| if (checkSelectionAndAlert(refactoringName, | |
| pleaseSelectCodeMessage, | |
| code)) | |
| return; | |
| // get the first line of the selection and calculate it's indentation | |
| String firstLine = docDisplay_.getLine( | |
| docDisplay_.getSelectionStart().getRow()); | |
| final String indentation = extractIndentation(firstLine); | |
| // used to parse the code | |
| server_.detectFreeVars(code, | |
| new RefactorServerRequestCallback(refactoringName) | |
| { | |
| @Override | |
| void doExtract(JsArrayString response) | |
| { | |
| globalDisplay_.promptForText( | |
| refactoringName, | |
| "Variable Name", | |
| "", | |
| new OperationWithInput<String>() | |
| { | |
| public void execute(String input) | |
| { | |
| final String extractedCode = indentation | |
| + input.trim() | |
| + " <- " | |
| + code | |
| + "\n"; | |
| InputEditorPosition insertPosition = docDisplay_ | |
| .getSelection() | |
| .extendToLineStart() | |
| .getStart(); | |
| docDisplay_.replaceSelection( | |
| input.trim()); | |
| docDisplay_.insertCode( | |
| insertPosition, | |
| extractedCode); | |
| } | |
| } | |
| ); | |
| } | |
| } | |
| ); | |
| } | |
| private void showRModeWarning(String command) | |
| { | |
| globalDisplay_.showMessage(MessageDisplay.MSG_WARNING, | |
| "Command Not Available", | |
| "The "+ command + " command is " + | |
| "only valid for R code chunks."); | |
| return; | |
| } | |
| @Handler | |
| void onExtractFunction() | |
| { | |
| if (!isCursorInRMode()) | |
| { | |
| showRModeWarning("Extract Function"); | |
| return; | |
| } | |
| docDisplay_.focus(); | |
| String initialSelection = docDisplay_.getSelectionValue(); | |
| final String refactoringName = "Extract Function"; | |
| final String pleaseSelectCodeMessage = "Please select the code to " + | |
| "extract into a function."; | |
| if (checkSelectionAndAlert(refactoringName, | |
| pleaseSelectCodeMessage, | |
| initialSelection)) return; | |
| docDisplay_.fitSelectionToLines(false); | |
| final String code = docDisplay_.getSelectionValue(); | |
| if (checkSelectionAndAlert(refactoringName, | |
| pleaseSelectCodeMessage, | |
| code)) return; | |
| final String indentation = extractIndentation(code); | |
| server_.detectFreeVars(code, | |
| new RefactorServerRequestCallback(refactoringName) | |
| { | |
| @Override | |
| void doExtract(final JsArrayString response) | |
| { | |
| globalDisplay_.promptForText( | |
| refactoringName, | |
| "Function Name", | |
| "", | |
| new OperationWithInput<String>() | |
| { | |
| public void execute(String input) | |
| { | |
| String prefix; | |
| if (docDisplay_.getSelectionOffset(true) == 0) | |
| prefix = ""; | |
| else prefix = "\n"; | |
| String args = response != null ? response.join(", ") | |
| : ""; | |
| docDisplay_.replaceSelection( | |
| prefix | |
| + indentation | |
| + input.trim() | |
| + " <- " | |
| + "function(" + args + ") {\n" | |
| + StringUtil.indent(code, " ") | |
| + "\n" | |
| + indentation | |
| + "}"); | |
| } | |
| } | |
| ); | |
| } | |
| } | |
| ); | |
| } | |
| private boolean checkSelectionAndAlert(String refactoringName, | |
| String pleaseSelectCodeMessage, | |
| String selection) | |
| { | |
| if (isSelectionValueEmpty(selection)) | |
| { | |
| globalDisplay_.showErrorMessage(refactoringName, | |
| pleaseSelectCodeMessage); | |
| return true; | |
| } | |
| return false; | |
| } | |
| private String extractIndentation(String code) | |
| { | |
| Pattern leadingWhitespace = Pattern.create("^(\\s*)"); | |
| Match match = leadingWhitespace.match(code, 0); | |
| return match == null ? "" : match.getGroup(1); | |
| } | |
| private boolean isSelectionValueEmpty(String selection) | |
| { | |
| return selection == null || selection.trim().length() == 0; | |
| } | |
| @Handler | |
| void onCommentUncomment() | |
| { | |
| if (isCursorInTexMode()) | |
| doCommentUncomment("%", null); | |
| else if (isCursorInRMode() || isCursorInYamlMode()) | |
| doCommentUncomment("#", null); | |
| else if (fileType_.isCpp() || fileType_.isStan() || fileType_.isC()) | |
| doCommentUncomment("//", null); | |
| else if (fileType_.isPlainMarkdown()) | |
| doCommentUncomment("<!--", "-->"); | |
| else if (DocumentMode.isSelectionInMarkdownMode(docDisplay_)) | |
| doCommentUncomment("<!--", "-->"); | |
| else if (DocumentMode.isSelectionInPythonMode(docDisplay_)) | |
| doCommentUncomment("#", null); | |
| } | |
| /** | |
| * Push the current contents and state of the text editor into the local | |
| * copy of the source database | |
| */ | |
| public void syncLocalSourceDb() | |
| { | |
| SourceWindowManager manager = | |
| RStudioGinjector.INSTANCE.getSourceWindowManager(); | |
| JsArray<SourceDocument> docs = manager.getSourceDocs(); | |
| for (int i = 0; i < docs.length(); i++) | |
| { | |
| if (docs.get(i).getId() == getId()) | |
| { | |
| docs.get(i).getNotebookDoc().setChunkDefs( | |
| docDisplay_.getChunkDefs()); | |
| docs.get(i).setContents(docDisplay_.getCode()); | |
| docs.get(i).setDirty(dirtyState_.getValue()); | |
| break; | |
| } | |
| } | |
| } | |
| @Handler | |
| void onPopoutDoc() | |
| { | |
| if (docUpdateSentinel_ != null) | |
| { | |
| // ensure doc is synchronized with source database before popping it | |
| // out | |
| docUpdateSentinel_.withSavedDoc(new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| // push the new doc state into the local source database | |
| syncLocalSourceDb(); | |
| // fire popout event (this triggers a close in the current window | |
| // and the creation of a new window with the doc) | |
| events_.fireEvent(new PopoutDocEvent(getId(), | |
| currentPosition())); | |
| } | |
| }); | |
| } | |
| } | |
| @Handler | |
| void onReturnDocToMain() | |
| { | |
| // ensure doc is synchronized with source database before returning it | |
| if (!SourceWindowManager.isMainSourceWindow() && | |
| docUpdateSentinel_ != null) | |
| { | |
| docUpdateSentinel_.withSavedDoc(new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| events_.fireEventToMainWindow(new DocWindowChangedEvent( | |
| getId(), SourceWindowManager.getSourceWindowId(), "", | |
| DocTabDragParams.create(getId(), currentPosition()), | |
| docUpdateSentinel_.getDoc().getCollabParams(), 0)); | |
| } | |
| }); | |
| } | |
| } | |
| @Handler | |
| public void onNotebookCollapseAllOutput() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onNotebookCollapseAllOutput(); | |
| } | |
| @Handler | |
| public void onNotebookExpandAllOutput() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onNotebookExpandAllOutput(); | |
| } | |
| @Handler | |
| public void onNotebookClearOutput() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onNotebookClearOutput(); | |
| } | |
| @Handler | |
| public void onNotebookClearAllOutput() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onNotebookClearAllOutput(); | |
| } | |
| @Handler | |
| public void onNotebookToggleExpansion() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onNotebookToggleExpansion(); | |
| } | |
| @Handler | |
| public void onRestartRRunAllChunks() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onRestartRRunAllChunks(); | |
| } | |
| @Handler | |
| public void onRestartRClearOutput() | |
| { | |
| if (notebook_ != null) | |
| notebook_.onRestartRClearOutput(); | |
| } | |
| @SuppressWarnings("deprecation") // GWT emulation only provides isSpace | |
| private void doCommentUncomment(String commentStart, | |
| String commentEnd) | |
| { | |
| Range initialRange = docDisplay_.getSelectionRange(); | |
| int rowStart = initialRange.getStart().getRow(); | |
| int rowEnd = initialRange.getEnd().getRow(); | |
| boolean isSingleLineAction = rowStart == rowEnd; | |
| boolean commentWhitespace = commentEnd == null; | |
| // Also figure out if we're commenting an Roxygen block. | |
| boolean looksLikeRoxygen = false; | |
| // Skip commenting the last line if the selection is | |
| // multiline and ends on the first column of the end row. | |
| boolean dontCommentLastLine = false; | |
| if (rowStart != rowEnd && initialRange.getEnd().getColumn() == 0) | |
| dontCommentLastLine = true; | |
| Range expanded = Range.create( | |
| rowStart, | |
| 0, | |
| rowEnd, | |
| dontCommentLastLine ? 0 : docDisplay_.getLine(rowEnd).length()); | |
| docDisplay_.setSelectionRange(expanded); | |
| String[] lines = JsUtil.toStringArray( | |
| docDisplay_.getLines(rowStart, rowEnd - (dontCommentLastLine ? 1 : 0))); | |
| String commonPrefix = StringUtil.getCommonPrefix( | |
| lines, | |
| true, | |
| true); | |
| String commonIndent = StringUtil.getIndent(commonPrefix); | |
| // First, figure out whether we're commenting or uncommenting. | |
| // If we discover any line that doesn't start with the comment sequence, | |
| // then we'll comment the whole selection. | |
| boolean isCommentAction = false; | |
| for (String line : lines) | |
| { | |
| String trimmed = line.trim(); | |
| // Ignore lines that are just whitespace. | |
| if (!commentWhitespace && trimmed.isEmpty()) | |
| continue; | |
| if (!isCommentAction) | |
| { | |
| if (!trimmed.startsWith(commentStart)) | |
| isCommentAction = true; | |
| } | |
| if (docDisplay_.getFileType().isR()) | |
| { | |
| if (!looksLikeRoxygen) | |
| { | |
| if (trimmed.startsWith("@")) | |
| looksLikeRoxygen = true; | |
| else if (trimmed.startsWith("#'")) | |
| looksLikeRoxygen = true; | |
| } | |
| } | |
| } | |
| if (looksLikeRoxygen) | |
| commentStart += "'"; | |
| // Now, construct a new, commented selection to replace with. | |
| StringBuilder builder = new StringBuilder(); | |
| if (isCommentAction) | |
| { | |
| for (String line : lines) | |
| { | |
| String trimmed = line.trim(); | |
| if (!commentWhitespace && trimmed.isEmpty()) | |
| { | |
| builder.append("\n"); | |
| continue; | |
| } | |
| builder.append(commonIndent); | |
| builder.append(commentStart); | |
| builder.append(" "); | |
| builder.append(line.substring(commonIndent.length())); | |
| if (commentEnd != null) | |
| { | |
| builder.append(" "); | |
| builder.append(commentEnd); | |
| } | |
| builder.append("\n"); | |
| } | |
| } | |
| else | |
| { | |
| for (String line : lines) | |
| { | |
| String trimmed = line.trim(); | |
| if (trimmed.isEmpty()) | |
| { | |
| builder.append("\n"); | |
| continue; | |
| } | |
| boolean isCommentedLine = true; | |
| int commentStartIdx = line.indexOf(commentStart); | |
| if (commentStartIdx == -1) | |
| isCommentedLine = false; | |
| int commentEndIdx = line.length(); | |
| if (commentEnd != null) | |
| { | |
| commentEndIdx = line.lastIndexOf(commentEnd); | |
| if (commentEndIdx == -1) | |
| isCommentedLine = false; | |
| } | |
| if (!isCommentedLine) | |
| { | |
| builder.append(line); | |
| continue; | |
| } | |
| // We want to strip out the leading comment prefix, | |
| // but preserve the indent. | |
| int startIdx = commentStartIdx + commentStart.length(); | |
| if (Character.isSpace(line.charAt(startIdx))) | |
| startIdx++; | |
| int endIdx = commentEndIdx; | |
| String afterComment = line.substring(startIdx, endIdx); | |
| builder.append(StringUtil.trimRight(commonIndent + afterComment)); | |
| builder.append("\n"); | |
| } | |
| } | |
| String newSelection = dontCommentLastLine ? | |
| builder.toString() : | |
| builder.substring(0, builder.length() - 1); | |
| docDisplay_.replaceSelection(newSelection); | |
| // Nudge the selection to match the commented action. | |
| if (isSingleLineAction) | |
| { | |
| int diff = newSelection.length() - lines[0].length(); | |
| if (commentEnd != null) | |
| diff = diff < 0 ? | |
| diff + commentEnd.length() + 1 : | |
| diff - commentEnd.length() - 1; | |
| int colStart = initialRange.getStart().getColumn(); | |
| int colEnd = initialRange.getEnd().getColumn(); | |
| Range newRange = Range.create( | |
| rowStart, | |
| colStart + diff, | |
| rowStart, | |
| colEnd + diff); | |
| docDisplay_.setSelectionRange(newRange); | |
| } | |
| } | |
| @Handler | |
| void onReflowComment() | |
| { | |
| if (DocumentMode.isSelectionInRMode(docDisplay_) || | |
| DocumentMode.isSelectionInPythonMode(docDisplay_)) | |
| { | |
| doReflowComment("(#)"); | |
| } | |
| else if (DocumentMode.isSelectionInCppMode(docDisplay_)) | |
| { | |
| String currentLine = docDisplay_.getLine( | |
| docDisplay_.getCursorPosition().getRow()); | |
| if (currentLine.startsWith(" *")) | |
| doReflowComment("( \\*[^/])", false); | |
| else | |
| doReflowComment("(//)"); | |
| } | |
| else if (DocumentMode.isSelectionInTexMode(docDisplay_)) | |
| doReflowComment("(%)"); | |
| else if (DocumentMode.isSelectionInMarkdownMode(docDisplay_)) | |
| doReflowComment("()"); | |
| else if (docDisplay_.getFileType().isText()) | |
| doReflowComment("()"); | |
| } | |
| public void reflowText() | |
| { | |
| if (docDisplay_.getSelectionValue().isEmpty()) | |
| docDisplay_.setSelectionRange( | |
| Range.fromPoints( | |
| Position.create(docDisplay_.getCursorPosition().getRow(), 0), | |
| Position.create(docDisplay_.getCursorPosition().getRow(), | |
| docDisplay_.getCurrentLine().length()))); | |
| onReflowComment(); | |
| docDisplay_.setCursorPosition( | |
| Position.create( | |
| docDisplay_.getSelectionEnd().getRow(), | |
| 0)); | |
| } | |
| public void showHelpAtCursor() | |
| { | |
| docDisplay_.goToHelp(); | |
| } | |
| @Handler | |
| void onDebugBreakpoint() | |
| { | |
| docDisplay_.toggleBreakpointAtCursor(); | |
| } | |
| @Handler | |
| void onRsconnectDeploy() | |
| { | |
| view_.invokePublish(); | |
| } | |
| @Handler | |
| void onRsconnectConfigure() | |
| { | |
| events_.fireEvent(RSConnectActionEvent.ConfigureAppEvent( | |
| docUpdateSentinel_.getPath())); | |
| } | |
| @Handler | |
| void onEditRmdFormatOptions() | |
| { | |
| rmarkdownHelper_.withRMarkdownPackage( | |
| "Editing R Markdown options", | |
| false, | |
| new CommandWithArg<RMarkdownContext>() { | |
| @Override | |
| public void execute(RMarkdownContext arg) | |
| { | |
| showFrontMatterEditor(); | |
| } | |
| }); | |
| } | |
| private void showFrontMatterEditor() | |
| { | |
| final String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| { | |
| globalDisplay_.showErrorMessage("Edit Format Failed", | |
| "Can't find the YAML front matter for this document. Make " + | |
| "sure the front matter is enclosed by lines containing only " + | |
| "three dashes: ---."); | |
| return; | |
| } | |
| rmarkdownHelper_.convertFromYaml(yaml, new CommandWithArg<RmdYamlData>() | |
| { | |
| @Override | |
| public void execute(RmdYamlData arg) | |
| { | |
| String errCaption = "Edit Format Failed"; | |
| String errMsg = | |
| "The YAML front matter in this document could not be " + | |
| "successfully parsed. This parse error needs to be " + | |
| "resolved before format options can be edited."; | |
| if (arg == null) | |
| { | |
| globalDisplay_.showErrorMessage(errCaption, errMsg); | |
| } | |
| else if (!arg.parseSucceeded()) | |
| { | |
| // try to find where the YAML segment begins in the document | |
| // so we can show an adjusted line number for the error | |
| int numLines = docDisplay_.getRowCount(); | |
| int offsetLine = 0; | |
| String separator = RmdFrontMatter.FRONTMATTER_SEPARATOR.trim(); | |
| for (int i = 0; i < numLines; i++) | |
| { | |
| if (docDisplay_.getLine(i).equals(separator)) | |
| { | |
| offsetLine = i + 1; | |
| break; | |
| } | |
| } | |
| globalDisplay_.showErrorMessage(errCaption, | |
| errMsg + "\n\n" + arg.getOffsetParseError(offsetLine)); | |
| } | |
| else | |
| { | |
| showFrontMatterEditorDialog(yaml, arg); | |
| } | |
| } | |
| }); | |
| } | |
| private void showFrontMatterEditorDialog(String yaml, RmdYamlData data) | |
| { | |
| RmdSelectedTemplate selTemplate = | |
| rmarkdownHelper_.getTemplateFormat(yaml); | |
| if (selTemplate == null) | |
| { | |
| // we don't expect this to happen since we disable the dialog | |
| // entry point when we can't find an associated template | |
| globalDisplay_.showErrorMessage("Edit Format Failed", | |
| "Couldn't determine the format options from the YAML front " + | |
| "matter. Make sure the YAML defines a supported output " + | |
| "format in its 'output' field."); | |
| return; | |
| } | |
| RmdTemplateOptionsDialog dialog = | |
| new RmdTemplateOptionsDialog(selTemplate.template, | |
| selTemplate.format, | |
| data.getFrontMatter(), | |
| getPath() == null ? null : FileSystemItem.createFile(getPath()), | |
| selTemplate.isShiny, | |
| new OperationWithInput<RmdTemplateOptionsDialog.Result>() | |
| { | |
| @Override | |
| public void execute(RmdTemplateOptionsDialog.Result in) | |
| { | |
| // when the dialog is completed successfully, apply the new | |
| // front matter | |
| applyRmdFormatOptions(in.format, in.outputOptions); | |
| } | |
| }, | |
| new Operation() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| // when the dialog is cancelled, update the view's format list | |
| // (to cancel in-place changes) | |
| updateRmdFormatList(); | |
| } | |
| }); | |
| dialog.showModal(); | |
| } | |
| private void applyRmdFormatOptions(String format, | |
| RmdFrontMatterOutputOptions options) | |
| { | |
| rmarkdownHelper_.replaceOutputFormatOptions( | |
| getRmdFrontMatter(), format, options, | |
| new OperationWithInput<String>() | |
| { | |
| @Override | |
| public void execute(String input) | |
| { | |
| applyRmdFrontMatter(input); | |
| } | |
| }); | |
| } | |
| private String getRmdFrontMatter() | |
| { | |
| return YamlFrontMatter.getFrontMatter(docDisplay_); | |
| } | |
| private void applyRmdFrontMatter(String yaml) | |
| { | |
| if (YamlFrontMatter.applyFrontMatter(docDisplay_, yaml)) | |
| { | |
| updateRmdFormatList(); | |
| } | |
| } | |
| private RmdSelectedTemplate getSelectedTemplate() | |
| { | |
| // try to extract the front matter and ascertain the template to which | |
| // it refers | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return null; | |
| return rmarkdownHelper_.getTemplateFormat(yaml); | |
| } | |
| private List<String> getOutputFormats() | |
| { | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return new ArrayList<String>(); | |
| List<String> formats = rmarkdownHelper_.getOutputFormats(yaml); | |
| if (formats == null) | |
| formats = new ArrayList<String>(); | |
| return formats; | |
| } | |
| private void updateRmdFormatList() | |
| { | |
| String formatUiName = ""; | |
| List<String> formatList = new ArrayList<String>(); | |
| List<String> valueList = new ArrayList<String>(); | |
| List<String> extensionList = new ArrayList<String>(); | |
| RmdSelectedTemplate selTemplate = getSelectedTemplate(); | |
| if (selTemplate != null && selTemplate.isShiny) | |
| { | |
| view_.setIsShinyFormat(selTemplate.format != null, | |
| selTemplate.format != null && | |
| selTemplate.format.endsWith( | |
| RmdOutputFormat.OUTPUT_PRESENTATION_SUFFIX), | |
| isShinyPrerenderedDoc()); | |
| } | |
| // could be runtime: shiny with a custom format | |
| else if (isShinyDoc()) | |
| { | |
| view_.setIsShinyFormat(false, // no output options b/c no template | |
| false, // not a presentation (unknown format) | |
| isShinyPrerenderedDoc()); | |
| } | |
| else | |
| { | |
| view_.setIsNotShinyFormat(); | |
| if (selTemplate != null) | |
| { | |
| JsArray<RmdTemplateFormat> formats = selTemplate.template.getFormats(); | |
| for (int i = 0; i < formats.length(); i++) | |
| { | |
| // skip notebook format (will enable it later if discovered) | |
| if (formats.get(i).getName() == | |
| RmdOutputFormat.OUTPUT_HTML_NOTEBOOK) | |
| { | |
| continue; | |
| } | |
| // hide powerpoint if not available | |
| if (!session_.getSessionInfo().getPptAvailable() && | |
| StringUtil.equals(formats.get(i).getName(), | |
| RmdOutputFormat.OUTPUT_PPT_PRESENTATION)) | |
| { | |
| continue; | |
| } | |
| String uiName = formats.get(i).getUiName(); | |
| formatList.add(uiName); | |
| valueList.add(formats.get(i).getName()); | |
| extensionList.add(formats.get(i).getExtension()); | |
| if (formats.get(i).getName() == selTemplate.format) | |
| { | |
| formatUiName = uiName; | |
| } | |
| } | |
| } | |
| // add formats not in the selected template | |
| boolean isNotebook = false; | |
| List<String> outputFormats = getOutputFormats(); | |
| for (int i = 0; i < outputFormats.size(); i++) | |
| { | |
| String format = outputFormats.get(i); | |
| if (format == RmdOutputFormat.OUTPUT_HTML_NOTEBOOK) | |
| { | |
| if (i == 0) | |
| isNotebook = true; | |
| formatList.add(0, "Notebook"); | |
| valueList.add(0, format); | |
| extensionList.add(0, ".nb.html"); | |
| continue; | |
| } | |
| if (!valueList.contains(format)) | |
| { | |
| String uiName = format; | |
| int nsLoc = uiName.indexOf("::"); | |
| if (nsLoc != -1) | |
| uiName = uiName.substring(nsLoc + 2); | |
| formatList.add(uiName); | |
| valueList.add(format); | |
| extensionList.add(null); | |
| } | |
| } | |
| view_.setFormatOptions(fileType_, | |
| // can choose output formats | |
| getCustomKnit().length() == 0, | |
| // can edit format options | |
| selTemplate != null, | |
| formatList, | |
| valueList, | |
| extensionList, | |
| formatUiName); | |
| // update notebook-specific options | |
| if (isNotebook) | |
| { | |
| // if the user manually set the output to console in a notebook, | |
| // respect that (even though it's weird) | |
| String outputType = RmdEditorOptions.getString( | |
| YamlFrontMatter.getFrontMatter(docDisplay_), | |
| TextEditingTargetNotebook.CHUNK_OUTPUT_TYPE, null); | |
| if (outputType != TextEditingTargetNotebook.CHUNK_OUTPUT_CONSOLE) | |
| { | |
| // chunk output should always be inline in notebooks | |
| outputType = docUpdateSentinel_.getProperty( | |
| TextEditingTargetNotebook.CHUNK_OUTPUT_TYPE); | |
| if (outputType != TextEditingTargetNotebook.CHUNK_OUTPUT_INLINE) | |
| { | |
| docUpdateSentinel_.setProperty( | |
| TextEditingTargetNotebook.CHUNK_OUTPUT_TYPE, | |
| TextEditingTargetNotebook.CHUNK_OUTPUT_INLINE); | |
| } | |
| } | |
| view_.setIsNotebookFormat(); | |
| } | |
| } | |
| if (isShinyDoc()) | |
| { | |
| // turn off inline output in Shiny documents (if it's not already) | |
| if (docDisplay_.showChunkOutputInline()) | |
| docDisplay_.setShowChunkOutputInline(false); | |
| } | |
| } | |
| private void setRmdFormat(String formatName) | |
| { | |
| // If the target format name already matches the first format then just | |
| // render and return | |
| List<String> outputFormats = getOutputFormats(); | |
| if (outputFormats.size() > 0 && outputFormats.get(0).equals(formatName)) | |
| { | |
| renderRmd(); | |
| return; | |
| } | |
| rmarkdownHelper_.setOutputFormat(getRmdFrontMatter(), formatName, | |
| new CommandWithArg<String>() | |
| { | |
| @Override | |
| public void execute(String yaml) | |
| { | |
| if (yaml != null) | |
| applyRmdFrontMatter(yaml); | |
| // re-knit the document | |
| renderRmd(); | |
| } | |
| }); | |
| } | |
| void doReflowComment(String commentPrefix) | |
| { | |
| doReflowComment(commentPrefix, true); | |
| } | |
| void doReflowComment(String commentPrefix, boolean multiParagraphIndent) | |
| { | |
| docDisplay_.focus(); | |
| InputEditorSelection originalSelection = docDisplay_.getSelection(); | |
| InputEditorSelection selection = originalSelection; | |
| if (selection.isEmpty()) | |
| { | |
| selection = selection.growToIncludeLines("^\\s*" + commentPrefix + ".*$"); | |
| } | |
| else | |
| { | |
| selection = selection.shrinkToNonEmptyLines(); | |
| selection = selection.extendToLineStart(); | |
| selection = selection.extendToLineEnd(); | |
| } | |
| if (selection.isEmpty()) | |
| return; | |
| reflowComments(commentPrefix, | |
| multiParagraphIndent, | |
| selection, | |
| originalSelection.isEmpty() ? | |
| originalSelection.getStart() : | |
| null); | |
| } | |
| private Position selectionToPosition(InputEditorPosition pos) | |
| { | |
| return docDisplay_.selectionToPosition(pos); | |
| } | |
| private void reflowComments(String commentPrefix, | |
| final boolean multiParagraphIndent, | |
| InputEditorSelection selection, | |
| final InputEditorPosition cursorPos) | |
| { | |
| String code = docDisplay_.getCode(selection); | |
| String[] lines = code.split("\n"); | |
| String prefix = StringUtil.getCommonPrefix(lines, true, false); | |
| Pattern pattern = Pattern.create("^\\s*" + commentPrefix + "+('?)\\s*"); | |
| Match match = pattern.match(prefix, 0); | |
| // Selection includes non-comments? Abort. | |
| if (match == null) | |
| return; | |
| prefix = match.getValue(); | |
| final boolean roxygen = match.hasGroup(1); | |
| int cursorRowIndex = 0; | |
| int cursorColIndex = 0; | |
| if (cursorPos != null) | |
| { | |
| cursorRowIndex = selectionToPosition(cursorPos).getRow() - | |
| selectionToPosition(selection.getStart()).getRow(); | |
| cursorColIndex = | |
| Math.max(0, cursorPos.getPosition() - prefix.length()); | |
| } | |
| final WordWrapCursorTracker wwct = new WordWrapCursorTracker( | |
| cursorRowIndex, cursorColIndex); | |
| int maxLineLength = | |
| prefs_.printMarginColumn().getValue() - prefix.length(); | |
| WordWrap wordWrap = new WordWrap(maxLineLength, false) | |
| { | |
| @Override | |
| protected boolean forceWrapBefore(String line) | |
| { | |
| String trimmed = line.trim(); | |
| if (roxygen && trimmed.startsWith("@") && !trimmed.startsWith("@@")) | |
| { | |
| // Roxygen tags always need to be at the start of a line. If | |
| // there is content immediately following the roxygen tag, then | |
| // content should be wrapped until the next roxygen tag is | |
| // encountered. | |
| indent_ = ""; | |
| if (TAG_WITH_CONTENTS.match(line, 0) != null) | |
| { | |
| indentRestOfLines_ = true; | |
| } | |
| return true; | |
| } | |
| // empty line disables indentation | |
| else if (!multiParagraphIndent && (line.trim().length() == 0)) | |
| { | |
| indent_ = ""; | |
| indentRestOfLines_ = false; | |
| } | |
| return super.forceWrapBefore(line); | |
| } | |
| @Override | |
| protected void onChunkWritten(String chunk, | |
| int insertionRow, | |
| int insertionCol, | |
| int indexInOriginalString) | |
| { | |
| if (indentRestOfLines_) | |
| { | |
| indentRestOfLines_ = false; | |
| indent_ = " "; // TODO: Use real indent from settings | |
| } | |
| wwct.onChunkWritten(chunk, insertionRow, insertionCol, | |
| indexInOriginalString); | |
| } | |
| private boolean indentRestOfLines_ = false; | |
| private Pattern TAG_WITH_CONTENTS = Pattern.create("@\\w+\\s+[^\\s]"); | |
| }; | |
| for (String line : lines) | |
| { | |
| String content = line.substring(Math.min(line.length(), | |
| prefix.length())); | |
| if (content.matches("^\\s*\\@examples\\b.*$")) | |
| wordWrap.setWrappingEnabled(false); | |
| else if (content.trim().startsWith("@")) | |
| wordWrap.setWrappingEnabled(true); | |
| wwct.onBeginInputRow(); | |
| wordWrap.appendLine(content); | |
| } | |
| String wrappedString = wordWrap.getOutput(); | |
| StringBuilder finalOutput = new StringBuilder(); | |
| for (String line : StringUtil.getLineIterator(wrappedString)) | |
| finalOutput.append(prefix).append(line).append("\n"); | |
| // Remove final \n | |
| if (finalOutput.length() > 0) | |
| finalOutput.deleteCharAt(finalOutput.length()-1); | |
| String reflowed = finalOutput.toString(); | |
| // Remove trailing whitespace that might have leaked in earlier | |
| reflowed = reflowed.replaceAll("\\s+\\n", "\n"); | |
| docDisplay_.setSelection(selection); | |
| if (!reflowed.equals(code)) | |
| { | |
| docDisplay_.replaceSelection(reflowed); | |
| } | |
| if (cursorPos != null) | |
| { | |
| if (wwct.getResult() != null) | |
| { | |
| int row = wwct.getResult().getY(); | |
| int col = wwct.getResult().getX(); | |
| row += selectionToPosition(selection.getStart()).getRow(); | |
| col += prefix.length(); | |
| Position pos = Position.create(row, col); | |
| docDisplay_.setSelection(docDisplay_.createSelection(pos, pos)); | |
| } | |
| else | |
| { | |
| docDisplay_.collapseSelection(false); | |
| } | |
| } | |
| } | |
| @Handler | |
| void onExecuteCodeWithoutFocus() | |
| { | |
| codeExecution_.executeSelection(false); | |
| } | |
| @Handler | |
| void onProfileCodeWithoutFocus() | |
| { | |
| dependencyManager_.withProfvis("The profiler", new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| codeExecution_.executeSelection(false, false, "profvis::profvis", true); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onExecuteCodeWithoutMovingCursor() | |
| { | |
| if (docDisplay_.isFocused()) | |
| codeExecution_.executeSelection(true, false); | |
| else if (view_.isAttached()) | |
| view_.findSelectAll(); | |
| } | |
| @Handler | |
| void onExecuteCode() | |
| { | |
| if (fileType_.isScript()) | |
| codeExecution_.sendSelectionToTerminal(true); | |
| else | |
| codeExecution_.executeSelection(true); | |
| } | |
| @Handler | |
| void onExecuteCurrentLine() | |
| { | |
| codeExecution_.executeBehavior(UIPrefsAccessor.EXECUTE_LINE); | |
| } | |
| @Handler | |
| void onExecuteCurrentStatement() | |
| { | |
| codeExecution_.executeBehavior(UIPrefsAccessor.EXECUTE_STATEMENT); | |
| } | |
| @Handler | |
| void onExecuteCurrentParagraph() | |
| { | |
| codeExecution_.executeBehavior(UIPrefsAccessor.EXECUTE_PARAGRAPH); | |
| } | |
| @Handler | |
| void onSendToTerminal() | |
| { | |
| codeExecution_.sendSelectionToTerminal(false); | |
| } | |
| @Override | |
| public String extractCode(DocDisplay docDisplay, Range range) | |
| { | |
| Scope sweaveChunk = null; | |
| if (fileType_.canExecuteChunks()) | |
| sweaveChunk = scopeHelper_.getCurrentSweaveChunk(range.getStart()); | |
| String code = sweaveChunk != null | |
| ? scopeHelper_.getSweaveChunkText(sweaveChunk, range) | |
| : docDisplay_.getCode(range.getStart(), range.getEnd()); | |
| return code; | |
| } | |
| @Handler | |
| void onExecuteAllCode() | |
| { | |
| boolean executeChunks = fileType_.canCompilePDF() || | |
| fileType_.canKnitToHTML() || | |
| fileType_.isRpres(); | |
| if (executeChunks) | |
| { | |
| executeChunks(Position.create( | |
| docDisplay_.getDocumentEnd().getRow() + 1, | |
| 0), | |
| TextEditingTargetScopeHelper.PREVIOUS_CHUNKS); | |
| } | |
| else | |
| { | |
| sourceActiveDocument(true); | |
| } | |
| } | |
| @Handler | |
| void onExecuteToCurrentLine() | |
| { | |
| docDisplay_.focus(); | |
| int row = docDisplay_.getSelectionEnd().getRow(); | |
| int col = docDisplay_.getLength(row); | |
| codeExecution_.executeRange(Range.fromPoints(Position.create(0, 0), | |
| Position.create(row, col))); | |
| } | |
| @Handler | |
| void onExecuteFromCurrentLine() | |
| { | |
| docDisplay_.focus(); | |
| int startRow = docDisplay_.getSelectionStart().getRow(); | |
| int startColumn = 0; | |
| Position start = Position.create(startRow, startColumn); | |
| codeExecution_.executeRange(Range.fromPoints(start, endPosition())); | |
| } | |
| @Handler | |
| void onExecuteCurrentFunction() | |
| { | |
| docDisplay_.focus(); | |
| // HACK: This is just to force the entire function tree to be built. | |
| // It's the easiest way to make sure getCurrentScope() returns | |
| // a Scope with an end. | |
| docDisplay_.getScopeTree(); | |
| Scope currentFunction = docDisplay_.getCurrentFunction(false); | |
| // Check if we're at the top level (i.e. not in a function), or in | |
| // an unclosed function | |
| if (currentFunction == null || currentFunction.getEnd() == null) | |
| return; | |
| Position start = currentFunction.getPreamble(); | |
| Position end = currentFunction.getEnd(); | |
| codeExecution_.executeRange(Range.fromPoints(start, end)); | |
| } | |
| @Handler | |
| void onExecuteCurrentSection() | |
| { | |
| docDisplay_.focus(); | |
| // Determine the current section. | |
| docDisplay_.getScopeTree(); | |
| Scope currentSection = docDisplay_.getCurrentSection(); | |
| if (currentSection == null) | |
| return; | |
| // Determine the start and end of the section | |
| Position start = currentSection.getBodyStart(); | |
| if (start == null) | |
| start = Position.create(0, 0); | |
| Position end = currentSection.getEnd(); | |
| if (end == null) | |
| end = endPosition(); | |
| codeExecution_.executeRange(Range.fromPoints(start, end)); | |
| } | |
| private Position endPosition() | |
| { | |
| int endRow = Math.max(0, docDisplay_.getRowCount() - 1); | |
| int endColumn = docDisplay_.getLength(endRow); | |
| return Position.create(endRow, endColumn); | |
| } | |
| // splits a chunk into two chunks | |
| // chunk 1: first chunk line to linePos (not including linePos) | |
| // chunk 2: linePos line to end | |
| private void splitChunk(Scope chunk, int linePos) | |
| { | |
| Position chunkStart = chunk.getBodyStart(); | |
| Position chunkEnd = chunk.getEnd(); | |
| if (chunkEnd == null) | |
| chunkEnd = docDisplay_.getDocumentEnd(); | |
| Position preamble = chunk.getPreamble(); | |
| String preambleLine = docDisplay_.getLine(preamble.getRow()) + "\n"; | |
| // if the cursor line is in the preamble of the chunk | |
| // reset it to the body of the chunk to get the same semantics | |
| // (empty chunk followed by the entire existing chunk) | |
| if (linePos < chunkStart.getRow()) | |
| linePos = chunkStart.getRow(); | |
| // get chunk contents from chunk start up to the specified line | |
| Range firstChunkRange = Range.create(chunkStart.getRow(), chunkStart.getColumn(), linePos, 0); | |
| String firstChunkContents = docDisplay_.getTextForRange(firstChunkRange); | |
| firstChunkContents = firstChunkContents.trim(); | |
| // add preamble line and ending line for new first chunk | |
| firstChunkContents = preambleLine + firstChunkContents; | |
| if (!firstChunkContents.endsWith("\n")) | |
| firstChunkContents += "\n"; | |
| firstChunkContents += getChunkEnd() + "\n\n"; | |
| // get second chunk contents from what's left | |
| Range secondChunkRange = Range.create(linePos, 0, chunkEnd.getRow(), chunkEnd.getColumn()); | |
| String secondChunkContents = docDisplay_.getTextForRange(secondChunkRange); | |
| secondChunkContents = secondChunkContents.trim(); | |
| // add the preamble line of the original chunk to the second chunk (so we have the correct language) | |
| secondChunkContents = preambleLine + secondChunkContents; | |
| // modify contents of original chunk with second chunk contents | |
| Range chunkRange = Range.create(chunkStart.getRow() - 1, chunkStart.getColumn(), chunkEnd.getRow(), chunkEnd.getColumn()); | |
| docDisplay_.replaceRange(chunkRange, secondChunkContents); | |
| // insert new chunk with first chunk contents | |
| docDisplay_.setCursorPosition(Position.create(chunkStart.getRow() - 1, chunkStart.getColumn())); | |
| docDisplay_.insertCode(firstChunkContents, false); | |
| } | |
| // splits a chunk into three chunks | |
| // chunk 1: first chunk line to start pos | |
| // chunk 2: start pos to end pos | |
| // chunk 3: end pos to end of chunk | |
| private void splitChunk(Scope chunk, Position startPos, Position endPos) | |
| { | |
| Position chunkStart = chunk.getBodyStart(); | |
| Position chunkEnd = chunk.getEnd(); | |
| if (chunkEnd == null) | |
| chunkEnd = docDisplay_.getDocumentEnd(); | |
| Position preamble = chunk.getPreamble(); | |
| String preambleLine = docDisplay_.getLine(preamble.getRow()) + "\n"; | |
| // if the selected position is only within the preamble, do nothing as it makes no sense to do any splitting | |
| if (startPos.getRow() == preamble.getRow() && endPos.getRow() == startPos.getRow()) | |
| return; | |
| // if the selected position starts within the preamble, reset the start position to not include the preamble | |
| // as it does not make any sense to do any splitting within the preamble | |
| if (startPos.getRow() == preamble.getRow()) | |
| { | |
| startPos.setRow(preamble.getRow() + 1); | |
| startPos.setColumn(0); | |
| } | |
| // if the selected position ends within the footer (```), reset the end position to not include it | |
| if (endPos.getRow() == chunkEnd.getRow() && endPos.getColumn() > 0) | |
| { | |
| endPos.setColumn(0); | |
| } | |
| // get chunk contents from chunk start up to the specified start pos | |
| Range firstChunkRange = Range.create(chunkStart.getRow(), chunkStart.getColumn(), startPos.getRow(), startPos.getColumn()); | |
| String firstChunkContents = docDisplay_.getTextForRange(firstChunkRange); | |
| firstChunkContents = firstChunkContents.trim(); | |
| // add preamble line and ending line for new first chunk | |
| firstChunkContents = preambleLine + firstChunkContents; | |
| if (!firstChunkContents.endsWith("\n")) | |
| firstChunkContents += "\n"; | |
| firstChunkContents += getChunkEnd() + "\n\n"; | |
| // get middle chunk contents from selected positions | |
| Range middleChunkRange = Range.create(startPos.getRow(), startPos.getColumn(), endPos.getRow(), endPos.getColumn()); | |
| String middleChunkContents = docDisplay_.getTextForRange(middleChunkRange); | |
| middleChunkContents = middleChunkContents.trim(); | |
| // // add preamble line and ending line for middle chunk | |
| middleChunkContents = preambleLine + middleChunkContents; | |
| if (!middleChunkContents.endsWith("\n")) | |
| middleChunkContents += "\n"; | |
| middleChunkContents += getChunkEnd() + "\n\n"; | |
| // get final chunk contents from ending selection position to ending chunk position | |
| Range finalChunkRange = Range.create(endPos.getRow(), endPos.getColumn(), chunkEnd.getRow(), chunkEnd.getColumn()); | |
| String finalChunkContents = docDisplay_.getTextForRange(finalChunkRange); | |
| finalChunkContents = finalChunkContents.trim(); | |
| // add preamble to final chunk | |
| finalChunkContents = preambleLine + finalChunkContents; | |
| // modify contents of original chunk with final chunk contents | |
| Range chunkRange = Range.create(chunkStart.getRow() - 1, chunkStart.getColumn(), chunkEnd.getRow(), chunkEnd.getColumn()); | |
| docDisplay_.replaceRange(chunkRange, finalChunkContents); | |
| // insert first and middle chunk contents as new chunks | |
| docDisplay_.setCursorPosition(Position.create(chunkStart.getRow() - 1, chunkStart.getColumn())); | |
| docDisplay_.insertCode(firstChunkContents, false); | |
| Position middleChunkPos = docDisplay_.getCursorPosition(); | |
| docDisplay_.insertCode(middleChunkContents, false); | |
| // reset cursor position to middle chunk (selected text) | |
| docDisplay_.setCursorPosition(middleChunkPos); | |
| } | |
| private void onInsertChunk(String chunkPlaceholder, int rowOffset, int colOffset) | |
| { | |
| String sel = ""; | |
| Range selRange = null; | |
| // if currently in a chunk | |
| // with no selection, split this chunk into two chunks at the current line | |
| // with selection, split this chunk into three chunks (prior to selection, selected, post selection) | |
| Scope currentChunk = docDisplay_.getCurrentChunk(); | |
| if (currentChunk != null) | |
| { | |
| // record current selection before manipulating text | |
| sel = docDisplay_.getSelectionValue(); | |
| selRange = docDisplay_.getSelectionRange(); | |
| if (selRange.isEmpty()) | |
| { | |
| splitChunk(currentChunk, docDisplay_.getCursorPosition().getRow()); | |
| return; | |
| } | |
| else | |
| { | |
| splitChunk(currentChunk, selRange.getStart() ,selRange.getEnd()); | |
| return; | |
| } | |
| } | |
| Position pos = moveCursorToNextInsertLocation(); | |
| InsertChunkInfo insertChunkInfo = docDisplay_.getInsertChunkInfo(); | |
| if (insertChunkInfo != null) | |
| { | |
| // inject the chunk skeleton | |
| docDisplay_.insertCode(chunkPlaceholder, false); | |
| // if we had text selected, inject it into the chunk | |
| if (!StringUtil.isNullOrEmpty(sel)) | |
| { | |
| Position contentPosition = insertChunkInfo.getContentPosition(); | |
| Position docContentPos = Position.create( | |
| pos.getRow() + contentPosition.getRow(), | |
| contentPosition.getColumn()); | |
| Position endPos = Position.create(docContentPos.getRow(), | |
| docContentPos.getColumn()); | |
| // move over newline if selected | |
| if (sel.endsWith("\n")) | |
| endPos.setRow(endPos.getRow() + 1); | |
| docDisplay_.replaceRange( | |
| Range.fromPoints(docContentPos, endPos), sel); | |
| docDisplay_.replaceRange(selRange, ""); | |
| } | |
| Position cursorPosition = insertChunkInfo.getCursorPosition(); | |
| docDisplay_.setCursorPosition(Position.create( | |
| pos.getRow() + cursorPosition.getRow() + rowOffset, | |
| colOffset)); | |
| docDisplay_.focus(); | |
| } | |
| else | |
| { | |
| assert false : "Mode did not have insertChunkInfo available"; | |
| } | |
| } | |
| String getChunkEnd() | |
| { | |
| InsertChunkInfo info = docDisplay_.getInsertChunkInfo(); | |
| if (info == null) | |
| return "```"; // default to Rmd | |
| // chunks are delimited by 2 new lines | |
| // if not, we will fallback on an empty chunk end just for safety | |
| String[] chunkParts = info.getValue().split("\n\n"); | |
| if (chunkParts.length == 2) | |
| return chunkParts[1]; | |
| else | |
| return ""; | |
| } | |
| @Handler | |
| void onInsertChunk() | |
| { | |
| InsertChunkInfo info = docDisplay_.getInsertChunkInfo(); | |
| if (info == null) | |
| return; | |
| onInsertChunk(info.getValue(), 1, 0); | |
| } | |
| @Handler | |
| void onInsertChunkR() | |
| { | |
| onInsertChunk("```{r}\n\n```\n", 1, 0); | |
| } | |
| @Handler | |
| void onInsertChunkBash() | |
| { | |
| onInsertChunk("```{bash}\n\n```\n", 1, 0); | |
| } | |
| @Handler | |
| void onInsertChunkPython() | |
| { | |
| onInsertChunk("```{python}\n\n```\n", 1, 0); | |
| } | |
| @Handler | |
| void onInsertChunkRCPP() | |
| { | |
| onInsertChunk("```{Rcpp}\n\n```\n", 1, 0); | |
| } | |
| @Handler | |
| void onInsertChunkStan() | |
| { | |
| onInsertChunk("```{stan output.var=}\n\n```\n", 0, 20); | |
| } | |
| @Handler | |
| void onInsertChunkSQL() | |
| { | |
| server_.defaultSqlConnectionName(new ServerRequestCallback<String>() | |
| { | |
| @Override | |
| public void onResponseReceived(String name) | |
| { | |
| if (name != null) | |
| { | |
| onInsertChunk("```{sql connection=" + name + "}\n\n```\n", 1, 0); | |
| } | |
| else | |
| { | |
| onInsertChunk("```{sql connection=}\n\n```\n", 0, 19); | |
| } | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| Debug.logError(error); | |
| onInsertChunk("```{sql connection=}\n\n```\n", 0, 19); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onInsertChunkD3() | |
| { | |
| if (notebook_ != null) { | |
| Scope setupScope = notebook_.getSetupChunkScope(); | |
| if (setupScope == null) | |
| { | |
| onInsertChunk("```{r setup}\nlibrary(r2d3)\n```\n\n```{d3 data=}\n\n```\n", 4, 12); | |
| } | |
| else { | |
| onInsertChunk("```{d3 data=}\n\n```\n", 0, 12); | |
| } | |
| } | |
| } | |
| @Handler | |
| void onInsertSection() | |
| { | |
| globalDisplay_.promptForText( | |
| "Insert Section", | |
| "Section label:", | |
| MessageDisplay.INPUT_OPTIONAL_TEXT, | |
| new OperationWithInput<String>() { | |
| @Override | |
| public void execute(String label) | |
| { | |
| // move cursor to next insert location | |
| Position pos = moveCursorToNextInsertLocation(); | |
| // truncate length to print margin - 5 | |
| int printMarginColumn = prefs_.printMarginColumn().getValue(); | |
| int length = printMarginColumn - 5; | |
| // truncate label to maxLength - 10 (but always allow at | |
| // least 20 chars for the label) | |
| int maxLabelLength = length - 10; | |
| maxLabelLength = Math.max(maxLabelLength, 20); | |
| if (label.length() > maxLabelLength) | |
| label = label.substring(0, maxLabelLength-1); | |
| // prefix | |
| String prefix = "# "; | |
| if (!label.isEmpty()) | |
| prefix = prefix + label + " "; | |
| // fill to maxLength (bit ensure at least 4 fill characters | |
| // so the section parser is sure to pick it up) | |
| StringBuffer sectionLabel = new StringBuffer(); | |
| sectionLabel.append("\n"); | |
| sectionLabel.append(prefix); | |
| int fillChars = length - prefix.length(); | |
| fillChars = Math.max(fillChars, 4); | |
| for (int i=0; i<fillChars; i++) | |
| sectionLabel.append("-"); | |
| sectionLabel.append("\n\n"); | |
| // insert code and move cursor | |
| docDisplay_.insertCode(sectionLabel.toString(), false); | |
| docDisplay_.setCursorPosition(Position.create(pos.getRow() + 3, | |
| 0)); | |
| docDisplay_.focus(); | |
| } | |
| }); | |
| } | |
| private Position moveCursorToNextInsertLocation() | |
| { | |
| docDisplay_.collapseSelection(true); | |
| if (!docDisplay_.moveSelectionToBlankLine()) | |
| { | |
| int lastRow = docDisplay_.getRowCount(); | |
| int lastCol = docDisplay_.getLength(lastRow); | |
| Position endPos = Position.create(lastRow, lastCol); | |
| docDisplay_.setCursorPosition(endPos); | |
| docDisplay_.insertCode("\n", false); | |
| } | |
| return docDisplay_.getCursorPosition(); | |
| } | |
| public void executeChunk(Position position) | |
| { | |
| docDisplay_.getScopeTree(); | |
| executeSweaveChunk(scopeHelper_.getCurrentSweaveChunk(position), | |
| NotebookQueueUnit.EXEC_MODE_SINGLE, false); | |
| } | |
| public void dequeueChunk(int row) | |
| { | |
| notebook_.dequeueChunk(row); | |
| } | |
| @Handler | |
| void onExecuteCurrentChunk() | |
| { | |
| // HACK: This is just to force the entire function tree to be built. | |
| // It's the easiest way to make sure getCurrentScope() returns | |
| // a Scope with an end. | |
| docDisplay_.getScopeTree(); | |
| executeSweaveChunk(scopeHelper_.getCurrentSweaveChunk(), | |
| NotebookQueueUnit.EXEC_MODE_SINGLE, false); | |
| } | |
| @Handler | |
| void onExecuteNextChunk() | |
| { | |
| // HACK: This is just to force the entire function tree to be built. | |
| // It's the easiest way to make sure getCurrentScope() returns | |
| // a Scope with an end. | |
| docDisplay_.getScopeTree(); | |
| Scope nextChunk = scopeHelper_.getNextSweaveChunk(); | |
| executeSweaveChunk(nextChunk, NotebookQueueUnit.EXEC_MODE_SINGLE, | |
| true); | |
| docDisplay_.setCursorPosition(nextChunk.getBodyStart()); | |
| docDisplay_.ensureCursorVisible(); | |
| } | |
| @Handler | |
| void onExecutePreviousChunks() | |
| { | |
| executeChunks(null, TextEditingTargetScopeHelper.PREVIOUS_CHUNKS); | |
| } | |
| @Handler | |
| void onExecuteSubsequentChunks() | |
| { | |
| executeChunks(null, TextEditingTargetScopeHelper.FOLLOWING_CHUNKS); | |
| } | |
| public void executeChunks(Position position, int which) | |
| { | |
| // null implies we should use current cursor position | |
| if (position == null) | |
| position = docDisplay_.getSelectionStart(); | |
| if (docDisplay_.showChunkOutputInline()) | |
| { | |
| executeChunksNotebookMode(position, which); | |
| return; | |
| } | |
| // HACK: This is just to force the entire function tree to be built. | |
| // It's the easiest way to make sure getCurrentScope() returns | |
| // a Scope with an end. | |
| docDisplay_.getScopeTree(); | |
| // execute the chunks | |
| Scope[] previousScopes = scopeHelper_.getSweaveChunks(position, | |
| which); | |
| StringBuilder builder = new StringBuilder(); | |
| for (Scope scope : previousScopes) | |
| { | |
| if (isRChunk(scope) && isExecutableChunk(scope)) | |
| { | |
| builder.append("# " + scope.getLabel() + "\n"); | |
| builder.append(scopeHelper_.getSweaveChunkText(scope)); | |
| builder.append("\n\n"); | |
| } | |
| } | |
| final String code = builder.toString().trim(); | |
| if (fileType_.isRmd()) | |
| { | |
| final Position positionFinal = position; | |
| docUpdateSentinel_.withSavedDoc(new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| rmarkdownHelper_.prepareForRmdChunkExecution( | |
| docUpdateSentinel_.getId(), | |
| docUpdateSentinel_.getContents(), | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| // compute the language for this chunk | |
| String language = "R"; | |
| if (DocumentMode.isPositionInPythonMode(docDisplay_, positionFinal)) | |
| language = "Python"; | |
| events_.fireEvent(new SendToConsoleEvent(code, language, true)); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| events_.fireEvent(new SendToConsoleEvent(code, true)); | |
| } | |
| } | |
| public void executeChunksNotebookMode(Position position, int which) | |
| { | |
| // HACK: This is just to force the entire function tree to be built. | |
| // It's the easiest way to make sure getCurrentScope() returns | |
| // a Scope with an end. | |
| docDisplay_.getScopeTree(); | |
| // execute the previous chunks | |
| Scope[] previousScopes = scopeHelper_.getSweaveChunks(position, which); | |
| // create job description | |
| String jobDesc = ""; | |
| if (previousScopes.length > 0) | |
| { | |
| if (position != null && | |
| position.getRow() > docDisplay_.getDocumentEnd().getRow()) | |
| jobDesc = "Run All"; | |
| else if (which == TextEditingTargetScopeHelper.PREVIOUS_CHUNKS) | |
| jobDesc = "Run Previous"; | |
| else if (which == TextEditingTargetScopeHelper.FOLLOWING_CHUNKS) | |
| jobDesc = "Run After"; | |
| } | |
| List<ChunkExecUnit> chunks = new ArrayList<ChunkExecUnit>(); | |
| for (Scope scope : previousScopes) | |
| { | |
| if (isExecutableChunk(scope)) | |
| chunks.add( | |
| new ChunkExecUnit(scope, NotebookQueueUnit.EXEC_MODE_BATCH)); | |
| } | |
| if (!chunks.isEmpty()) | |
| notebook_.executeChunks(jobDesc, chunks); | |
| } | |
| @Handler | |
| public void onExecuteSetupChunk() | |
| { | |
| // attempt to find the setup scope by name | |
| Scope setupScope = null; | |
| if (notebook_ != null) | |
| setupScope = notebook_.getSetupChunkScope(); | |
| // if we didn't find it by name, flatten the scope list and find the | |
| // first chunk | |
| if (setupScope == null) | |
| { | |
| ScopeList scopes = new ScopeList(docDisplay_); | |
| for (Scope scope: scopes) | |
| { | |
| if (scope.isChunk()) | |
| { | |
| setupScope = scope; | |
| break; | |
| } | |
| } | |
| } | |
| // if we found a candidate, run it | |
| if (setupScope != null) | |
| { | |
| executeSweaveChunk(setupScope, NotebookQueueUnit.EXEC_MODE_BATCH, | |
| false); | |
| } | |
| } | |
| public void renderLatex() | |
| { | |
| if (mathjax_ != null) | |
| mathjax_.renderLatex(); | |
| } | |
| public void renderLatex(Range range, boolean background) | |
| { | |
| if (mathjax_ != null) | |
| mathjax_.renderLatex(range, background); | |
| } | |
| public String getDefaultNamePrefix() | |
| { | |
| return null; | |
| } | |
| private boolean isRChunk(Scope scope) | |
| { | |
| String labelText = docDisplay_.getLine(scope.getPreamble().getRow()); | |
| Pattern reEngine = Pattern.create(".*engine\\s*=\\s*['\"]([^'\"]*)['\"]"); | |
| Match match = reEngine.match(labelText, 0); | |
| if (match == null) | |
| return true; | |
| String engine = match.getGroup(1).toLowerCase(); | |
| // NOTE: We might want to include 'Rscript' but such chunks are typically | |
| // intended to be run in their own process so it might not make sense to | |
| // collect those here. | |
| return engine.equals("r"); | |
| } | |
| private boolean isExecutableChunk(final Scope chunk) | |
| { | |
| if (!chunk.isChunk()) | |
| return false; | |
| String headerText = docDisplay_.getLine(chunk.getPreamble().getRow()); | |
| Pattern reEvalFalse = Pattern.create("eval\\s*=\\s*F(?:ALSE)?"); | |
| if (reEvalFalse.test(headerText)) | |
| return false; | |
| return true; | |
| } | |
| private void executeSweaveChunk(final Scope chunk, | |
| final int mode, | |
| final boolean scrollNearTop) | |
| { | |
| if (chunk == null) | |
| return; | |
| // command used to execute chunk (we may need to defer it if this | |
| // is an Rmd document as populating params might be necessary) | |
| final Command executeChunk = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| Range range = scopeHelper_.getSweaveChunkInnerRange(chunk); | |
| if (scrollNearTop) | |
| { | |
| docDisplay_.navigateToPosition( | |
| SourcePosition.create(range.getStart().getRow(), | |
| range.getStart().getColumn()), | |
| true); | |
| } | |
| if (!range.isEmpty()) | |
| { | |
| codeExecution_.setLastExecuted(range.getStart(), range.getEnd()); | |
| } | |
| if (fileType_.isRmd() && | |
| docDisplay_.showChunkOutputInline()) | |
| { | |
| // in notebook mode, an empty chunk can refer to external code, | |
| // so always execute it | |
| notebook_.executeChunk(chunk); | |
| } | |
| else if (!range.isEmpty()) | |
| { | |
| String code = scopeHelper_.getSweaveChunkText(chunk); | |
| // compute the language for this chunk | |
| String language = "R"; | |
| if (DocumentMode.isPositionInPythonMode(docDisplay_, chunk.getBodyStart())) | |
| language = "Python"; | |
| events_.fireEvent(new SendToConsoleEvent(code, language, true)); | |
| } | |
| docDisplay_.collapseSelection(true); | |
| } | |
| }; | |
| // Rmd allows server-side prep for chunk execution | |
| if (fileType_.isRmd() && !docDisplay_.showChunkOutputInline()) | |
| { | |
| // ensure source is synced with server | |
| docUpdateSentinel_.withSavedDoc(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| // allow server to prepare for chunk execution | |
| // (e.g. by populating 'params' in the global environment) | |
| rmarkdownHelper_.prepareForRmdChunkExecution( | |
| docUpdateSentinel_.getId(), | |
| docUpdateSentinel_.getContents(), | |
| executeChunk); | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| executeChunk.execute(); | |
| } | |
| } | |
| @Handler | |
| void onJumpTo() | |
| { | |
| statusBar_.getScope().click(); | |
| } | |
| @Handler | |
| void onGoToLine() | |
| { | |
| globalDisplay_.promptForInteger( | |
| "Go to Line", | |
| "Enter line number:", | |
| null, | |
| new ProgressOperationWithInput<Integer>() | |
| { | |
| @Override | |
| public void execute(Integer line, ProgressIndicator indicator) | |
| { | |
| indicator.onCompleted(); | |
| line = Math.max(1, line); | |
| line = Math.min(docDisplay_.getRowCount(), line); | |
| docDisplay_.navigateToPosition( | |
| SourcePosition.create(line-1, 0), | |
| true); | |
| } | |
| }, | |
| null); | |
| } | |
| @Handler | |
| void onCodeCompletion() | |
| { | |
| docDisplay_.codeCompletion(); | |
| } | |
| @Handler | |
| void onGoToHelp() | |
| { | |
| docDisplay_.goToHelp(); | |
| } | |
| @Handler | |
| void onGoToDefinition() | |
| { | |
| docDisplay_.goToDefinition(); | |
| } | |
| @Handler | |
| void onFindAll() | |
| { | |
| docDisplay_.selectAll(docDisplay_.getSelectionValue()); | |
| } | |
| @Handler | |
| void onFindUsages() | |
| { | |
| cppHelper_.findUsages(); | |
| } | |
| @Handler | |
| public void onSetWorkingDirToActiveDoc() | |
| { | |
| // get path | |
| String activeDocPath = docUpdateSentinel_.getPath(); | |
| if (activeDocPath != null) | |
| { | |
| FileSystemItem wdPath = | |
| FileSystemItem.createFile(activeDocPath).getParentPath(); | |
| consoleDispatcher_.executeSetWd(wdPath, true); | |
| } | |
| else | |
| { | |
| globalDisplay_.showMessage( | |
| MessageDialog.WARNING, | |
| "Source File Not Saved", | |
| "The currently active source file is not saved so doesn't " + | |
| "have a directory to change into."); | |
| return; | |
| } | |
| } | |
| private String stangle(String sweaveStr) | |
| { | |
| ScopeList chunks = new ScopeList(docDisplay_); | |
| chunks.selectAll(ScopeList.CHUNK); | |
| StringBuilder code = new StringBuilder(); | |
| for (Scope chunk : chunks) | |
| { | |
| String text = scopeHelper_.getSweaveChunkText(chunk); | |
| code.append(text); | |
| if (text.length() > 0 && text.charAt(text.length()-1) != '\n') | |
| code.append('\n'); | |
| } | |
| return code.toString(); | |
| } | |
| @Handler | |
| void onPreviewJS() | |
| { | |
| previewJS(); | |
| } | |
| @Handler | |
| void onPreviewSql() | |
| { | |
| previewSql(); | |
| } | |
| @Handler | |
| void onSourceActiveDocument() | |
| { | |
| sourceActiveDocument(false); | |
| } | |
| @Handler | |
| void onSourceActiveDocumentWithEcho() | |
| { | |
| sourceActiveDocument(true); | |
| } | |
| @Handler | |
| void onSourceAsJob() | |
| { | |
| saveThenExecute(null, () -> | |
| { | |
| events_.fireEvent(new JobRunScriptEvent(getPath())); | |
| }); | |
| } | |
| @Handler | |
| void onProfileCode() | |
| { | |
| dependencyManager_.withProfvis("The profiler", new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| codeExecution_.executeSelection(true, true, "profvis::profvis", true); | |
| } | |
| }); | |
| } | |
| private void sourceActiveDocument(final boolean echo) | |
| { | |
| docDisplay_.focus(); | |
| // If this is a Python file, use reticulate. | |
| if (fileType_.isPython()) | |
| { | |
| sourcePython(); | |
| return; | |
| } | |
| if (fileType_.isR()) | |
| { | |
| if (extendedType_.startsWith(SourceDocument.XT_SHINY_PREFIX)) | |
| { | |
| // If the document being sourced is a Shiny file, run the app instead. | |
| runShinyApp(); | |
| return; | |
| } | |
| else if (extendedType_ == SourceDocument.XT_PLUMBER_API) | |
| { | |
| // If the document being sourced in a Plumber file, run the API instead. | |
| runPlumberAPI(); | |
| return; | |
| } | |
| } | |
| // if the document is an R Markdown notebook, run all its chunks instead | |
| if (fileType_.isRmd() && isRmdNotebook()) | |
| { | |
| onExecuteAllCode(); | |
| return; | |
| } | |
| // If the document being sourced is a script then use that codepath | |
| if (fileType_.isScript()) | |
| { | |
| runScript(); | |
| return; | |
| } | |
| // If the document is previewable | |
| if (fileType_.canPreviewFromR()) | |
| { | |
| previewFromR(); | |
| return; | |
| } | |
| String code = docDisplay_.getCode(); | |
| if (code != null && code.trim().length() > 0) | |
| { | |
| // R 2.14 prints a warning when sourcing a file with no trailing \n | |
| if (!code.endsWith("\n")) | |
| code = code + "\n"; | |
| boolean sweave = | |
| fileType_.canCompilePDF() || | |
| fileType_.canKnitToHTML() || | |
| fileType_.isRpres(); | |
| RnwWeave rnwWeave = compilePdfHelper_.getActiveRnwWeave(); | |
| final boolean forceEcho = sweave && (rnwWeave != null) ? rnwWeave.forceEchoOnExec() : false; | |
| // NOTE: we always set echo to true for knitr because knitr doesn't | |
| // require print statements so if you don't echo to the console | |
| // then you don't see any of the output | |
| boolean saveWhenSourcing = fileType_.isCpp() || | |
| docDisplay_.hasBreakpoints() || (prefs_.saveBeforeSourcing().getValue() && (getPath() != null) && !sweave); | |
| if ((dirtyState_.getValue() || sweave) && !saveWhenSourcing) | |
| { | |
| server_.saveActiveDocument(code, | |
| sweave, | |
| compilePdfHelper_.getActiveRnwWeaveName(), | |
| new SimpleRequestCallback<Void>() { | |
| @Override | |
| public void onResponseReceived(Void response) | |
| { | |
| consoleDispatcher_.executeSourceCommand( | |
| "~/.active-rstudio-document", | |
| fileType_, | |
| "UTF-8", | |
| activeCodeIsAscii(), | |
| forceEcho ? true : echo, | |
| prefs_.focusConsoleAfterExec().getValue(), | |
| docDisplay_.hasBreakpoints()); | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| Command sourceCommand = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| if (docDisplay_.hasBreakpoints()) | |
| { | |
| hideBreakpointWarningBar(); | |
| } | |
| consoleDispatcher_.executeSourceCommand( | |
| getPath(), | |
| fileType_, | |
| docUpdateSentinel_.getEncoding(), | |
| activeCodeIsAscii(), | |
| forceEcho ? true : echo, | |
| prefs_.focusConsoleAfterExec().getValue(), | |
| docDisplay_.hasBreakpoints()); | |
| } | |
| }; | |
| if (saveWhenSourcing && (dirtyState_.getValue() || (getPath() == null))) | |
| saveThenExecute(null, sourceCommand); | |
| else | |
| sourceCommand.execute(); | |
| } | |
| } | |
| // update pref if necessary | |
| if (prefs_.sourceWithEcho().getValue() != echo) | |
| { | |
| prefs_.sourceWithEcho().setGlobalValue(echo, true); | |
| prefs_.writeUIPrefs(); | |
| } | |
| } | |
| private void runShinyApp() | |
| { | |
| sourceBuildHelper_.withSaveFilesBeforeCommand(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| events_.fireEvent(new LaunchShinyApplicationEvent(getPath(), | |
| getExtendedFileType())); | |
| } | |
| }, "Run Shiny Application"); | |
| } | |
| private void runPlumberAPI() | |
| { | |
| sourceBuildHelper_.withSaveFilesBeforeCommand(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| events_.fireEvent(new LaunchPlumberAPIEvent(getPath())); | |
| } | |
| }, "Run Plumber API"); | |
| } | |
| private void sourcePython() | |
| { | |
| saveThenExecute(null, () -> { | |
| dependencyManager_.withReticulate( | |
| "Executing Python", | |
| "Sourcing Python scripts", | |
| () -> { | |
| String command = "reticulate::source_python('" + getPath() + "')"; | |
| events_.fireEvent(new SendToConsoleEvent(command, true)); | |
| }); | |
| }); | |
| } | |
| private void runScript() | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| String interpreter = fileType_.getScriptInterpreter(); | |
| server_.getScriptRunCommand( | |
| interpreter, | |
| getPath(), | |
| new SimpleRequestCallback<String>() { | |
| @Override | |
| public void onResponseReceived(String cmd) | |
| { | |
| events_.fireEvent(new SendToConsoleEvent(cmd, true)); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| private void previewFromR() | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| server_.getMinimalSourcePath( | |
| getPath(), | |
| new SimpleRequestCallback<String>() { | |
| @Override | |
| public void onResponseReceived(String path) | |
| { | |
| String cmd = fileType_.createPreviewCommand(path); | |
| if (cmd != null) | |
| events_.fireEvent(new SendToConsoleEvent(cmd, true)); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| private boolean activeCodeIsAscii() | |
| { | |
| String code = docDisplay_.getCode(); | |
| for (int i=0; i< code.length(); i++) | |
| { | |
| if (code.charAt(i) > 127) | |
| return false; | |
| } | |
| return true; | |
| } | |
| @Handler | |
| void onExecuteLastCode() | |
| { | |
| docDisplay_.focus(); | |
| codeExecution_.executeLastCode(); | |
| } | |
| @Handler | |
| void onKnitDocument() | |
| { | |
| onPreviewHTML(); | |
| } | |
| @Handler | |
| void onPreviewHTML() | |
| { | |
| // last ditch extended type detection | |
| String extendedType = extendedType_; | |
| extendedType = rmarkdownHelper_.detectExtendedType(docDisplay_.getCode(), | |
| extendedType, | |
| fileType_); | |
| if (extendedType == SourceDocument.XT_RMARKDOWN) | |
| { | |
| renderRmd(); | |
| } | |
| else if (fileType_.isRd()) | |
| previewRd(); | |
| else if (fileType_.isRpres()) | |
| previewRpresentation(); | |
| else if (fileType_.isR()) | |
| onCompileNotebook(); | |
| else | |
| previewHTML(); | |
| } | |
| void previewRpresentation() | |
| { | |
| SessionInfo sessionInfo = session_.getSessionInfo(); | |
| if (!fileTypeCommands_.getHTMLCapabiliites().isRMarkdownSupported()) | |
| { | |
| globalDisplay_.showMessage( | |
| MessageDisplay.MSG_WARNING, | |
| "Unable to Preview", | |
| "R Presentations require the knitr package " + | |
| "(version 1.2 or higher)"); | |
| return; | |
| } | |
| PresentationState state = sessionInfo.getPresentationState(); | |
| // if we are showing a tutorial then don't allow preview | |
| if (state.isTutorial()) | |
| { | |
| globalDisplay_.showMessage( | |
| MessageDisplay.MSG_WARNING, | |
| "Unable to Preview", | |
| "R Presentations cannot be previewed when a Tutorial " + | |
| "is active"); | |
| return; | |
| } | |
| // if this presentation is already showing then just activate | |
| if (state.isActive() && | |
| state.getFilePath().equals(docUpdateSentinel_.getPath())) | |
| { | |
| commands_.activatePresentation().execute(); | |
| save(); | |
| } | |
| // otherwise reload | |
| else | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| server_.showPresentationPane(docUpdateSentinel_.getPath(), | |
| new VoidServerRequestCallback()); | |
| } | |
| }); | |
| } | |
| } | |
| void previewRd() | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| String previewURL = "help/preview?file="; | |
| previewURL += URL.encodeQueryString(docUpdateSentinel_.getPath()); | |
| events_.fireEvent(new ShowHelpEvent(previewURL)) ; | |
| } | |
| }); | |
| } | |
| void previewJS() | |
| { | |
| verifyD3Prequisites(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| jsHelper_.previewJS(TextEditingTarget.this); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| void previewSql() | |
| { | |
| verifySqlPrequisites(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| sqlHelper_.previewSql(TextEditingTarget.this); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| void customSource() | |
| { | |
| rHelper_.customSource(TextEditingTarget.this); | |
| } | |
| void renderRmd() | |
| { | |
| renderRmd(null); | |
| } | |
| void renderRmd(final String paramsFile) | |
| { | |
| events_.fireEvent(new RmdRenderPendingEvent(docUpdateSentinel_.getId())); | |
| final int type = isShinyDoc() ? RmdOutput.TYPE_SHINY: | |
| isRmdNotebook() ? RmdOutput.TYPE_NOTEBOOK: | |
| RmdOutput.TYPE_STATIC; | |
| final Command renderCommand = new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| boolean asTempfile = isPackageDocumentationFile(); | |
| String viewerType = RmdEditorOptions.getString( | |
| getRmdFrontMatter(), RmdEditorOptions.PREVIEW_IN, null); | |
| rmarkdownHelper_.renderRMarkdown( | |
| docUpdateSentinel_.getPath(), | |
| docDisplay_.getCursorPosition().getRow() + 1, | |
| null, | |
| docUpdateSentinel_.getEncoding(), | |
| paramsFile, | |
| asTempfile, | |
| type, | |
| false, | |
| rmarkdownHelper_.getKnitWorkingDir(docUpdateSentinel_), | |
| viewerType); | |
| } | |
| }; | |
| final Command saveCommand = new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| saveThenExecute(null, renderCommand); | |
| } | |
| }; | |
| // save before rendering if the document is dirty or has never been saved; | |
| // otherwise render directly | |
| Command command = | |
| docUpdateSentinel_.getPath() == null || dirtyState_.getValue() ? | |
| saveCommand : renderCommand; | |
| if (isRmdNotebook()) | |
| dependencyManager_.withRMarkdown("Creating R Notebooks", command); | |
| else | |
| command.execute(); | |
| } | |
| public boolean isRmdNotebook() | |
| { | |
| List<String> outputFormats = getOutputFormats(); | |
| return outputFormats.size() > 0 && | |
| outputFormats.get(0) == RmdOutputFormat.OUTPUT_HTML_NOTEBOOK; | |
| } | |
| public boolean hasRmdNotebook() | |
| { | |
| List<String> outputFormats = getOutputFormats(); | |
| for (String format: outputFormats) | |
| { | |
| if (format == RmdOutputFormat.OUTPUT_HTML_NOTEBOOK) | |
| return true; | |
| } | |
| return false; | |
| } | |
| private boolean isShinyDoc() | |
| { | |
| try | |
| { | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return false; | |
| return rmarkdownHelper_.isRuntimeShiny(yaml); | |
| } | |
| catch(Exception e) | |
| { | |
| Debug.log(e.getMessage()); | |
| return false; | |
| } | |
| } | |
| private boolean isShinyPrerenderedDoc() | |
| { | |
| try | |
| { | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return false; | |
| return rmarkdownHelper_.isRuntimeShinyPrerendered(yaml); | |
| } | |
| catch(Exception e) | |
| { | |
| Debug.log(e.getMessage()); | |
| return false; | |
| } | |
| } | |
| private String getCustomKnit() | |
| { | |
| try | |
| { | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return new String(); | |
| return rmarkdownHelper_.getCustomKnit(yaml); | |
| } | |
| catch(Exception e) | |
| { | |
| Debug.log(e.getMessage()); | |
| return new String(); | |
| } | |
| } | |
| void previewHTML() | |
| { | |
| // validate pre-reqs | |
| if (!rmarkdownHelper_.verifyPrerequisites(view_, fileType_)) | |
| return; | |
| doHtmlPreview(new Provider<HTMLPreviewParams>() | |
| { | |
| @Override | |
| public HTMLPreviewParams get() | |
| { | |
| return HTMLPreviewParams.create(docUpdateSentinel_.getPath(), | |
| docUpdateSentinel_.getEncoding(), | |
| fileType_.isMarkdown(), | |
| fileType_.requiresKnit(), | |
| false); | |
| } | |
| }); | |
| } | |
| private void doHtmlPreview(final Provider<HTMLPreviewParams> pParams) | |
| { | |
| // command to show the preview window | |
| final Command showPreviewWindowCommand = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| HTMLPreviewParams params = pParams.get(); | |
| events_.fireEvent(new ShowHTMLPreviewEvent(params)); | |
| } | |
| }; | |
| // command to run the preview | |
| final Command runPreviewCommand = new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| final HTMLPreviewParams params = pParams.get(); | |
| server_.previewHTML(params, new SimpleRequestCallback<Boolean>()); | |
| } | |
| }; | |
| if (pParams.get().isNotebook()) | |
| { | |
| saveThenExecute(null, new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| generateNotebook(new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| showPreviewWindowCommand.execute(); | |
| runPreviewCommand.execute(); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| // if the document is new and unsaved, then resolve that and then | |
| // show the preview window -- it won't activate in web mode | |
| // due to popup activation rules but at least it will show up | |
| else if (isNewDoc()) | |
| { | |
| saveThenExecute(null, CommandUtil.join(showPreviewWindowCommand, | |
| runPreviewCommand)); | |
| } | |
| // otherwise if it's dirty then show the preview window first (to | |
| // beat the popup blockers) then save & run | |
| else if (dirtyState().getValue()) | |
| { | |
| showPreviewWindowCommand.execute(); | |
| saveThenExecute(null, runPreviewCommand); | |
| } | |
| // otherwise show the preview window then run the preview | |
| else | |
| { | |
| showPreviewWindowCommand.execute(); | |
| runPreviewCommand.execute(); | |
| } | |
| } | |
| private void generateNotebook(final Command executeOnSuccess) | |
| { | |
| // default title | |
| String defaultTitle = docUpdateSentinel_.getProperty(NOTEBOOK_TITLE); | |
| if (StringUtil.isNullOrEmpty(defaultTitle)) | |
| defaultTitle = FileSystemItem.getNameFromPath(docUpdateSentinel_.getPath()); | |
| // default author | |
| String defaultAuthor = docUpdateSentinel_.getProperty(NOTEBOOK_AUTHOR); | |
| if (StringUtil.isNullOrEmpty(defaultAuthor)) | |
| { | |
| defaultAuthor = prefs_.compileNotebookOptions().getValue().getAuthor(); | |
| if (StringUtil.isNullOrEmpty(defaultAuthor)) | |
| defaultAuthor = session_.getSessionInfo().getUserIdentity(); | |
| } | |
| // default type | |
| String defaultType = docUpdateSentinel_.getProperty(NOTEBOOK_TYPE); | |
| if (StringUtil.isNullOrEmpty(defaultType)) | |
| { | |
| defaultType = prefs_.compileNotebookOptions().getValue().getType(); | |
| if (StringUtil.isNullOrEmpty(defaultType)) | |
| defaultType = CompileNotebookOptions.TYPE_DEFAULT; | |
| } | |
| CompileNotebookOptionsDialog dialog = new CompileNotebookOptionsDialog( | |
| getId(), | |
| defaultTitle, | |
| defaultAuthor, | |
| defaultType, | |
| new OperationWithInput<CompileNotebookOptions>() | |
| { | |
| @Override | |
| public void execute(CompileNotebookOptions input) | |
| { | |
| server_.createNotebook( | |
| input, | |
| new SimpleRequestCallback<CompileNotebookResult>() | |
| { | |
| @Override | |
| public void onResponseReceived(CompileNotebookResult response) | |
| { | |
| if (response.getSucceeded()) | |
| { | |
| executeOnSuccess.execute(); | |
| } | |
| else | |
| { | |
| globalDisplay_.showErrorMessage( | |
| "Unable to Compile Report", | |
| response.getFailureMessage()); | |
| } | |
| } | |
| }); | |
| // save options for this document | |
| HashMap<String, String> changedProperties = new HashMap<String, String>(); | |
| changedProperties.put(NOTEBOOK_TITLE, input.getNotebookTitle()); | |
| changedProperties.put(NOTEBOOK_AUTHOR, input.getNotebookAuthor()); | |
| changedProperties.put(NOTEBOOK_TYPE, input.getNotebookType()); | |
| docUpdateSentinel_.modifyProperties(changedProperties, null); | |
| // save global prefs | |
| CompileNotebookPrefs prefs = CompileNotebookPrefs.create( | |
| input.getNotebookAuthor(), | |
| input.getNotebookType()); | |
| if (!CompileNotebookPrefs.areEqual( | |
| prefs, | |
| prefs_.compileNotebookOptions().getValue())) | |
| { | |
| prefs_.compileNotebookOptions().setGlobalValue(prefs); | |
| prefs_.writeUIPrefs(); | |
| } | |
| } | |
| } | |
| ); | |
| dialog.showModal(); | |
| } | |
| @Handler | |
| void onCompileNotebook() | |
| { | |
| if (session_.getSessionInfo().getRMarkdownPackageAvailable()) | |
| { | |
| saveThenExecute(null, new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| rmarkdownHelper_.renderNotebookv2(docUpdateSentinel_, null); | |
| } | |
| }); | |
| } | |
| else | |
| { | |
| if (!rmarkdownHelper_.verifyPrerequisites("Compile Report", | |
| view_, | |
| FileTypeRegistry.RMARKDOWN)) | |
| { | |
| return; | |
| } | |
| doHtmlPreview(new Provider<HTMLPreviewParams>() | |
| { | |
| @Override | |
| public HTMLPreviewParams get() | |
| { | |
| return HTMLPreviewParams.create(docUpdateSentinel_.getPath(), | |
| docUpdateSentinel_.getEncoding(), | |
| true, | |
| true, | |
| true); | |
| } | |
| }); | |
| } | |
| } | |
| @Handler | |
| void onCompilePDF() | |
| { | |
| String pdfPreview = prefs_.pdfPreview().getValue(); | |
| boolean showPdf = !pdfPreview.equals(UIPrefsAccessor.PDF_PREVIEW_NONE); | |
| boolean useInternalPreview = | |
| pdfPreview.equals(UIPrefsAccessor.PDF_PREVIEW_RSTUDIO); | |
| boolean useDesktopSynctexPreview = | |
| pdfPreview.equals(UIPrefsAccessor.PDF_PREVIEW_DESKTOP_SYNCTEX) && | |
| Desktop.isDesktop(); | |
| String action = new String(); | |
| if (showPdf && !useInternalPreview && !useDesktopSynctexPreview) | |
| action = "view_external"; | |
| handlePdfCommand(action, useInternalPreview, null); | |
| } | |
| @Handler | |
| void onKnitWithParameters() | |
| { | |
| saveThenExecute(null, new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| rmarkdownHelper_.getRMarkdownParamsFile( | |
| docUpdateSentinel_.getPath(), | |
| docUpdateSentinel_.getEncoding(), | |
| activeCodeIsAscii(), | |
| new CommandWithArg<String>() { | |
| @Override | |
| public void execute(String paramsFile) | |
| { | |
| // null return means user cancelled | |
| if (paramsFile != null) | |
| { | |
| // special "none" value means no parameters | |
| if (paramsFile.equals("none")) | |
| { | |
| new RMarkdownNoParamsDialog().showModal(); | |
| } | |
| else | |
| { | |
| renderRmd(paramsFile); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onClearKnitrCache() | |
| { | |
| withSavedDoc(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| // determine the cache path (use relative path if possible) | |
| String path = docUpdateSentinel_.getPath(); | |
| FileSystemItem fsi = FileSystemItem.createFile(path); | |
| path = fsi.getParentPath().completePath(fsi.getStem() + "_cache"); | |
| String relativePath = FileSystemItem.createFile(path).getPathRelativeTo( | |
| workbenchContext_.getCurrentWorkingDir()); | |
| if (relativePath != null) | |
| path = relativePath; | |
| final String docPath = path; | |
| globalDisplay_.showYesNoMessage( | |
| MessageDialog.QUESTION, | |
| "Clear Knitr Cache", | |
| "Clearing the Knitr cache will delete the cache " + | |
| "directory for " + docPath + ". " + | |
| "\n\nAre you sure you want to clear the cache now?", | |
| false, | |
| new Operation() { | |
| @Override | |
| public void execute() | |
| { | |
| String code = "unlink(" + | |
| ConsoleDispatcher.escapedPath(docPath) + | |
| ", recursive = TRUE)"; | |
| events_.fireEvent(new SendToConsoleEvent(code, true)); | |
| } | |
| }, | |
| null, | |
| true); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onClearPrerenderedOutput() | |
| { | |
| withSavedDoc(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| // determine the output path (use relative path if possible) | |
| String path = docUpdateSentinel_.getPath(); | |
| String relativePath = FileSystemItem.createFile(path).getPathRelativeTo( | |
| workbenchContext_.getCurrentWorkingDir()); | |
| if (relativePath != null) | |
| path = relativePath; | |
| final String docPath = path; | |
| globalDisplay_.showYesNoMessage( | |
| MessageDialog.QUESTION, | |
| "Clear Prerendered Output", | |
| "This will remove all previously generated output " + | |
| "for " + docPath + " (html, prerendered data, knitr cache, etc.)." + | |
| "\n\nAre you sure you want to clear the output now?", | |
| false, | |
| new Operation() { | |
| @Override | |
| public void execute() | |
| { | |
| String code = "rmarkdown::shiny_prerendered_clean(" + | |
| ConsoleDispatcher.escapedPath(docPath) + | |
| ")"; | |
| events_.fireEvent(new SendToConsoleEvent(code, true)); | |
| } | |
| }, | |
| null, | |
| true); | |
| } | |
| }); | |
| } | |
| @Handler | |
| void onSynctexSearch() | |
| { | |
| doSynctexSearch(true); | |
| } | |
| private void doSynctexSearch(boolean fromClick) | |
| { | |
| SourceLocation sourceLocation = getSelectionAsSourceLocation(fromClick); | |
| if (sourceLocation == null) | |
| return; | |
| // compute the target pdf | |
| FileSystemItem editorFile = FileSystemItem.createFile( | |
| docUpdateSentinel_.getPath()); | |
| FileSystemItem targetFile = compilePdfHelper_.getTargetFile(editorFile); | |
| String pdfFile = | |
| targetFile.getParentPath().completePath(targetFile.getStem() + ".pdf"); | |
| synctex_.forwardSearch(pdfFile, sourceLocation); | |
| } | |
| private SourceLocation getSelectionAsSourceLocation(boolean fromClick) | |
| { | |
| // get doc path (bail if the document is unsaved) | |
| String file = docUpdateSentinel_.getPath(); | |
| if (file == null) | |
| return null; | |
| Position selPos = docDisplay_.getSelectionStart(); | |
| int line = selPos.getRow() + 1; | |
| int column = selPos.getColumn() + 1; | |
| return SourceLocation.create(file, line, column, fromClick); | |
| } | |
| @Handler | |
| void onQuickAddNext() | |
| { | |
| docDisplay_.quickAddNext(); | |
| } | |
| @Handler | |
| void onFindReplace() | |
| { | |
| view_.showFindReplace(true); | |
| } | |
| @Handler | |
| void onFindNext() | |
| { | |
| view_.findNext(); | |
| } | |
| @Handler | |
| void onFindPrevious() | |
| { | |
| view_.findPrevious(); | |
| } | |
| @Handler | |
| void onFindSelectAll() | |
| { | |
| view_.findSelectAll(); | |
| } | |
| @Handler | |
| void onFindFromSelection() | |
| { | |
| view_.findFromSelection(); | |
| docDisplay_.focus(); | |
| } | |
| @Handler | |
| void onReplaceAndFind() | |
| { | |
| view_.replaceAndFind(); | |
| } | |
| @Override | |
| public Position search(String regex) | |
| { | |
| return search(Position.create(0, 0), regex); | |
| } | |
| @Override | |
| public Position search(Position startPos, String regex) | |
| { | |
| InputEditorSelection sel = docDisplay_.search(regex, | |
| false, | |
| false, | |
| false, | |
| false, | |
| startPos, | |
| null, | |
| true); | |
| if (sel != null) | |
| return docDisplay_.selectionToPosition(sel.getStart()); | |
| else | |
| return null; | |
| } | |
| @Handler | |
| void onFold() | |
| { | |
| if (useScopeTreeFolding()) | |
| { | |
| Range range = Range.fromPoints(docDisplay_.getSelectionStart(), | |
| docDisplay_.getSelectionEnd()); | |
| if (range.isEmpty()) | |
| { | |
| // If no selection, fold the innermost non-anonymous scope | |
| Scope scope = docDisplay_.getCurrentScope(); | |
| while (scope != null && scope.isAnon()) | |
| scope = scope.getParentScope(); | |
| if (scope == null || scope.isTopLevel()) | |
| return; | |
| docDisplay_.addFoldFromRow(scope.getFoldStart().getRow()); | |
| } | |
| else | |
| { | |
| // If selection, fold the selection | |
| docDisplay_.addFold(range); | |
| } | |
| } | |
| else | |
| { | |
| int row = docDisplay_.getSelectionStart().getRow(); | |
| docDisplay_.addFoldFromRow(row); | |
| } | |
| } | |
| @Handler | |
| void onUnfold() | |
| { | |
| if (useScopeTreeFolding()) | |
| { | |
| Range range = Range.fromPoints(docDisplay_.getSelectionStart(), | |
| docDisplay_.getSelectionEnd()); | |
| if (range.isEmpty()) | |
| { | |
| // If no selection, either: | |
| // | |
| // 1) Unfold a fold containing the cursor, or | |
| // 2) Unfold the closest fold on the current row. | |
| Position pos = docDisplay_.getCursorPosition(); | |
| AceFold containingCandidate = null; | |
| AceFold startCandidate = null; | |
| AceFold endCandidate = null; | |
| for (AceFold f : JsUtil.asIterable(docDisplay_.getFolds())) | |
| { | |
| // Check to see whether this fold contains the cursor position. | |
| if (f.getRange().contains(pos)) | |
| { | |
| if (containingCandidate == null || | |
| containingCandidate.getRange().contains(f.getRange())) | |
| { | |
| containingCandidate = f; | |
| } | |
| } | |
| if (startCandidate == null | |
| && f.getStart().getRow() == pos.getRow() | |
| && f.getStart().getColumn() >= pos.getColumn()) | |
| { | |
| startCandidate = f; | |
| } | |
| if (startCandidate == null && | |
| f.getEnd().getRow() == pos.getRow() && | |
| f.getEnd().getColumn() <= pos.getColumn()) | |
| { | |
| endCandidate = f; | |
| } | |
| } | |
| if (containingCandidate != null) | |
| { | |
| docDisplay_.unfold(containingCandidate); | |
| } | |
| else if (startCandidate == null ^ endCandidate == null) | |
| { | |
| docDisplay_.unfold(startCandidate != null ? startCandidate | |
| : endCandidate); | |
| } | |
| else if (startCandidate != null && endCandidate != null) | |
| { | |
| // Both are candidates; see which is closer | |
| int startDelta = startCandidate.getStart().getColumn() - pos.getColumn(); | |
| int endDelta = pos.getColumn() - endCandidate.getEnd().getColumn(); | |
| docDisplay_.unfold(startDelta <= endDelta? startCandidate | |
| : endCandidate); | |
| } | |
| } | |
| else | |
| { | |
| // If selection, unfold the selection | |
| docDisplay_.unfold(range); | |
| } | |
| } | |
| else | |
| { | |
| int row = docDisplay_.getSelectionStart().getRow(); | |
| docDisplay_.unfold(row); | |
| } | |
| } | |
| @Handler | |
| void onFoldAll() | |
| { | |
| if (useScopeTreeFolding()) | |
| { | |
| // Fold all except anonymous braces | |
| HashSet<Integer> rowsFolded = new HashSet<Integer>(); | |
| for (AceFold f : JsUtil.asIterable(docDisplay_.getFolds())) | |
| rowsFolded.add(f.getStart().getRow()); | |
| ScopeList scopeList = new ScopeList(docDisplay_); | |
| scopeList.removeAll(ScopeList.ANON_BRACE); | |
| for (Scope scope : scopeList) | |
| { | |
| int row = scope.getFoldStart().getRow(); | |
| if (!rowsFolded.contains(row)) | |
| docDisplay_.addFoldFromRow(row); | |
| } | |
| } | |
| else | |
| { | |
| docDisplay_.foldAll(); | |
| } | |
| } | |
| @Handler | |
| void onUnfoldAll() | |
| { | |
| if (useScopeTreeFolding()) | |
| { | |
| for (AceFold f : JsUtil.asIterable(docDisplay_.getFolds())) | |
| docDisplay_.unfold(f); | |
| } | |
| else | |
| { | |
| docDisplay_.unfoldAll(); | |
| } | |
| } | |
| @Handler | |
| void onToggleEditorTokenInfo() | |
| { | |
| docDisplay_.toggleTokenInfo(); | |
| } | |
| boolean useScopeTreeFolding() | |
| { | |
| return docDisplay_.hasCodeModelScopeTree(); | |
| } | |
| void handlePdfCommand(final String completedAction, | |
| final boolean useInternalPreview, | |
| final Command onBeforeCompile) | |
| { | |
| if (fileType_.isRnw() && prefs_.alwaysEnableRnwConcordance().getValue()) | |
| compilePdfHelper_.ensureRnwConcordance(); | |
| // if the document has been previously saved then we should execute | |
| // the onBeforeCompile command immediately | |
| final boolean isNewDoc = isNewDoc(); | |
| if (!isNewDoc && (onBeforeCompile != null)) | |
| onBeforeCompile.execute(); | |
| saveThenExecute(null, new Command() | |
| { | |
| public void execute() | |
| { | |
| // if this was a new doc then we still need to execute the | |
| // onBeforeCompile command | |
| if (isNewDoc && (onBeforeCompile != null)) | |
| onBeforeCompile.execute(); | |
| String path = docUpdateSentinel_.getPath(); | |
| if (path != null) | |
| { | |
| String encoding = StringUtil.notNull( | |
| docUpdateSentinel_.getEncoding()); | |
| fireCompilePdfEvent(path, | |
| encoding, | |
| completedAction, | |
| useInternalPreview); | |
| } | |
| } | |
| }); | |
| } | |
| private void fireCompilePdfEvent(String path, | |
| String encoding, | |
| String completedAction, | |
| boolean useInternalPreview) | |
| { | |
| // first validate the path to make sure it doesn't contain spaces | |
| FileSystemItem file = FileSystemItem.createFile(path); | |
| if (file.getName().indexOf(' ') != -1) | |
| { | |
| globalDisplay_.showErrorMessage( | |
| "Invalid Filename", | |
| "The file '" + file.getName() + "' cannot be compiled to " + | |
| "a PDF because TeX does not understand paths with spaces. " + | |
| "If you rename the file to remove spaces then " + | |
| "PDF compilation will work correctly."); | |
| return; | |
| } | |
| CompilePdfEvent event = new CompilePdfEvent( | |
| compilePdfHelper_.getTargetFile(file), | |
| encoding, | |
| getSelectionAsSourceLocation(false), | |
| completedAction, | |
| useInternalPreview); | |
| events_.fireEvent(event); | |
| } | |
| private Command postSaveCommand() | |
| { | |
| return new Command() | |
| { | |
| public void execute() | |
| { | |
| // fire source document saved event | |
| FileSystemItem file = FileSystemItem.createFile( | |
| docUpdateSentinel_.getPath()); | |
| events_.fireEvent(new SourceFileSaveCompletedEvent( | |
| file, | |
| docUpdateSentinel_.getContents(), | |
| docDisplay_.getCursorPosition())); | |
| // check for source on save | |
| if (fileType_.canSourceOnSave() && docUpdateSentinel_.sourceOnSave()) | |
| { | |
| if (fileType_.isRd()) | |
| { | |
| previewRd(); | |
| } | |
| else if (fileType_.isJS()) | |
| { | |
| if (extendedType_ == SourceDocument.XT_JS_PREVIEWABLE) | |
| previewJS(); | |
| } | |
| else if (fileType_.isSql()) | |
| { | |
| if (extendedType_ == SourceDocument.XT_SQL_PREVIEWABLE) | |
| previewSql(); | |
| } | |
| else if (fileType_.canPreviewFromR()) | |
| { | |
| previewFromR(); | |
| } | |
| else if (fileType_.isR() && extendedType_ == SourceDocument.XT_R_CUSTOM_SOURCE) | |
| { | |
| customSource(); | |
| } | |
| else | |
| { | |
| if (docDisplay_.hasBreakpoints()) | |
| { | |
| hideBreakpointWarningBar(); | |
| } | |
| consoleDispatcher_.executeSourceCommand( | |
| docUpdateSentinel_.getPath(), | |
| fileType_, | |
| docUpdateSentinel_.getEncoding(), | |
| activeCodeIsAscii(), | |
| false, | |
| false, | |
| docDisplay_.hasBreakpoints()); | |
| } | |
| } | |
| } | |
| }; | |
| } | |
| public void checkForExternalEdit() | |
| { | |
| if (!externalEditCheckInterval_.hasElapsed()) | |
| return; | |
| externalEditCheckInterval_.reset(); | |
| externalEditCheckInvalidation_.invalidate(); | |
| // If the doc has never been saved, don't even bother checking | |
| if (getPath() == null) | |
| return; | |
| // If we're already waiting for the user to respond to an edit event, bail | |
| if (isWaitingForUserResponseToExternalEdit_) | |
| return; | |
| final Invalidation.Token token = externalEditCheckInvalidation_.getInvalidationToken(); | |
| server_.checkForExternalEdit( | |
| id_, | |
| new ServerRequestCallback<CheckForExternalEditResult>() | |
| { | |
| @Override | |
| public void onResponseReceived(CheckForExternalEditResult response) | |
| { | |
| if (token.isInvalid()) | |
| return; | |
| if (response.isDeleted()) | |
| { | |
| if (ignoreDeletes_) | |
| return; | |
| isWaitingForUserResponseToExternalEdit_ = true; | |
| globalDisplay_.showYesNoMessage( | |
| GlobalDisplay.MSG_WARNING, | |
| "File Deleted", | |
| "The file " + | |
| StringUtil.notNull(docUpdateSentinel_.getPath()) + | |
| " has been deleted or moved. " + | |
| "Do you want to close this file now?", | |
| false, | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| isWaitingForUserResponseToExternalEdit_ = false; | |
| CloseEvent.fire(TextEditingTarget.this, null); | |
| } | |
| }, | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| isWaitingForUserResponseToExternalEdit_ = false; | |
| externalEditCheckInterval_.reset(); | |
| ignoreDeletes_ = true; | |
| // Make sure it stays dirty | |
| dirtyState_.markDirty(false); | |
| } | |
| }, | |
| true | |
| ); | |
| } | |
| else if (response.isModified()) | |
| { | |
| // If we're in a collaborative session, we need to let it | |
| // reconcile the modification | |
| if (docDisplay_ != null && | |
| docDisplay_.hasActiveCollabSession() && | |
| response.getItem() != null) | |
| { | |
| events_.fireEvent(new CollabExternalEditEvent( | |
| getId(), getPath(), | |
| response.getItem().getLastModifiedNative())); | |
| return; | |
| } | |
| ignoreDeletes_ = false; // Now we know it exists | |
| // Use StringUtil.formatDate(response.getLastModified())? | |
| if (!dirtyState_.getValue()) | |
| { | |
| docUpdateSentinel_.revert(); | |
| } | |
| else | |
| { | |
| externalEditCheckInterval_.reset(); | |
| isWaitingForUserResponseToExternalEdit_ = true; | |
| globalDisplay_.showYesNoMessage( | |
| GlobalDisplay.MSG_WARNING, | |
| "File Changed", | |
| "The file " + name_.getValue() + " has changed " + | |
| "on disk. Do you want to reload the file from " + | |
| "disk and discard your unsaved changes?", | |
| false, | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| isWaitingForUserResponseToExternalEdit_ = false; | |
| docUpdateSentinel_.revert(); | |
| } | |
| }, | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| isWaitingForUserResponseToExternalEdit_ = false; | |
| externalEditCheckInterval_.reset(); | |
| docUpdateSentinel_.ignoreExternalEdit(); | |
| // Make sure it stays dirty | |
| dirtyState_.markDirty(false); | |
| } | |
| }, | |
| true | |
| ); | |
| } | |
| } | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| Debug.logError(error); | |
| } | |
| }); | |
| } | |
| private SourcePosition toSourcePosition(Scope func) | |
| { | |
| Position pos = func.getPreamble(); | |
| return SourcePosition.create(pos.getRow(), pos.getColumn()); | |
| } | |
| private boolean isCursorInTexMode() | |
| { | |
| if (fileType_.canCompilePDF()) | |
| { | |
| if (fileType_.isRnw()) | |
| { | |
| return SweaveFileType.TEX_LANG_MODE.equals( | |
| docDisplay_.getLanguageMode(docDisplay_.getCursorPosition())); | |
| } | |
| else | |
| { | |
| return true; | |
| } | |
| } | |
| else | |
| { | |
| return false; | |
| } | |
| } | |
| private boolean isCursorInRMode() | |
| { | |
| String mode = docDisplay_.getLanguageMode(docDisplay_.getCursorPosition()); | |
| if (mode == null) | |
| return true; | |
| if (mode.equals(TextFileType.R_LANG_MODE)) | |
| return true; | |
| return false; | |
| } | |
| private boolean isCursorInYamlMode() | |
| { | |
| String mode = docDisplay_.getLanguageMode(docDisplay_.getCursorPosition()); | |
| if (mode == null) | |
| return false; | |
| if (mode.equals("YAML")) | |
| return true; | |
| return false; | |
| } | |
| private boolean isNewDoc() | |
| { | |
| return docUpdateSentinel_.getPath() == null; | |
| } | |
| private static boolean shouldEnforceHardTabs(FileSystemItem item) | |
| { | |
| if (item == null) | |
| return false; | |
| String[] requiresHardTabs = new String[] { | |
| "Makefile", "Makefile.in", "Makefile.win", | |
| "Makevars", "Makevars.in", "Makevars.win" | |
| }; | |
| for (String file : requiresHardTabs) | |
| if (file.equals(item.getName())) | |
| return true; | |
| if (".tsv".equals(item.getExtension())) | |
| return true; | |
| return false; | |
| } | |
| private CppCompletionContext cppCompletionContext_ = | |
| new CppCompletionContext() { | |
| @Override | |
| public boolean isCompletionEnabled() | |
| { | |
| return session_.getSessionInfo().getClangAvailable() && | |
| (docUpdateSentinel_.getPath() != null) && | |
| fileType_.isC(); | |
| } | |
| @Override | |
| public void withUpdatedDoc(final CommandWith2Args<String, String> onUpdated) | |
| { | |
| docUpdateSentinel_.withSavedDoc(new Command() { | |
| @Override | |
| public void execute() | |
| { | |
| onUpdated.execute(docUpdateSentinel_.getPath(), | |
| docUpdateSentinel_.getId()); | |
| } | |
| }); | |
| } | |
| @Override | |
| public void cppCompletionOperation(final CppCompletionOperation operation) | |
| { | |
| if (isCompletionEnabled()) | |
| { | |
| withUpdatedDoc(new CommandWith2Args<String, String>() { | |
| @Override | |
| public void execute(String docPath, String docId) | |
| { | |
| Position pos = docDisplay_.getSelectionStart(); | |
| operation.execute(docPath, | |
| pos.getRow() + 1, | |
| pos.getColumn() + 1); | |
| } | |
| }); | |
| } | |
| } | |
| @Override | |
| public String getDocPath() | |
| { | |
| if (docUpdateSentinel_ == null) | |
| return ""; | |
| return docUpdateSentinel_.getPath(); | |
| } | |
| }; | |
| private CompletionContext rContext_ = new CompletionContext() { | |
| @Override | |
| public String getPath() | |
| { | |
| if (docUpdateSentinel_ == null) | |
| return null; | |
| else | |
| return docUpdateSentinel_.getPath(); | |
| } | |
| @Override | |
| public String getId() | |
| { | |
| if (docUpdateSentinel_ == null) | |
| return null; | |
| else | |
| return docUpdateSentinel_.getId(); | |
| } | |
| }; | |
| // these methods are public static so that other editing targets which | |
| // display source code (but don't inherit from TextEditingTarget) can share | |
| // their implementation | |
| public static interface PrefsContext | |
| { | |
| FileSystemItem getActiveFile(); | |
| } | |
| public static void registerPrefs( | |
| ArrayList<HandlerRegistration> releaseOnDismiss, | |
| UIPrefs prefs, | |
| ProjectConfig projectConfig, | |
| DocDisplay docDisplay, | |
| final SourceDocument sourceDoc) | |
| { | |
| registerPrefs(releaseOnDismiss, | |
| prefs, | |
| projectConfig, | |
| docDisplay, | |
| new PrefsContext() { | |
| @Override | |
| public FileSystemItem getActiveFile() | |
| { | |
| String path = sourceDoc.getPath(); | |
| if (path != null) | |
| return FileSystemItem.createFile(path); | |
| else | |
| return null; | |
| } | |
| }); | |
| } | |
| public static void registerPrefs( | |
| ArrayList<HandlerRegistration> releaseOnDismiss, | |
| UIPrefs prefs, | |
| final ProjectConfig projectConfig, | |
| final DocDisplay docDisplay, | |
| final PrefsContext context) | |
| { | |
| releaseOnDismiss.add(prefs.highlightSelectedLine().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setHighlightSelectedLine(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.highlightSelectedWord().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setHighlightSelectedWord(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.showLineNumbers().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setShowLineNumbers(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.useSpacesForTab().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| if (shouldEnforceHardTabs(context.getActiveFile())) | |
| { | |
| docDisplay.setUseSoftTabs(false); | |
| } | |
| else | |
| { | |
| if (projectConfig == null) | |
| docDisplay.setUseSoftTabs(arg); | |
| } | |
| }})); | |
| releaseOnDismiss.add(prefs.numSpacesForTab().bind( | |
| new CommandWithArg<Integer>() { | |
| public void execute(Integer arg) { | |
| if (projectConfig == null) | |
| docDisplay.setTabSize(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.autoDetectIndentation().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| if (projectConfig == null) | |
| docDisplay.autoDetectIndentation(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.showMargin().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setShowPrintMargin(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.blinkingCursor().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setBlinkingCursor(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.printMarginColumn().bind( | |
| new CommandWithArg<Integer>() { | |
| public void execute(Integer arg) { | |
| docDisplay.setPrintMarginColumn(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.showInvisibles().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setShowInvisibles(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.showIndentGuides().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setShowIndentGuides(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.scrollPastEndOfDocument().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setScrollPastEndOfDocument(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.highlightRFunctionCalls().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setHighlightRFunctionCalls(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.useVimMode().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setUseVimMode(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.enableEmacsKeybindings().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setUseEmacsKeybindings(arg); | |
| }})); | |
| releaseOnDismiss.add(prefs.codeCompleteOther().bind( | |
| new CommandWithArg<String>() { | |
| public void execute(String arg) { | |
| docDisplay.syncCompletionPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.alwaysCompleteCharacters().bind( | |
| new CommandWithArg<Integer>() { | |
| public void execute(Integer arg) { | |
| docDisplay.syncCompletionPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.alwaysCompleteDelayMs().bind( | |
| new CommandWithArg<Integer>() { | |
| public void execute(Integer arg) { | |
| docDisplay.syncCompletionPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.enableSnippets().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.syncCompletionPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.showDiagnosticsOther().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.syncDiagnosticsPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.diagnosticsOnSave().bind( | |
| new CommandWithArg<Boolean>() { | |
| @Override | |
| public void execute(Boolean arg) | |
| { | |
| docDisplay.syncDiagnosticsPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.backgroundDiagnosticsDelayMs().bind( | |
| new CommandWithArg<Integer>() { | |
| public void execute(Integer arg) { | |
| docDisplay.syncDiagnosticsPrefs(); | |
| }})); | |
| releaseOnDismiss.add(prefs.showInlineToolbarForRCodeChunks().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.forceImmediateRender(); | |
| }})); | |
| releaseOnDismiss.add(prefs.foldStyle().bind( | |
| new CommandWithArg<String>() { | |
| public void execute(String style) | |
| { | |
| docDisplay.setFoldStyle(style); | |
| }})); | |
| releaseOnDismiss.add(prefs.surroundSelection().bind( | |
| new CommandWithArg<String>() { | |
| public void execute(String string) | |
| { | |
| docDisplay.setSurroundSelectionPref(string); | |
| }})); | |
| releaseOnDismiss.add(prefs.enableTextDrag().bind( | |
| new CommandWithArg<Boolean>() { | |
| public void execute(Boolean arg) { | |
| docDisplay.setDragEnabled(arg); | |
| }})); | |
| } | |
| public static void syncFontSize( | |
| ArrayList<HandlerRegistration> releaseOnDismiss, | |
| EventBus events, | |
| final TextDisplay view, | |
| FontSizeManager fontSizeManager) | |
| { | |
| releaseOnDismiss.add(events.addHandler( | |
| ChangeFontSizeEvent.TYPE, | |
| new ChangeFontSizeHandler() | |
| { | |
| public void onChangeFontSize(ChangeFontSizeEvent event) | |
| { | |
| view.setFontSize(event.getFontSize()); | |
| } | |
| })); | |
| view.setFontSize(fontSizeManager.getSize()); | |
| } | |
| public static void onPrintSourceDoc(final DocDisplay docDisplay) | |
| { | |
| Scheduler.get().scheduleDeferred(new ScheduledCommand() | |
| { | |
| public void execute() | |
| { | |
| docDisplay.print(); | |
| } | |
| }); | |
| } | |
| public static void addRecordNavigationPositionHandler( | |
| ArrayList<HandlerRegistration> releaseOnDismiss, | |
| final DocDisplay docDisplay, | |
| final EventBus events, | |
| final EditingTarget target) | |
| { | |
| releaseOnDismiss.add(docDisplay.addRecordNavigationPositionHandler( | |
| new RecordNavigationPositionHandler() { | |
| @Override | |
| public void onRecordNavigationPosition( | |
| RecordNavigationPositionEvent event) | |
| { | |
| SourcePosition pos = SourcePosition.create( | |
| target.getContext(), | |
| event.getPosition().getRow(), | |
| event.getPosition().getColumn(), | |
| docDisplay.getScrollTop()); | |
| events.fireEvent(new SourceNavigationEvent( | |
| SourceNavigation.create( | |
| target.getId(), | |
| target.getPath(), | |
| pos))); | |
| } | |
| })); | |
| } | |
| public Position screenCoordinatesToDocumentPosition(int pageX, int pageY) | |
| { | |
| return docDisplay_.screenCoordinatesToDocumentPosition(pageX, pageY); | |
| } | |
| public DocDisplay getDocDisplay() | |
| { | |
| return docDisplay_; | |
| } | |
| private void addAdditionalResourceFiles(ArrayList<String> additionalFiles) | |
| { | |
| // it does--get the YAML front matter and modify it to include | |
| // the additional files named in the deployment | |
| String yaml = getRmdFrontMatter(); | |
| if (yaml == null) | |
| return; | |
| rmarkdownHelper_.addAdditionalResourceFiles(yaml, | |
| additionalFiles, | |
| new CommandWithArg<String>() | |
| { | |
| @Override | |
| public void execute(String yamlOut) | |
| { | |
| if (yamlOut != null) | |
| { | |
| applyRmdFrontMatter(yamlOut); | |
| } | |
| } | |
| }); | |
| } | |
| private void syncPublishPath(String path) | |
| { | |
| // if we have a view, a type, and a path, sync the view's content publish | |
| // path to the new content path--note that we need to do this even if the | |
| // document isn't currently of a publishable type, since it may become | |
| // publishable once saved. | |
| if (view_ != null && path != null) | |
| { | |
| view_.setPublishPath(extendedType_, path); | |
| } | |
| } | |
| public void setPreferredOutlineWidgetSize(double size) | |
| { | |
| prefs_.preferredDocumentOutlineWidth().setGlobalValue((int) size); | |
| prefs_.writeUIPrefs(); | |
| docUpdateSentinel_.setProperty(DOC_OUTLINE_SIZE, size + ""); | |
| } | |
| public double getPreferredOutlineWidgetSize() | |
| { | |
| String property = docUpdateSentinel_.getProperty(DOC_OUTLINE_SIZE); | |
| if (StringUtil.isNullOrEmpty(property)) | |
| return prefs_.preferredDocumentOutlineWidth().getGlobalValue(); | |
| try { | |
| double value = Double.parseDouble(property); | |
| // Don't allow too-small widget sizes. This helps to protect against | |
| // a user who might drag the outline width to just a few pixels, and | |
| // then toggle its visibility by clicking on the 'toggle outline' | |
| // button. It's unlikely that, realistically, any user would desire an | |
| // outline width less than ~30 pixels; at minimum we just need to | |
| // ensure they will be able to see + drag the widget to a larger | |
| // size if desired. | |
| if (value < 30) | |
| return 30; | |
| return value; | |
| } catch (Exception e) { | |
| return prefs_.preferredDocumentOutlineWidth().getGlobalValue(); | |
| } | |
| } | |
| public void setPreferredOutlineWidgetVisibility(boolean visible) | |
| { | |
| docUpdateSentinel_.setProperty(DOC_OUTLINE_VISIBLE, visible ? "1" : "0"); | |
| } | |
| public boolean getPreferredOutlineWidgetVisibility() | |
| { | |
| String property = docUpdateSentinel_.getProperty(DOC_OUTLINE_VISIBLE); | |
| return StringUtil.isNullOrEmpty(property) | |
| ? (getTextFileType().isRmd() && prefs_.showDocumentOutlineRmd().getGlobalValue()) | |
| : Integer.parseInt(property) > 0; | |
| } | |
| public boolean isActiveDocument() | |
| { | |
| return commandHandlerReg_ != null; | |
| } | |
| public StatusBar getStatusBar() | |
| { | |
| return statusBar_; | |
| } | |
| public TextEditingTargetNotebook getNotebook() | |
| { | |
| return notebook_; | |
| } | |
| /** | |
| * Updates the path of the file loaded in the editor, as though the user | |
| * had just saved the file at the new paht. | |
| * | |
| * @param path New path for the editor | |
| */ | |
| public void setPath(FileSystemItem path) | |
| { | |
| // Find the new type | |
| TextFileType type = fileTypeRegistry_.getTextTypeForFile(path); | |
| // Simulate a completed save of the new path | |
| new SaveProgressIndicator(path, type, null).onCompleted(); | |
| } | |
| private void setRMarkdownBehaviorEnabled(boolean enabled) | |
| { | |
| // register idle monitor; automatically creates/refreshes previews | |
| // of images and LaTeX equations during idle | |
| if (bgIdleMonitor_ == null && enabled) | |
| bgIdleMonitor_ = new TextEditingTargetIdleMonitor(this, | |
| docUpdateSentinel_); | |
| else if (bgIdleMonitor_ != null) | |
| { | |
| if (enabled) | |
| bgIdleMonitor_.beginMonitoring(); | |
| else | |
| bgIdleMonitor_.endMonitoring(); | |
| } | |
| // set up mathjax | |
| if (mathjax_ == null && enabled) | |
| mathjax_ = new MathJax(docDisplay_, docUpdateSentinel_, prefs_); | |
| if (enabled) | |
| { | |
| // auto preview images and equations | |
| if (inlinePreviewer_ == null) | |
| inlinePreviewer_ = new InlinePreviewer( | |
| this, docUpdateSentinel_, prefs_); | |
| inlinePreviewer_.preview(); | |
| // sync the notebook's output mode (enable/disable inline output) | |
| if (notebook_ != null) | |
| notebook_.syncOutputMode(); | |
| } | |
| else | |
| { | |
| // clean up previewers | |
| if (inlinePreviewer_ != null) | |
| inlinePreviewer_.onDismiss(); | |
| // clean up line widgets | |
| if (notebook_ != null) | |
| notebook_.onNotebookClearAllOutput(); | |
| docDisplay_.removeAllLineWidgets(); | |
| } | |
| } | |
| public void setIntendedAsReadOnly(List<String> alternatives) | |
| { | |
| view_.showReadOnlyWarning(alternatives); | |
| } | |
| void installShinyTestDependencies(final Command success) { | |
| server_.installShinyTestDependencies(new ServerRequestCallback<ConsoleProcess>() { | |
| @Override | |
| public void onResponseReceived(ConsoleProcess process) | |
| { | |
| final ConsoleProgressDialog dialog = new ConsoleProgressDialog(process, server_); | |
| dialog.showModal(); | |
| process.addProcessExitHandler(new ProcessExitEvent.Handler() | |
| { | |
| @Override | |
| public void onProcessExit(ProcessExitEvent event) | |
| { | |
| if (event.getExitCode() == 0) | |
| { | |
| success.execute(); | |
| dialog.closeDialog(); | |
| } | |
| } | |
| }); | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| Debug.logError(error); | |
| globalDisplay_.showErrorMessage("Failed to install additional dependencies", error.getUserMessage()); | |
| } | |
| }); | |
| } | |
| void checkTestPackageDependencies(final Command success, boolean isTestThat) { | |
| dependencyManager_.withTestPackage( | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| if (isTestThat) | |
| success.execute(); | |
| else { | |
| server_.hasShinyTestDependenciesInstalled(new ServerRequestCallback<Boolean>() { | |
| @Override | |
| public void onResponseReceived(Boolean hasPackageDependencies) | |
| { | |
| if (hasPackageDependencies) | |
| success.execute(); | |
| else { | |
| globalDisplay_.showYesNoMessage( | |
| GlobalDisplay.MSG_WARNING, | |
| "Install Shinytest Dependencies", | |
| "The package shinytest requires additional components to run.\n\n" + | |
| "Install additional components?", | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| installShinyTestDependencies(success); | |
| } | |
| }, | |
| false); | |
| } | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| Debug.logError(error); | |
| globalDisplay_.showErrorMessage("Failed to check for additional dependencies", error.getMessage()); | |
| } | |
| }); | |
| } | |
| } | |
| }, | |
| isTestThat | |
| ); | |
| } | |
| @Handler | |
| void onTestTestthatFile() | |
| { | |
| final String buildCommand = "test-file"; | |
| checkTestPackageDependencies( | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| save(new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| server_.startBuild(buildCommand, docUpdateSentinel_.getPath(), | |
| new SimpleRequestCallback<Boolean>() { | |
| @Override | |
| public void onResponseReceived(Boolean response) | |
| { | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| super.onError(error); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| }, | |
| true | |
| ); | |
| } | |
| @Handler | |
| void onTestShinytestFile() | |
| { | |
| final String buildCommand = "test-shiny-file"; | |
| checkTestPackageDependencies( | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| save(new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| server_.startBuild(buildCommand, docUpdateSentinel_.getPath(), | |
| new SimpleRequestCallback<Boolean>() { | |
| @Override | |
| public void onResponseReceived(Boolean response) | |
| { | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| super.onError(error); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| }, | |
| false | |
| ); | |
| } | |
| @Handler | |
| void onShinyRecordTest() | |
| { | |
| checkTestPackageDependencies( | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| String shinyAppPath = FilePathUtils.dirFromFile(docUpdateSentinel_.getPath()); | |
| if (fileType_.canKnitToHTML()) | |
| { | |
| shinyAppPath = docUpdateSentinel_.getPath(); | |
| } | |
| String code = "shinytest::recordTest(\"" + shinyAppPath.replace("\"", "\\\"") + "\")"; | |
| events_.fireEvent(new SendToConsoleEvent(code, true)); | |
| } | |
| }, | |
| false | |
| ); | |
| } | |
| @Handler | |
| void onShinyRunAllTests() | |
| { | |
| checkTestPackageDependencies( | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| server_.startBuild("test-shiny", FilePathUtils.dirFromFile(docUpdateSentinel_.getPath()), | |
| new SimpleRequestCallback<Boolean>() { | |
| @Override | |
| public void onResponseReceived(Boolean response) | |
| { | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| super.onError(error); | |
| } | |
| }); | |
| } | |
| }, | |
| false | |
| ); | |
| } | |
| @Handler | |
| void onShinyCompareTest() | |
| { | |
| final String appDir = FilePathUtils.parent(FilePathUtils.dirFromFile(docUpdateSentinel_.getPath())); | |
| final String testName = FilePathUtils.fileNameSansExtension(docUpdateSentinel_.getPath()); | |
| server_.hasShinyTestResults(appDir, testName, new ServerRequestCallback<Boolean>() { | |
| @Override | |
| public void onResponseReceived(Boolean hasResults) | |
| { | |
| if (!hasResults) { | |
| globalDisplay_.showMessage( | |
| GlobalDisplay.MSG_INFO, | |
| "No Failed Results", | |
| "There are no failed tests to compare." | |
| ); | |
| } | |
| else { | |
| checkTestPackageDependencies( | |
| new Command() | |
| { | |
| @Override | |
| public void execute() | |
| { | |
| String code = "shinytest::viewTestDiff(\"" + appDir + "\", \"" + testName + "\")"; | |
| events_.fireEvent(new SendToConsoleEvent(code, true)); | |
| } | |
| }, | |
| false | |
| ); | |
| } | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| Debug.logError(error); | |
| globalDisplay_.showErrorMessage("Failed to check if results are available", error.getUserMessage()); | |
| } | |
| }); | |
| } | |
| private StatusBar statusBar_; | |
| private final DocDisplay docDisplay_; | |
| private final UIPrefs prefs_; | |
| private Display view_; | |
| private final Commands commands_; | |
| private SourceServerOperations server_; | |
| private EventBus events_; | |
| private final GlobalDisplay globalDisplay_; | |
| private final FileDialogs fileDialogs_; | |
| private final FileTypeRegistry fileTypeRegistry_; | |
| private final FileTypeCommands fileTypeCommands_; | |
| private final ConsoleDispatcher consoleDispatcher_; | |
| private final WorkbenchContext workbenchContext_; | |
| private final Session session_; | |
| private final Synctex synctex_; | |
| private final FontSizeManager fontSizeManager_; | |
| private final SourceBuildHelper sourceBuildHelper_; | |
| private final DependencyManager dependencyManager_; | |
| private DocUpdateSentinel docUpdateSentinel_; | |
| private Value<String> name_ = new Value<String>(null); | |
| private TextFileType fileType_; | |
| private String id_; | |
| private HandlerRegistration commandHandlerReg_; | |
| private ArrayList<HandlerRegistration> releaseOnDismiss_ = | |
| new ArrayList<HandlerRegistration>(); | |
| private final DirtyState dirtyState_; | |
| private HandlerManager handlers_ = new HandlerManager(this); | |
| private FileSystemContext fileContext_; | |
| private final TextEditingTargetCompilePdfHelper compilePdfHelper_; | |
| private final TextEditingTargetRMarkdownHelper rmarkdownHelper_; | |
| private final TextEditingTargetCppHelper cppHelper_; | |
| private final TextEditingTargetJSHelper jsHelper_; | |
| private final TextEditingTargetSqlHelper sqlHelper_; | |
| private final TextEditingTargetPresentationHelper presentationHelper_; | |
| private final TextEditingTargetReformatHelper reformatHelper_; | |
| private final TextEditingTargetRHelper rHelper_; | |
| private TextEditingTargetIdleMonitor bgIdleMonitor_; | |
| private TextEditingTargetThemeHelper themeHelper_; | |
| private RoxygenHelper roxygenHelper_; | |
| private boolean ignoreDeletes_; | |
| private boolean forceSaveCommandActive_ = false; | |
| private final TextEditingTargetScopeHelper scopeHelper_; | |
| private TextEditingTargetPackageDependencyHelper packageDependencyHelper_; | |
| private TextEditingTargetSpelling spelling_; | |
| private TextEditingTargetNotebook notebook_; | |
| private TextEditingTargetChunks chunks_; | |
| private BreakpointManager breakpointManager_; | |
| private final LintManager lintManager_; | |
| private final TextEditingTargetRenameHelper renameHelper_; | |
| private CollabEditStartParams queuedCollabParams_; | |
| private MathJax mathjax_; | |
| private InlinePreviewer inlinePreviewer_; | |
| private ProjectConfig projConfig_; | |
| // Allows external edit checks to supercede one another | |
| private final Invalidation externalEditCheckInvalidation_ = | |
| new Invalidation(); | |
| // Prevents external edit checks from happening too soon after each other | |
| private final IntervalTracker externalEditCheckInterval_ = | |
| new IntervalTracker(1000, true); | |
| private boolean isWaitingForUserResponseToExternalEdit_ = false; | |
| private EditingTargetCodeExecution codeExecution_; | |
| private SourcePosition debugStartPos_ = null; | |
| private SourcePosition debugEndPos_ = null; | |
| private boolean isDebugWarningVisible_ = false; | |
| private boolean isBreakpointWarningVisible_ = false; | |
| private String extendedType_; | |
| private abstract class RefactorServerRequestCallback | |
| extends ServerRequestCallback<JsArrayString> | |
| { | |
| private final String refactoringName_; | |
| public RefactorServerRequestCallback(String refactoringName) | |
| { | |
| refactoringName_ = refactoringName; | |
| } | |
| @Override | |
| public void onResponseReceived(final JsArrayString response) | |
| { | |
| doExtract(response); | |
| } | |
| @Override | |
| public void onError(ServerError error) | |
| { | |
| globalDisplay_.showYesNoMessage( | |
| GlobalDisplay.MSG_WARNING, | |
| refactoringName_, | |
| "The selected code could not be " + | |
| "parsed.\n\n" + | |
| "Are you sure you want to continue?", | |
| new Operation() | |
| { | |
| public void execute() | |
| { | |
| doExtract(null); | |
| } | |
| }, | |
| false); | |
| } | |
| abstract void doExtract(final JsArrayString response); | |
| } | |
| private static final String PROPERTY_CURSOR_POSITION = "cursorPosition"; | |
| private static final String PROPERTY_SCROLL_LINE = "scrollLine"; | |
| } |