/* * Source.java * * Copyright (C) 2009-17 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; 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.ScheduledCommand; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.logical.shared.HasBeforeSelectionHandlers; import com.google.gwt.event.logical.shared.HasSelectionHandlers; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.json.client.JSONString; import com.google.gwt.json.client.JSONValue; import com.google.gwt.resources.client.ImageResource; 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.Event.NativePreviewHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.IsWidget; 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.Handler; import org.rstudio.core.client.command.KeyboardShortcut; import org.rstudio.core.client.command.ShortcutManager; import org.rstudio.core.client.events.*; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.js.JsObject; import org.rstudio.core.client.widget.Operation; import org.rstudio.core.client.widget.OperationWithInput; import org.rstudio.core.client.widget.ProgressIndicator; import org.rstudio.core.client.widget.ProgressOperationWithInput; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.application.ApplicationAction; import org.rstudio.studio.client.application.ApplicationUtils; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.events.CrossWindowEvent; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.common.FileDialogs; import org.rstudio.studio.client.common.FilePathUtils; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.GlobalProgressDelayer; import org.rstudio.studio.client.common.SimpleRequestCallback; import org.rstudio.studio.client.common.dependencies.DependencyManager; import org.rstudio.studio.client.common.filetypes.EditableFileType; import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; import org.rstudio.studio.client.common.filetypes.TextFileType; import org.rstudio.studio.client.common.filetypes.events.OpenPresentationSourceFileEvent; import org.rstudio.studio.client.common.filetypes.events.OpenSourceFileEvent; import org.rstudio.studio.client.common.filetypes.events.OpenSourceFileHandler; import org.rstudio.studio.client.common.filetypes.model.NavigationMethods; import org.rstudio.studio.client.common.rnw.RnwWeave; import org.rstudio.studio.client.common.rnw.RnwWeaveRegistry; import org.rstudio.studio.client.common.satellite.Satellite; import org.rstudio.studio.client.common.synctex.Synctex; import org.rstudio.studio.client.common.synctex.events.SynctexStatusChangedEvent; import org.rstudio.studio.client.events.GetEditorContextEvent; import org.rstudio.studio.client.events.GetEditorContextEvent.DocumentSelection; import org.rstudio.studio.client.events.ReplaceRangesEvent; import org.rstudio.studio.client.events.ReplaceRangesEvent.ReplacementData; import org.rstudio.studio.client.events.SetSelectionRangesEvent; import org.rstudio.studio.client.rmarkdown.model.RmdChosenTemplate; import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatter; import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat; import org.rstudio.studio.client.rmarkdown.model.RmdTemplateData; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.server.VoidServerRequestCallback; import org.rstudio.studio.client.workbench.ConsoleEditorProvider; import org.rstudio.studio.client.workbench.MainWindowObject; import org.rstudio.studio.client.workbench.FileMRUList; import org.rstudio.studio.client.workbench.WorkbenchContext; import org.rstudio.studio.client.workbench.codesearch.model.SearchPathFunctionDefinition; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.events.ZoomPaneEvent; import org.rstudio.studio.client.workbench.model.ClientState; import org.rstudio.studio.client.workbench.model.RemoteFileSystemContext; import org.rstudio.studio.client.workbench.model.Session; import org.rstudio.studio.client.workbench.model.SessionInfo; import org.rstudio.studio.client.workbench.model.SessionUtils; import org.rstudio.studio.client.workbench.model.UnsavedChangesItem; import org.rstudio.studio.client.workbench.model.UnsavedChangesTarget; import org.rstudio.studio.client.workbench.model.helper.IntStateValue; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.snippets.SnippetHelper; import org.rstudio.studio.client.workbench.snippets.model.SnippetsChangedEvent; import org.rstudio.studio.client.workbench.ui.unsaved.UnsavedChangesDialog; import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorDisplay; import org.rstudio.studio.client.workbench.views.data.events.ViewDataEvent; import org.rstudio.studio.client.workbench.views.data.events.ViewDataHandler; import org.rstudio.studio.client.workbench.views.environment.events.DebugModeChangedEvent; import org.rstudio.studio.client.workbench.views.output.find.events.FindInFilesEvent; import org.rstudio.studio.client.workbench.views.source.NewShinyWebApplication.Result; import org.rstudio.studio.client.workbench.views.source.SourceWindowManager.NavigationResult; import org.rstudio.studio.client.workbench.views.source.editors.EditingTarget; import org.rstudio.studio.client.workbench.views.source.editors.EditingTargetSource; import org.rstudio.studio.client.workbench.views.source.editors.codebrowser.CodeBrowserEditingTarget; import org.rstudio.studio.client.workbench.views.source.editors.data.DataEditingTarget; import org.rstudio.studio.client.workbench.views.source.editors.explorer.ObjectExplorerEditingTarget; import org.rstudio.studio.client.workbench.views.source.editors.explorer.events.OpenObjectExplorerEvent; import org.rstudio.studio.client.workbench.views.source.editors.explorer.model.ObjectExplorerHandle; import org.rstudio.studio.client.workbench.views.source.editors.profiler.OpenProfileEvent; import org.rstudio.studio.client.workbench.views.source.editors.profiler.model.ProfilerContents; import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor; import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay; import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTarget; import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTargetPresentationHelper; import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTargetRMarkdownHelper; import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceEditorNative; 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.Selection; import org.rstudio.studio.client.workbench.views.source.editors.text.events.FileTypeChangedEvent; import org.rstudio.studio.client.workbench.views.source.editors.text.events.FileTypeChangedHandler; import org.rstudio.studio.client.workbench.views.source.editors.text.events.NewWorkingCopyEvent; import org.rstudio.studio.client.workbench.views.source.editors.text.events.SourceOnSaveChangedEvent; import org.rstudio.studio.client.workbench.views.source.editors.text.events.SourceOnSaveChangedHandler; import org.rstudio.studio.client.workbench.views.source.editors.text.ui.NewRMarkdownDialog; import org.rstudio.studio.client.workbench.views.source.editors.text.ui.NewRdDialog; import org.rstudio.studio.client.workbench.views.source.events.*; import org.rstudio.studio.client.workbench.views.source.model.ContentItem; import org.rstudio.studio.client.workbench.views.source.model.DataItem; import org.rstudio.studio.client.workbench.views.source.model.DocTabDragParams; import org.rstudio.studio.client.workbench.views.source.model.RdShellResult; import org.rstudio.studio.client.workbench.views.source.model.SourceDocument; import org.rstudio.studio.client.workbench.views.source.model.SourceDocumentResult; import org.rstudio.studio.client.workbench.views.source.model.SourceNavigation; import org.rstudio.studio.client.workbench.views.source.model.SourceNavigationHistory; import org.rstudio.studio.client.workbench.views.source.model.SourcePosition; import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.Queue; public class Source implements InsertSourceHandler, IsWidget, OpenSourceFileHandler, TabClosingHandler, TabCloseHandler, TabReorderHandler, SelectionHandler<Integer>, TabClosedHandler, FileEditHandler, ShowContentHandler, ShowDataHandler, CodeBrowserNavigationHandler, CodeBrowserFinishedHandler, CodeBrowserHighlightEvent.Handler, SourceExtendedTypeDetectedEvent.Handler, BeforeShowHandler, SnippetsChangedEvent.Handler, PopoutDocEvent.Handler, DocWindowChangedEvent.Handler, DocTabDragInitiatedEvent.Handler, PopoutDocInitiatedEvent.Handler, DebugModeChangedEvent.Handler, OpenProfileEvent.Handler, OpenObjectExplorerEvent.Handler, ReplaceRangesEvent.Handler, SetSelectionRangesEvent.Handler, GetEditorContextEvent.Handler { public interface Display extends IsWidget, HasTabClosingHandlers, HasTabCloseHandlers, HasTabClosedHandlers, HasTabReorderHandlers, HasBeforeSelectionHandlers<Integer>, HasSelectionHandlers<Integer> { void addTab(Widget widget, ImageResource icon, String docId, String name, String tooltip, Integer position, boolean switchToTab); void selectTab(int tabIndex); void selectTab(Widget widget); int getTabCount(); int getActiveTabIndex(); void closeTab(Widget widget, boolean interactive); void closeTab(Widget widget, boolean interactive, Command onClosed); void closeTab(int index, boolean interactive); void closeTab(int index, boolean interactive, Command onClosed); void moveTab(int index, int delta); void setDirty(Widget widget, boolean dirty); void manageChevronVisibility(); void showOverflowPopup(); void cancelTabDrag(); void showUnsavedChangesDialog( String title, ArrayList<UnsavedChangesTarget> dirtyTargets, OperationWithInput<UnsavedChangesDialog.Result> saveOperation, Command onCancelled); void ensureVisible(); void renameTab(Widget child, ImageResource icon, String value, String tooltip); HandlerRegistration addBeforeShowHandler(BeforeShowHandler handler); } public interface CPSEditingTargetCommand { void execute(EditingTarget editingTarget, Command continuation); } @Inject public Source(Commands commands, Display view, SourceServerOperations server, EditingTargetSource editingTargetSource, FileTypeRegistry fileTypeRegistry, GlobalDisplay globalDisplay, FileDialogs fileDialogs, RemoteFileSystemContext fileContext, EventBus events, final Session session, Synctex synctex, WorkbenchContext workbenchContext, Provider<FileMRUList> pMruList, UIPrefs uiPrefs, Satellite satellite, ConsoleEditorProvider consoleEditorProvider, RnwWeaveRegistry rnwWeaveRegistry, DependencyManager dependencyManager, SourceWindowManager windowManager) { commands_ = commands; view_ = view; server_ = server; editingTargetSource_ = editingTargetSource; fileTypeRegistry_ = fileTypeRegistry; globalDisplay_ = globalDisplay; fileDialogs_ = fileDialogs; fileContext_ = fileContext; rmarkdown_ = new TextEditingTargetRMarkdownHelper(); events_ = events; session_ = session; synctex_ = synctex; workbenchContext_ = workbenchContext; pMruList_ = pMruList; uiPrefs_ = uiPrefs; consoleEditorProvider_ = consoleEditorProvider; rnwWeaveRegistry_ = rnwWeaveRegistry; dependencyManager_ = dependencyManager; windowManager_ = windowManager; vimCommands_ = new SourceVimCommands(); view_.addTabClosingHandler(this); view_.addTabCloseHandler(this); view_.addTabClosedHandler(this); view_.addTabReorderHandler(this); view_.addSelectionHandler(this); view_.addBeforeShowHandler(this); dynamicCommands_ = new HashSet<AppCommand>(); dynamicCommands_.add(commands.saveSourceDoc()); dynamicCommands_.add(commands.reopenSourceDocWithEncoding()); dynamicCommands_.add(commands.saveSourceDocAs()); dynamicCommands_.add(commands.saveSourceDocWithEncoding()); dynamicCommands_.add(commands.printSourceDoc()); dynamicCommands_.add(commands.vcsFileLog()); dynamicCommands_.add(commands.vcsFileDiff()); dynamicCommands_.add(commands.vcsFileRevert()); dynamicCommands_.add(commands.executeCode()); dynamicCommands_.add(commands.executeCodeWithoutFocus()); dynamicCommands_.add(commands.executeAllCode()); dynamicCommands_.add(commands.executeToCurrentLine()); dynamicCommands_.add(commands.executeFromCurrentLine()); dynamicCommands_.add(commands.executeCurrentFunction()); dynamicCommands_.add(commands.executeCurrentSection()); dynamicCommands_.add(commands.executeLastCode()); dynamicCommands_.add(commands.insertChunk()); dynamicCommands_.add(commands.insertSection()); dynamicCommands_.add(commands.executeSetupChunk()); dynamicCommands_.add(commands.executePreviousChunks()); dynamicCommands_.add(commands.executeSubsequentChunks()); dynamicCommands_.add(commands.executeCurrentChunk()); dynamicCommands_.add(commands.executeNextChunk()); dynamicCommands_.add(commands.sourceActiveDocument()); dynamicCommands_.add(commands.sourceActiveDocumentWithEcho()); dynamicCommands_.add(commands.knitDocument()); dynamicCommands_.add(commands.previewHTML()); dynamicCommands_.add(commands.compilePDF()); dynamicCommands_.add(commands.compileNotebook()); dynamicCommands_.add(commands.synctexSearch()); dynamicCommands_.add(commands.popoutDoc()); dynamicCommands_.add(commands.returnDocToMain()); dynamicCommands_.add(commands.findReplace()); dynamicCommands_.add(commands.findNext()); dynamicCommands_.add(commands.findPrevious()); dynamicCommands_.add(commands.findFromSelection()); dynamicCommands_.add(commands.replaceAndFind()); dynamicCommands_.add(commands.extractFunction()); dynamicCommands_.add(commands.extractLocalVariable()); dynamicCommands_.add(commands.commentUncomment()); dynamicCommands_.add(commands.reindent()); dynamicCommands_.add(commands.reflowComment()); dynamicCommands_.add(commands.jumpTo()); dynamicCommands_.add(commands.jumpToMatching()); dynamicCommands_.add(commands.goToHelp()); dynamicCommands_.add(commands.goToFunctionDefinition()); dynamicCommands_.add(commands.setWorkingDirToActiveDoc()); dynamicCommands_.add(commands.debugDumpContents()); dynamicCommands_.add(commands.debugImportDump()); dynamicCommands_.add(commands.goToLine()); dynamicCommands_.add(commands.checkSpelling()); dynamicCommands_.add(commands.codeCompletion()); dynamicCommands_.add(commands.findUsages()); dynamicCommands_.add(commands.debugBreakpoint()); dynamicCommands_.add(commands.vcsViewOnGitHub()); dynamicCommands_.add(commands.vcsBlameOnGitHub()); dynamicCommands_.add(commands.editRmdFormatOptions()); dynamicCommands_.add(commands.reformatCode()); dynamicCommands_.add(commands.showDiagnosticsActiveDocument()); dynamicCommands_.add(commands.renameInScope()); dynamicCommands_.add(commands.insertRoxygenSkeleton()); dynamicCommands_.add(commands.expandSelection()); dynamicCommands_.add(commands.shrinkSelection()); dynamicCommands_.add(commands.toggleDocumentOutline()); dynamicCommands_.add(commands.knitWithParameters()); dynamicCommands_.add(commands.clearKnitrCache()); dynamicCommands_.add(commands.goToNextSection()); dynamicCommands_.add(commands.goToPrevSection()); dynamicCommands_.add(commands.goToNextChunk()); dynamicCommands_.add(commands.goToPrevChunk()); dynamicCommands_.add(commands.profileCode()); dynamicCommands_.add(commands.profileCodeWithoutFocus()); dynamicCommands_.add(commands.saveProfileAs()); dynamicCommands_.add(commands.restartRClearOutput()); dynamicCommands_.add(commands.restartRRunAllChunks()); dynamicCommands_.add(commands.notebookCollapseAllOutput()); dynamicCommands_.add(commands.notebookExpandAllOutput()); dynamicCommands_.add(commands.notebookClearOutput()); dynamicCommands_.add(commands.notebookClearAllOutput()); dynamicCommands_.add(commands.notebookToggleExpansion()); for (AppCommand command : dynamicCommands_) { command.setVisible(false); command.setEnabled(false); } // fake shortcuts for commands which we handle at a lower level commands.goToHelp().setShortcut(new KeyboardShortcut(112)); commands.goToFunctionDefinition().setShortcut(new KeyboardShortcut(113)); commands.codeCompletion().setShortcut( new KeyboardShortcut(KeyCodes.KEY_TAB)); // See bug 3673 and https://bugs.webkit.org/show_bug.cgi?id=41016 if (BrowseCap.isMacintosh()) { ShortcutManager.INSTANCE.register( KeyboardShortcut.META | KeyboardShortcut.ALT, 192, commands.executeNextChunk(), "Execute", commands.executeNextChunk().getMenuLabel(false), ""); } events.addHandler(ShowContentEvent.TYPE, this); events.addHandler(ShowDataEvent.TYPE, this); events.addHandler(OpenObjectExplorerEvent.TYPE, this); events.addHandler(ViewDataEvent.TYPE, new ViewDataHandler() { public void onViewData(ViewDataEvent event) { server_.newDocument( FileTypeRegistry.DATAFRAME.getTypeId(), null, JsObject.createJsObject(), new SimpleRequestCallback<SourceDocument>("Edit Data Frame") { public void onResponseReceived(SourceDocument response) { addTab(response, OPEN_INTERACTIVE); } }); } }); events.addHandler(CodeBrowserNavigationEvent.TYPE, this); events.addHandler(CodeBrowserFinishedEvent.TYPE, this); events.addHandler(CodeBrowserHighlightEvent.TYPE, this); events.addHandler(FileTypeChangedEvent.TYPE, new FileTypeChangedHandler() { public void onFileTypeChanged(FileTypeChangedEvent event) { manageCommands(); } }); events.addHandler(SourceOnSaveChangedEvent.TYPE, new SourceOnSaveChangedHandler() { @Override public void onSourceOnSaveChanged(SourceOnSaveChangedEvent event) { manageSaveCommands(); } }); events.addHandler(SwitchToDocEvent.TYPE, new SwitchToDocHandler() { public void onSwitchToDoc(SwitchToDocEvent event) { ensureVisible(false); setPhysicalTabIndex(event.getSelectedIndex()); // Fire an activation event just to ensure the activated // tab gets focus commands_.activateSource().execute(); } }); events.addHandler(SourceFileSavedEvent.TYPE, new SourceFileSavedHandler() { public void onSourceFileSaved(SourceFileSavedEvent event) { pMruList_.get().add(event.getPath()); } }); events.addHandler(SourcePathChangedEvent.TYPE, new SourcePathChangedEvent.Handler() { @Override public void onSourcePathChanged(final SourcePathChangedEvent event) { inEditorForPath(event.getFrom(), new OperationWithInput<EditingTarget>() { @Override public void execute(EditingTarget input) { FileSystemItem toPath = FileSystemItem.createFile(event.getTo()); if (input instanceof TextEditingTarget) { // for text files, notify the editing surface so it can // react to the new file type ((TextEditingTarget)input).setPath(toPath); } else { // for other files, just rename the tab input.getName().setValue(toPath.getName(), true); } events_.fireEvent(new SourceFileSavedEvent( input.getId(), event.getTo())); } }); } }); events.addHandler(SourceNavigationEvent.TYPE, new SourceNavigationHandler() { @Override public void onSourceNavigation(SourceNavigationEvent event) { if (!suspendSourceNavigationAdding_) { sourceNavigationHistory_.add(event.getNavigation()); } } }); events.addHandler(SourceExtendedTypeDetectedEvent.TYPE, this); sourceNavigationHistory_.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { manageSourceNavigationCommands(); } }); events.addHandler(SynctexStatusChangedEvent.TYPE, new SynctexStatusChangedEvent.Handler() { @Override public void onSynctexStatusChanged(SynctexStatusChangedEvent event) { manageSynctexCommands(); } }); events.addHandler(CollabEditStartedEvent.TYPE, new CollabEditStartedEvent.Handler() { @Override public void onCollabEditStarted(final CollabEditStartedEvent collab) { inEditorForPath(collab.getStartParams().getPath(), new OperationWithInput<EditingTarget>() { @Override public void execute(EditingTarget editor) { editor.beginCollabSession(collab.getStartParams()); } }); } }); events.addHandler(CollabEditEndedEvent.TYPE, new CollabEditEndedEvent.Handler() { @Override public void onCollabEditEnded(final CollabEditEndedEvent collab) { inEditorForPath(collab.getPath(), new OperationWithInput<EditingTarget>() { @Override public void execute(EditingTarget editor) { editor.endCollabSession(); } }); } }); events.addHandler(NewWorkingCopyEvent.TYPE, new NewWorkingCopyEvent.Handler() { @Override public void onNewWorkingCopy(NewWorkingCopyEvent event) { newDoc(event.getType(), event.getContents(), null); } }); events.addHandler(PopoutDocEvent.TYPE, this); events.addHandler(DocWindowChangedEvent.TYPE, this); events.addHandler(DocTabDragInitiatedEvent.TYPE, this); events.addHandler(PopoutDocInitiatedEvent.TYPE, this); events.addHandler(DebugModeChangedEvent.TYPE, this); events.addHandler(ReplaceRangesEvent.TYPE, this); events.addHandler(GetEditorContextEvent.TYPE, this); events.addHandler(SetSelectionRangesEvent.TYPE, this); events.addHandler(OpenProfileEvent.TYPE, this); // Suppress 'CTRL + ALT + SHIFT + click' to work around #2483 in Ace Event.addNativePreviewHandler(new NativePreviewHandler() { @Override public void onPreviewNativeEvent(NativePreviewEvent event) { int type = event.getTypeInt(); if (type == Event.ONMOUSEDOWN || type == Event.ONMOUSEUP) { int modifier = KeyboardShortcut.getModifierValue(event.getNativeEvent()); if (modifier == (KeyboardShortcut.ALT | KeyboardShortcut.CTRL | KeyboardShortcut.SHIFT)) { event.cancel(); return; } } } }); restoreDocuments(session); // get the key to use for active tab persistence; use ordinal-based key // for source windows rather than their ID to avoid unbounded accumulation String activeTabKey = KEY_ACTIVETAB; if (!SourceWindowManager.isMainSourceWindow()) activeTabKey += "SourceWindow" + windowManager_.getSourceWindowOrdinal(); new IntStateValue(MODULE_SOURCE, activeTabKey, ClientState.PROJECT_PERSISTENT, session.getSessionInfo().getClientState()) { @Override protected void onInit(Integer value) { if (value == null) return; if (value >= 0 && view_.getTabCount() > value) view_.selectTab(value); if (view_.getTabCount() > 0 && view_.getActiveTabIndex() >= 0) { editors_.get(view_.getActiveTabIndex()).onInitiallyLoaded(); } // clear the history manager sourceNavigationHistory_.clear(); } @Override protected Integer getValue() { return getPhysicalTabIndex(); } }; AceEditorNative.syncUiPrefs(uiPrefs_); // sync UI prefs with shortcut manager if (uiPrefs_.useVimMode().getGlobalValue()) ShortcutManager.INSTANCE.setEditorMode(KeyboardShortcut.MODE_VIM); else if (uiPrefs_.enableEmacsKeybindings().getGlobalValue()) ShortcutManager.INSTANCE.setEditorMode(KeyboardShortcut.MODE_EMACS); else ShortcutManager.INSTANCE.setEditorMode(KeyboardShortcut.MODE_DEFAULT); initialized_ = true; // As tabs were added before, manageCommands() was suppressed due to // initialized_ being false, so we need to run it explicitly manageCommands(); // Same with this event fireDocTabsChanged(); // open project or edit_published docs (only for main source window) if (SourceWindowManager.isMainSourceWindow()) { openProjectDocs(session); openEditPublishedDocs(); } // add vim commands initVimCommands(); } private boolean consoleEditorHadFocusLast() { String id = MainWindowObject.lastFocusedEditor().get(); return "rstudio_console_input".equals(id); } private void withTarget(String id, CommandWithArg<TextEditingTarget> command, Command onFailure) { EditingTarget target = StringUtil.isNullOrEmpty(id) ? activeEditor_ : getEditingTargetForId(id); if (target == null) { if (onFailure != null) onFailure.execute(); return; } if (!(target instanceof TextEditingTarget)) { if (onFailure != null) onFailure.execute(); return; } command.execute((TextEditingTarget) target); } private void getEditorContext(String id, String path, DocDisplay docDisplay) { AceEditor editor = (AceEditor) docDisplay; Selection selection = editor.getNativeSelection(); Range[] ranges = selection.getAllRanges(); JsArray<DocumentSelection> docSelections = JavaScriptObject.createArray().cast(); for (int i = 0; i < ranges.length; i++) { docSelections.push(DocumentSelection.create( ranges[i], editor.getTextForRange(ranges[i]))); } id = StringUtil.notNull(id); path = StringUtil.notNull(path); GetEditorContextEvent.SelectionData data = GetEditorContextEvent.SelectionData.create(id, path, editor.getCode(), docSelections); server_.getEditorContextCompleted(data, new VoidServerRequestCallback()); } private void withTarget(String id, CommandWithArg<TextEditingTarget> command) { withTarget(id, command, null); } private void initVimCommands() { vimCommands_.save(this); vimCommands_.selectTabIndex(this); vimCommands_.selectNextTab(this); vimCommands_.selectPreviousTab(this); vimCommands_.closeActiveTab(this); vimCommands_.closeAllTabs(this); vimCommands_.createNewDocument(this); vimCommands_.saveAndCloseActiveTab(this); vimCommands_.readFile(this, uiPrefs_.defaultEncoding().getValue()); vimCommands_.runRScript(this); vimCommands_.reflowText(this); vimCommands_.showVimHelp( RStudioGinjector.INSTANCE.getShortcutViewer()); vimCommands_.showHelpAtCursor(this); vimCommands_.reindent(this); vimCommands_.expandShrinkSelection(this); vimCommands_.addStarRegister(); } private void vimSetTabIndex(int index) { int tabCount = view_.getTabCount(); if (index >= tabCount) return; setPhysicalTabIndex(index); } private void closeAllTabs(boolean interactive) { if (interactive) { // call into the interactive tab closer onCloseAllSourceDocs(); } else { // revert unsaved targets and close tabs revertUnsavedTargets(new Command() { @Override public void execute() { // documents have been reverted; we can close cpsExecuteForEachEditor(editors_, new CPSEditingTargetCommand() { @Override public void execute(EditingTarget editingTarget, Command continuation) { view_.closeTab( editingTarget.asWidget(), false, continuation); } }); } }); } } private void saveActiveSourceDoc() { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget target = (TextEditingTarget) activeEditor_; target.save(); } } private void saveAndCloseActiveSourceDoc() { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget target = (TextEditingTarget) activeEditor_; target.save(new Command() { @Override public void execute() { onCloseSourceDoc(); } }); } } /** * @param isNewTabPending True if a new tab is about to be created. (If * false and there are no tabs already, then a new source doc might * be created to make sure we don't end up with a source pane showing * with no tabs in it.) */ private void ensureVisible(boolean isNewTabPending) { newTabPending_++; try { view_.ensureVisible(); } finally { newTabPending_--; } } public Widget asWidget() { return view_.asWidget(); } private void restoreDocuments(final Session session) { final JsArray<SourceDocument> docs = session.getSessionInfo().getSourceDocuments(); for (int i = 0; i < docs.length(); i++) { // restore the docs assigned to this source window SourceDocument doc = docs.get(i); String docWindowId = doc.getProperties().getString( SourceWindowManager.SOURCE_WINDOW_ID); if (docWindowId == null) docWindowId = ""; String currentSourceWindowId = SourceWindowManager.getSourceWindowId(); // it belongs in this window if (a) it's assigned to it, or (b) this // is the main window, and the window it's assigned to isn't open. if (currentSourceWindowId == docWindowId || (SourceWindowManager.isMainSourceWindow() && !windowManager_.isSourceWindowOpen(docWindowId))) { // attempt to add a tab for the current doc; try/catch this since // we don't want to allow one failure to prevent all docs from // opening EditingTarget sourceEditor = null; try { sourceEditor = addTab(doc, true, OPEN_REPLAY); } catch (Exception e) { Debug.logException(e); } // if we couldn't add the tab for this doc, just continue to the // next one if (sourceEditor == null) continue; } } } private void openEditPublishedDocs() { // don't do this if we are switching projects (it // will be done after the switch) if (ApplicationAction.isSwitchProject()) return; // check for edit_published url parameter final String kEditPublished = "edit_published"; String editPublished = StringUtil.notNull( Window.Location.getParameter(kEditPublished)); // this is an appPath which we can call the server // to determine source files to edit if (editPublished.length() > 0) { // remove it from the url ApplicationUtils.removeQueryParam(kEditPublished); server_.getEditPublishedDocs( editPublished, new SimpleRequestCallback<JsArrayString>() { @Override public void onResponseReceived(JsArrayString docs) { new SourceFilesOpener(docs).run(); } } ); } } private void openProjectDocs(final Session session) { JsArrayString openDocs = session.getSessionInfo().getProjectOpenDocs(); if (openDocs.length() > 0) { // set new tab pending for the duration of the continuation newTabPending_++; // create a continuation for opening the source docs SerializedCommandQueue openCommands = new SerializedCommandQueue(); for (int i=0; i<openDocs.length(); i++) { String doc = openDocs.get(i); final FileSystemItem fsi = FileSystemItem.createFile(doc); openCommands.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { openFile(fsi, fileTypeRegistry_.getTextTypeForFile(fsi), new CommandWithArg<EditingTarget>() { @Override public void execute(EditingTarget arg) { continuation.execute(); } }); } }); } // decrement newTabPending and select first tab when done openCommands.addCommand(new SerializedCommand() { @Override public void onExecute(Command continuation) { newTabPending_--; onFirstTab(); continuation.execute(); } }); // execute the continuation openCommands.run(); } } public void onShowContent(ShowContentEvent event) { // ignore if we're a satellite if (!SourceWindowManager.isMainSourceWindow()) return; ensureVisible(true); ContentItem content = event.getContent(); server_.newDocument( FileTypeRegistry.URLCONTENT.getTypeId(), null, (JsObject) content.cast(), new SimpleRequestCallback<SourceDocument>("Show") { @Override public void onResponseReceived(SourceDocument response) { addTab(response, OPEN_INTERACTIVE); } }); } @Override public void onOpenObjectExplorerEvent(OpenObjectExplorerEvent event) { // ignore if we're a satellite if (!SourceWindowManager.isMainSourceWindow()) return; ObjectExplorerHandle handle = event.getHandle(); // attempt to open pre-existing tab for (int i = 0; i < editors_.size(); i++) { String path = editors_.get(i).getPath(); if (path != null && path.equals(handle.getPath())) { ((ObjectExplorerEditingTarget)editors_.get(i)).update(handle); ensureVisible(false); view_.selectTab(i); return; } } ensureVisible(true); server_.newDocument( FileTypeRegistry.OBJECT_EXPLORER.getTypeId(), null, (JsObject) handle.cast(), new SimpleRequestCallback<SourceDocument>("Show Object Explorer") { @Override public void onResponseReceived(SourceDocument response) { addTab(response, OPEN_INTERACTIVE); } }); } @Override public void onShowData(ShowDataEvent event) { // ignore if we're a satellite if (!SourceWindowManager.isMainSourceWindow()) return; DataItem data = event.getData(); for (int i = 0; i < editors_.size(); i++) { String path = editors_.get(i).getPath(); if (path != null && path.equals(data.getURI())) { ((DataEditingTarget)editors_.get(i)).updateData(data); ensureVisible(false); view_.selectTab(i); return; } } ensureVisible(true); server_.newDocument( FileTypeRegistry.DATAFRAME.getTypeId(), null, (JsObject) data.cast(), new SimpleRequestCallback<SourceDocument>("Show Data Frame") { @Override public void onResponseReceived(SourceDocument response) { addTab(response, OPEN_INTERACTIVE); } }); } public void onShowProfiler(OpenProfileEvent event) { String profilePath = event.getFilePath(); String htmlPath = event.getHtmlPath(); String htmlLocalPath = event.getHtmlLocalPath(); // first try to activate existing for (int idx = 0; idx < editors_.size(); idx++) { String path = editors_.get(idx).getPath(); if (path != null && profilePath.equals(path)) { ensureVisible(false); view_.selectTab(idx); return; } } // create new profiler ensureVisible(true); if (event.getDocId() != null) { server_.getSourceDocument(event.getDocId(), new ServerRequestCallback<SourceDocument>() { @Override public void onResponseReceived(SourceDocument response) { addTab(response, OPEN_INTERACTIVE); } @Override public void onError(ServerError error) { Debug.logError(error); globalDisplay_.showErrorMessage("Source Document Error", error.getUserMessage()); } }); } else { server_.newDocument( FileTypeRegistry.PROFILER.getTypeId(), null, (JsObject) ProfilerContents.create( profilePath, htmlPath, htmlLocalPath, event.getCreateProfile()).cast(), new SimpleRequestCallback<SourceDocument>("Show Profiler") { @Override public void onResponseReceived(SourceDocument response) { addTab(response, OPEN_INTERACTIVE); } @Override public void onError(ServerError error) { Debug.logError(error); globalDisplay_.showErrorMessage("Source Document Error", error.getUserMessage()); } }); } } @Handler public void onNewSourceDoc() { newDoc(FileTypeRegistry.R, null); } @Handler public void onNewTextDoc() { newDoc(FileTypeRegistry.TEXT, null); } @Handler public void onNewRNotebook() { dependencyManager_.withRMarkdown("R Notebook", "Create R Notebook", new CommandWithArg<Boolean>() { @Override public void execute(Boolean succeeded) { if (!succeeded) { globalDisplay_.showErrorMessage("Notebook Creation Failed", "One or more packages required for R Notebook " + "creation were not installed."); return; } String basename = "r_markdown_notebook"; if (BrowseCap.isMacintosh()) basename += "_osx"; newSourceDocWithTemplate( FileTypeRegistry.RMARKDOWN, "", basename + ".Rmd", Position.create(3, 0)); } }); } @Handler public void onNewCppDoc() { if (uiPrefs_.useRcppTemplate().getValue()) { newSourceDocWithTemplate( FileTypeRegistry.CPP, "", "rcpp.cpp", Position.create(0, 0), new CommandWithArg<EditingTarget> () { @Override public void execute(EditingTarget target) { target.verifyCppPrerequisites(); } } ); } else { newDoc(FileTypeRegistry.CPP, new ResultCallback<EditingTarget, ServerError> () { @Override public void onSuccess(EditingTarget target) { target.verifyCppPrerequisites(); } }); } } @Handler public void onNewSweaveDoc() { // set concordance value if we need to String concordance = new String(); if (uiPrefs_.alwaysEnableRnwConcordance().getValue()) { RnwWeave activeWeave = rnwWeaveRegistry_.findTypeIgnoreCase( uiPrefs_.defaultSweaveEngine().getValue()); if (activeWeave.getInjectConcordance()) concordance = "\\SweaveOpts{concordance=TRUE}\n"; } final String concordanceValue = concordance; // show progress final ProgressIndicator indicator = new GlobalProgressDelayer( globalDisplay_, 500, "Creating new document...").getIndicator(); // get the template server_.getSourceTemplate("", "sweave.Rnw", new ServerRequestCallback<String>() { @Override public void onResponseReceived(String templateContents) { indicator.onCompleted(); // add in concordance if necessary final boolean hasConcordance = concordanceValue.length() > 0; if (hasConcordance) { String beginDoc = "\\begin{document}\n"; templateContents = templateContents.replace( beginDoc, beginDoc + concordanceValue); } newDoc(FileTypeRegistry.SWEAVE, templateContents, new ResultCallback<EditingTarget, ServerError> () { @Override public void onSuccess(EditingTarget target) { int startRow = 4 + (hasConcordance ? 1 : 0); target.setCursorPosition(Position.create(startRow, 0)); } }); } @Override public void onError(ServerError error) { indicator.onError(error.getUserMessage()); } }); } @Handler public void onNewRMarkdownDoc() { SessionInfo sessionInfo = session_.getSessionInfo(); boolean useRMarkdownV2 = sessionInfo.getRMarkdownPackageAvailable(); if (useRMarkdownV2) newRMarkdownV2Doc(); else newRMarkdownV1Doc(); } private void doNewRShinyApp(NewShinyWebApplication.Result result) { server_.createShinyApp( result.getAppName(), result.getAppType(), result.getAppDir(), new SimpleRequestCallback<JsArrayString>("Error Creating Shiny Application", true) { @Override public void onResponseReceived(JsArrayString createdFiles) { // Open and focus files that we created new SourceFilesOpener(createdFiles).run(); } }); } // open a list of source files then focus the first one within the list private class SourceFilesOpener extends SerializedCommandQueue { public SourceFilesOpener(JsArrayString sourceFiles) { for (int i=0; i<sourceFiles.length(); i++) { final String filePath = sourceFiles.get(i); addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { FileSystemItem path = FileSystemItem.createFile(filePath); openFile(path, FileTypeRegistry.R, new CommandWithArg<EditingTarget>() { @Override public void execute(EditingTarget target) { // record first target if necessary if (firstTarget_ == null) firstTarget_ = target; continuation.execute(); } }); } }); } addCommand(new SerializedCommand() { @Override public void onExecute(Command continuation) { if (firstTarget_ != null) { view_.selectTab(firstTarget_.asWidget()); firstTarget_.setCursorPosition(Position.create(0, 0)); } continuation.execute(); } }); } private EditingTarget firstTarget_ = null; } @Handler public void onNewRShinyApp() { dependencyManager_.withShiny("Creating Shiny applications", new Command() { @Override public void execute() { NewShinyWebApplication widget = new NewShinyWebApplication( "New Shiny Web Application", new OperationWithInput<NewShinyWebApplication.Result>() { @Override public void execute(Result input) { doNewRShinyApp(input); } }); widget.showModal(); } }); } @Handler public void onNewRHTMLDoc() { newSourceDocWithTemplate(FileTypeRegistry.RHTML, "", "r_html.Rhtml"); } @Handler public void onNewRDocumentationDoc() { new NewRdDialog( new OperationWithInput<NewRdDialog.Result>() { @Override public void execute(final NewRdDialog.Result result) { final Command createEmptyDoc = new Command() { @Override public void execute() { newSourceDocWithTemplate(FileTypeRegistry.RD, result.name, "r_documentation_empty.Rd", Position.create(3, 7)); } }; if (!result.type.equals(NewRdDialog.Result.TYPE_NONE)) { server_.createRdShell( result.name, result.type, new SimpleRequestCallback<RdShellResult>() { @Override public void onResponseReceived(RdShellResult result) { if (result.getPath() != null) { fileTypeRegistry_.openFile( FileSystemItem.createFile(result.getPath())); } else if (result.getContents() != null) { newDoc(FileTypeRegistry.RD, result.getContents(), null); } else { createEmptyDoc.execute(); } } }); } else { createEmptyDoc.execute(); } } }).showModal(); } @Handler public void onNewRPresentationDoc() { dependencyManager_.withRMarkdown( "Authoring R Presentations", new Command() { @Override public void execute() { fileDialogs_.saveFile( "New R Presentation", fileContext_, workbenchContext_.getDefaultFileDialogDir(), ".Rpres", true, new ProgressOperationWithInput<FileSystemItem>() { @Override public void execute(final FileSystemItem input, final ProgressIndicator indicator) { if (input == null) { indicator.onCompleted(); return; } indicator.onProgress("Creating Presentation..."); server_.createNewPresentation( input.getPath(), new VoidServerRequestCallback(indicator) { @Override public void onSuccess() { openFile(input, FileTypeRegistry.RPRESENTATION, new CommandWithArg<EditingTarget>() { @Override public void execute(EditingTarget arg) { server_.showPresentationPane( input.getPath(), new VoidServerRequestCallback()); } }); } }); } }); } }); } private void newRMarkdownV1Doc() { newSourceDocWithTemplate(FileTypeRegistry.RMARKDOWN, "", "r_markdown.Rmd", Position.create(3, 0)); } private void newRMarkdownV2Doc() { rmarkdown_.showNewRMarkdownDialog( new OperationWithInput<NewRMarkdownDialog.Result>() { @Override public void execute(final NewRMarkdownDialog.Result result) { if (result.isNewDocument()) { NewRMarkdownDialog.RmdNewDocument doc = result.getNewDocument(); String author = doc.getAuthor(); if (author.length() > 0) { uiPrefs_.documentAuthor().setGlobalValue(author); uiPrefs_.writeUIPrefs(); } newRMarkdownV2Doc(doc); } else { newDocFromRmdTemplate(result); } } }); } private void newDocFromRmdTemplate(final NewRMarkdownDialog.Result result) { final RmdChosenTemplate template = result.getFromTemplate(); if (template.createDir()) { rmarkdown_.createDraftFromTemplate(template); return; } rmarkdown_.getTemplateContent(template, new OperationWithInput<String>() { @Override public void execute(final String content) { if (content.length() == 0) globalDisplay_.showErrorMessage("Template Content Missing", "The template at " + template.getTemplatePath() + " is missing."); newDoc(FileTypeRegistry.RMARKDOWN, content, null); } }); } private void newRMarkdownV2Doc( final NewRMarkdownDialog.RmdNewDocument doc) { rmarkdown_.frontMatterToYAML((RmdFrontMatter)doc.getJSOResult().cast(), null, new CommandWithArg<String>() { @Override public void execute(final String yaml) { String template = ""; // select a template appropriate to the document type we're creating if (doc.getTemplate().equals(RmdTemplateData.PRESENTATION_TEMPLATE)) template = "r_markdown_v2_presentation.Rmd"; else if (doc.isShiny()) { if (doc.getFormat().endsWith( RmdOutputFormat.OUTPUT_PRESENTATION_SUFFIX)) template = "r_markdown_presentation_shiny.Rmd"; else template = "r_markdown_shiny.Rmd"; } else template = "r_markdown_v2.Rmd"; newSourceDocWithTemplate(FileTypeRegistry.RMARKDOWN, "", template, Position.create(1, 0), null, new TransformerCommand<String>() { @Override public String transform(String input) { return RmdFrontMatter.FRONTMATTER_SEPARATOR + yaml + RmdFrontMatter.FRONTMATTER_SEPARATOR + "\n" + input; } }); } }); } private void newSourceDocWithTemplate(final TextFileType fileType, String name, String template) { newSourceDocWithTemplate(fileType, name, template, null); } private void newSourceDocWithTemplate(final TextFileType fileType, String name, String template, final Position cursorPosition) { newSourceDocWithTemplate(fileType, name, template, cursorPosition, null); } private void newSourceDocWithTemplate( final TextFileType fileType, String name, String template, final Position cursorPosition, final CommandWithArg<EditingTarget> onSuccess) { newSourceDocWithTemplate(fileType, name, template, cursorPosition, onSuccess, null); } private void newSourceDocWithTemplate( final TextFileType fileType, String name, String template, final Position cursorPosition, final CommandWithArg<EditingTarget> onSuccess, final TransformerCommand<String> contentTransformer) { final ProgressIndicator indicator = new GlobalProgressDelayer( globalDisplay_, 500, "Creating new document...").getIndicator(); server_.getSourceTemplate(name, template, new ServerRequestCallback<String>() { @Override public void onResponseReceived(String templateContents) { indicator.onCompleted(); if (contentTransformer != null) templateContents = contentTransformer.transform(templateContents); newDoc(fileType, templateContents, new ResultCallback<EditingTarget, ServerError> () { @Override public void onSuccess(EditingTarget target) { if (cursorPosition != null) target.setCursorPosition(cursorPosition); if (onSuccess != null) onSuccess.execute(target); } }); } @Override public void onError(ServerError error) { indicator.onError(error.getUserMessage()); } }); } private void newDoc(EditableFileType fileType, ResultCallback<EditingTarget, ServerError> callback) { newDoc(fileType, null, callback); } private void newDoc(EditableFileType fileType, final String contents, final ResultCallback<EditingTarget, ServerError> resultCallback) { ensureVisible(true); server_.newDocument( fileType.getTypeId(), contents, JsObject.createJsObject(), new SimpleRequestCallback<SourceDocument>( "Error Creating New Document") { @Override public void onResponseReceived(SourceDocument newDoc) { EditingTarget target = addTab(newDoc, OPEN_INTERACTIVE); if (contents != null) { target.forceSaveCommandActive(); manageSaveCommands(); } if (resultCallback != null) resultCallback.onSuccess(target); } @Override public void onError(ServerError error) { if (resultCallback != null) resultCallback.onFailure(error); } }); } @Handler public void onFindInFiles() { String searchPattern = ""; if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget textEditor = (TextEditingTarget) activeEditor_; String selection = textEditor.getSelectedText(); boolean multiLineSelection = selection.indexOf('\n') != -1; if ((selection.length() != 0) && !multiLineSelection) searchPattern = selection; } events_.fireEvent(new FindInFilesEvent(searchPattern)); } @Handler public void onActivateSource() { onActivateSource(null); } public void onActivateSource(final Command afterActivation) { // give the window manager a chance to activate the last source pane if (windowManager_.activateLastFocusedSource()) return; if (activeEditor_ == null) { newDoc(FileTypeRegistry.R, new ResultCallback<EditingTarget, ServerError>() { @Override public void onSuccess(EditingTarget target) { activeEditor_ = target; doActivateSource(afterActivation); } }); } else { doActivateSource(afterActivation); } } @Handler public void onLayoutZoomSource() { onActivateSource(new Command() { @Override public void execute() { events_.fireEvent(new ZoomPaneEvent("Source")); } }); } private void doActivateSource(final Command afterActivation) { ensureVisible(false); if (activeEditor_ != null) { activeEditor_.focus(); activeEditor_.ensureCursorVisible(); } if (afterActivation != null) afterActivation.execute(); } @Handler public void onSwitchToTab() { if (view_.getTabCount() == 0) return; ensureVisible(false); view_.showOverflowPopup(); } @Handler public void onFirstTab() { if (view_.getTabCount() == 0) return; ensureVisible(false); if (view_.getTabCount() > 0) setPhysicalTabIndex(0); } @Handler public void onPreviousTab() { switchToTab(-1, uiPrefs_.wrapTabNavigation().getValue()); } @Handler public void onNextTab() { switchToTab(1, uiPrefs_.wrapTabNavigation().getValue()); } @Handler public void onLastTab() { if (view_.getTabCount() == 0) return; ensureVisible(false); if (view_.getTabCount() > 0) setPhysicalTabIndex(view_.getTabCount() - 1); } public void nextTabWithWrap() { switchToTab(1, true); } public void prevTabWithWrap() { switchToTab(-1, true); } private void switchToTab(int delta, boolean wrap) { if (view_.getTabCount() == 0) return; ensureVisible(false); int targetIndex = getPhysicalTabIndex() + delta; if (targetIndex > (view_.getTabCount() - 1)) { if (wrap) targetIndex = 0; else return; } else if (targetIndex < 0) { if (wrap) targetIndex = view_.getTabCount() - 1; else return; } setPhysicalTabIndex(targetIndex); } @Handler public void onMoveTabRight() { view_.moveTab(getPhysicalTabIndex(), 1); } @Handler public void onMoveTabLeft() { view_.moveTab(getPhysicalTabIndex(), -1); } @Handler public void onMoveTabToFirst() { view_.moveTab(getPhysicalTabIndex(), getPhysicalTabIndex() * -1); } @Handler public void onMoveTabToLast() { view_.moveTab(getPhysicalTabIndex(), (view_.getTabCount() - getPhysicalTabIndex()) - 1); } @Override public void onPopoutDoc(final PopoutDocEvent e) { // disowning the doc may cause the entire window to close, so defer it // to allow any other popout processing to occur Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { disownDoc(e.getDocId()); } }); } @Override public void onDebugModeChanged(DebugModeChangedEvent evt) { // when debugging ends, always disengage any active debug highlights if (!evt.debugging() && activeEditor_ != null) { activeEditor_.endDebugHighlighting(); } } @Override public void onDocWindowChanged(final DocWindowChangedEvent e) { if (e.getNewWindowId() == SourceWindowManager.getSourceWindowId()) { ensureVisible(true); // look for a collaborative editing session currently running inside // the document being transferred between windows--if we didn't know // about one with the event, try to look it up in the local cache of // source documents final CollabEditStartParams collabParams = e.getCollabParams() == null ? windowManager_.getDocCollabParams(e.getDocId()) : e.getCollabParams(); // if we're the adopting window, add the doc server_.getSourceDocument(e.getDocId(), new ServerRequestCallback<SourceDocument>() { @Override public void onResponseReceived(final SourceDocument doc) { final EditingTarget target = addTab(doc, e.getPos()); Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { @Override public void execute() { // if there was a collab session, resume it if (collabParams != null) target.beginCollabSession(e.getCollabParams()); } }); } @Override public void onError(ServerError error) { globalDisplay_.showErrorMessage("Document Tab Move Failed", "Couldn't move the tab to this window: \n" + error.getMessage()); } }); } else if (e.getOldWindowId() == SourceWindowManager.getSourceWindowId()) { // cancel tab drag if it was occurring view_.cancelTabDrag(); // disown this doc if it was our own disownDoc(e.getDocId()); } } private void disownDoc(String docId) { suspendDocumentClose_ = true; for (int i = 0; i < editors_.size(); i++) { if (editors_.get(i).getId() == docId) { view_.closeTab(i, false); break; } } suspendDocumentClose_ = false; } @Override public void onDocTabDragInitiated(final DocTabDragInitiatedEvent event) { inEditorForId(event.getDragParams().getDocId(), new OperationWithInput<EditingTarget>() { @Override public void execute(EditingTarget editor) { DocTabDragParams params = event.getDragParams(); params.setSourcePosition(editor.currentPosition()); events_.fireEvent(new DocTabDragStartedEvent(params)); } }); } @Override public void onPopoutDocInitiated(final PopoutDocInitiatedEvent event) { inEditorForId(event.getDocId(), new OperationWithInput<EditingTarget>() { @Override public void execute(EditingTarget editor) { // if this is a text editor, ensure that its content is // synchronized with the server before we pop it out if (editor instanceof TextEditingTarget) { final TextEditingTarget textEditor = (TextEditingTarget)editor; textEditor.withSavedDoc(new Command() { @Override public void execute() { textEditor.syncLocalSourceDb(); events_.fireEvent(new PopoutDocEvent(event, textEditor.currentPosition())); } }); } else { events_.fireEvent(new PopoutDocEvent(event, editor.currentPosition())); } } }); } @Handler public void onCloseSourceDoc() { closeSourceDoc(true); } void closeSourceDoc(boolean interactive) { if (view_.getTabCount() == 0) return; view_.closeTab(view_.getActiveTabIndex(), interactive); } /** * Execute the given command for each editor, using continuation-passing * style. When executed, the CPSEditingTargetCommand needs to execute its * own Command parameter to continue the iteration. * @param command The command to run on each EditingTarget */ private void cpsExecuteForEachEditor(ArrayList<EditingTarget> editors, final CPSEditingTargetCommand command, final Command completedCommand) { SerializedCommandQueue queue = new SerializedCommandQueue(); // Clone editors_, since the original may be mutated during iteration for (final EditingTarget editor : new ArrayList<EditingTarget>(editors)) { queue.addCommand(new SerializedCommand() { @Override public void onExecute(Command continuation) { command.execute(editor, continuation); } }); } if (completedCommand != null) { queue.addCommand(new SerializedCommand() { public void onExecute(Command continuation) { completedCommand.execute(); continuation.execute(); } }); } } private void cpsExecuteForEachEditor(ArrayList<EditingTarget> editors, final CPSEditingTargetCommand command) { cpsExecuteForEachEditor(editors, command, null); } @Handler public void onSaveAllSourceDocs() { cpsExecuteForEachEditor(editors_, new CPSEditingTargetCommand() { @Override public void execute(EditingTarget target, Command continuation) { if (target.dirtyState().getValue()) { target.save(continuation); } else { continuation.execute(); } } }); } private void saveEditingTargetsWithPrompt( String title, ArrayList<EditingTarget> editingTargets, final Command onCompleted, final Command onCancelled) { // execute on completed right away if the list is empty if (editingTargets.size() == 0) { onCompleted.execute(); } // if there is just one thing dirty then go straight to the save dialog else if (editingTargets.size() == 1) { editingTargets.get(0).saveWithPrompt(onCompleted, onCancelled); } // otherwise use the multi save changes dialog else { // convert to UnsavedChangesTarget collection ArrayList<UnsavedChangesTarget> unsavedTargets = new ArrayList<UnsavedChangesTarget>(); unsavedTargets.addAll(editingTargets); // show dialog view_.showUnsavedChangesDialog( title, unsavedTargets, new OperationWithInput<UnsavedChangesDialog.Result>() { @Override public void execute(UnsavedChangesDialog.Result result) { saveChanges(result.getSaveTargets(), onCompleted); } }, onCancelled); } } private void saveChanges(ArrayList<UnsavedChangesTarget> targets, Command onCompleted) { // convert back to editing targets ArrayList<EditingTarget> saveTargets = new ArrayList<EditingTarget>(); for (UnsavedChangesTarget target: targets) { EditingTarget saveTarget = getEditingTargetForId(target.getId()); if (saveTarget != null) saveTargets.add(saveTarget); } // execute the save cpsExecuteForEachEditor( // targets the user chose to save saveTargets, // save each editor new CPSEditingTargetCommand() { @Override public void execute(EditingTarget saveTarget, Command continuation) { saveTarget.save(continuation); } }, // onCompleted at the end onCompleted ); } private EditingTarget getEditingTargetForId(String id) { for (EditingTarget target : editors_) if (id.equals(target.getId())) return target; return null; } @Handler public void onCloseAllSourceDocs() { closeAllSourceDocs("Close All", null, false); } @Handler public void onCloseOtherSourceDocs() { closeAllSourceDocs("Close Other", null, true); } public void closeAllSourceDocs(final String caption, final Command onCompleted, final boolean excludeActive) { if (SourceWindowManager.isMainSourceWindow() && !excludeActive) { // if this is the main window, close docs in the satellites first windowManager_.closeAllSatelliteDocs(caption, new Command() { @Override public void execute() { closeAllLocalSourceDocs(caption, onCompleted, excludeActive); } }); } else { // this is a satellite (or we don't need to query satellites)--just // close our own tabs closeAllLocalSourceDocs(caption, onCompleted, excludeActive); } } private void closeAllLocalSourceDocs(String caption, Command onCompleted, final boolean excludeActive) { // save active editor for exclusion (it changes as we close tabs) final EditingTarget activeEditor = activeEditor_; // collect up a list of dirty documents ArrayList<EditingTarget> dirtyTargets = new ArrayList<EditingTarget>(); for (EditingTarget target : editors_) { if (excludeActive && target == activeEditor) continue; if (target.dirtyState().getValue()) dirtyTargets.add(target); } // create a command used to close all tabs final Command closeAllTabsCommand = new Command() { @Override public void execute() { cpsExecuteForEachEditor(editors_, new CPSEditingTargetCommand() { @Override public void execute(EditingTarget target, Command continuation) { if (excludeActive && target == activeEditor) { continuation.execute(); return; } else { view_.closeTab(target.asWidget(), false, continuation); } } }); } }; // save targets saveEditingTargetsWithPrompt(caption, dirtyTargets, CommandUtil.join(closeAllTabsCommand, onCompleted), null); } private boolean isUnsavedTarget(EditingTarget target, int type) { boolean fileBacked = target.getPath() != null; return target.dirtyState().getValue() && ((type == TYPE_FILE_BACKED && fileBacked) || (type == TYPE_UNTITLED && !fileBacked)); } public ArrayList<UnsavedChangesTarget> getUnsavedChanges(int type) { ArrayList<UnsavedChangesTarget> targets = new ArrayList<UnsavedChangesTarget>(); // if this is the main window, collect all unsaved changes from // the satellite windows as well if (SourceWindowManager.isMainSourceWindow()) { targets.addAll(windowManager_.getAllSatelliteUnsavedChanges(type)); } for (EditingTarget target : editors_) if (isUnsavedTarget(target, type)) targets.add(target); return targets; } public void saveAllUnsaved(final Command onCompleted) { Command saveAllLocal = new Command() { @Override public void execute() { saveChanges(getUnsavedChanges(TYPE_FILE_BACKED), onCompleted); } }; // if this is the main source window, save all files in satellites first if (SourceWindowManager.isMainSourceWindow()) windowManager_.saveAllUnsaved(saveAllLocal); else saveAllLocal.execute(); } public void saveWithPrompt(UnsavedChangesTarget target, Command onCompleted, Command onCancelled) { if (SourceWindowManager.isMainSourceWindow() && !windowManager_.getWindowIdOfDocId(target.getId()).isEmpty()) { // we are the main window, and we're being asked to save a document // that's in a different window; perform the save over there windowManager_.saveWithPrompt(UnsavedChangesItem.create(target), onCompleted); return; } EditingTarget editingTarget = getEditingTargetForId(target.getId()); if (editingTarget != null) editingTarget.saveWithPrompt(onCompleted, onCancelled); } public void handleUnsavedChangesBeforeExit( final ArrayList<UnsavedChangesTarget> saveTargets, final Command onCompleted) { // first handle saves, then revert unsaved, then callback on completed final Command completed = new Command() { @Override public void execute() { // revert unsaved revertUnsavedTargets(onCompleted); } }; // if this is the main source window, let satellite windows save any // changes first if (SourceWindowManager.isMainSourceWindow()) { windowManager_.handleUnsavedChangesBeforeExit( saveTargets, new Command() { @Override public void execute() { saveChanges(saveTargets, completed); } }); } else { saveChanges(saveTargets, completed); } } public Display getView() { return view_; } private void revertActiveDocument() { if (activeEditor_ == null) return; if (activeEditor_.getPath() != null) activeEditor_.revertChanges(null); // Ensure that the document is in view activeEditor_.ensureCursorVisible(); } private void revertUnsavedTargets(Command onCompleted) { // collect up unsaved targets ArrayList<EditingTarget> unsavedTargets = new ArrayList<EditingTarget>(); for (EditingTarget target : editors_) if (isUnsavedTarget(target, TYPE_FILE_BACKED)) unsavedTargets.add(target); // revert all of them cpsExecuteForEachEditor( // targets the user chose not to save unsavedTargets, // save each editor new CPSEditingTargetCommand() { @Override public void execute(EditingTarget saveTarget, Command continuation) { if (saveTarget.getPath() != null) { // file backed document -- revert it saveTarget.revertChanges(continuation); } else { // untitled document -- just close the tab non-interactively view_.closeTab(saveTarget.asWidget(), false, continuation); } } }, // onCompleted at the end onCompleted ); } @Handler public void onOpenSourceDoc() { fileDialogs_.openFile( "Open File", fileContext_, workbenchContext_.getDefaultFileDialogDir(), new ProgressOperationWithInput<FileSystemItem>() { public void execute(final FileSystemItem input, ProgressIndicator indicator) { if (input == null) return; workbenchContext_.setDefaultFileDialogDir( input.getParentPath()); indicator.onCompleted(); Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { fileTypeRegistry_.openFile(input); } }); } }); } public void onNewDocumentWithCode(final NewDocumentWithCodeEvent event) { // determine the type final EditableFileType docType; if (event.getType().equals(NewDocumentWithCodeEvent.R_SCRIPT)) docType = FileTypeRegistry.R; else docType = FileTypeRegistry.RMARKDOWN; // command to create and run the new doc Command newDocCommand = new Command() { @Override public void execute() { newDoc(docType, new ResultCallback<EditingTarget, ServerError>() { public void onSuccess(EditingTarget arg) { TextEditingTarget editingTarget = (TextEditingTarget)arg; editingTarget.insertCode(event.getCode(), false); if (event.getCursorPosition() != null) { editingTarget.navigateToPosition(event.getCursorPosition(), false); } if (event.getExecute()) { if (docType.equals(FileTypeRegistry.R)) { commands_.executeToCurrentLine().execute(); commands_.activateSource().execute(); } else { commands_.executePreviousChunks().execute(); } } } }); } }; // do it if (docType.equals(FileTypeRegistry.R)) { newDocCommand.execute(); } else { dependencyManager_.withRMarkdown("R Notebook", "Create R Notebook", newDocCommand); } } public void onOpenSourceFile(final OpenSourceFileEvent event) { doOpenSourceFile(event.getFile(), event.getFileType(), event.getPosition(), null, event.getNavigationMethod(), false); } public void onOpenPresentationSourceFile(OpenPresentationSourceFileEvent event) { // don't do the navigation if the active document is a source // file from this presentation module doOpenSourceFile(event.getFile(), event.getFileType(), event.getPosition(), event.getPattern(), NavigationMethods.HIGHLIGHT_LINE, true); } public void onEditPresentationSource(final EditPresentationSourceEvent event) { openFile( event.getSourceFile(), FileTypeRegistry.RPRESENTATION, new CommandWithArg<EditingTarget>() { @Override public void execute(final EditingTarget editor) { TextEditingTargetPresentationHelper.navigateToSlide( editor, event.getSlideIndex()); } }); } private void doOpenSourceFile(final FileSystemItem file, final TextFileType fileType, final FilePosition position, final String pattern, final int navMethod, final boolean forceHighlightMode) { // if the navigation should happen in another window, do that instead NavigationResult navResult = windowManager_.navigateToFile(file, position, navMethod); // we navigated externally, just skip this if (navResult.getType() == NavigationResult.RESULT_NAVIGATED) return; // we're about to open in this window--if it's the main window, focus it if (SourceWindowManager.isMainSourceWindow() && Desktop.isDesktop()) Desktop.getFrame().bringMainFrameToFront(); final boolean isDebugNavigation = navMethod == NavigationMethods.DEBUG_STEP || navMethod == NavigationMethods.DEBUG_END; final CommandWithArg<EditingTarget> editingTargetAction = new CommandWithArg<EditingTarget>() { @Override public void execute(EditingTarget target) { if (position != null) { SourcePosition endPosition = null; if (isDebugNavigation) { DebugFilePosition filePos = (DebugFilePosition) position.cast(); endPosition = SourcePosition.create( filePos.getEndLine() - 1, filePos.getEndColumn() + 1); if (Desktop.isDesktop() && navMethod != NavigationMethods.DEBUG_END) Desktop.getFrame().bringMainFrameToFront(); } navigate(target, SourcePosition.create(position.getLine() - 1, position.getColumn() - 1), endPosition); } else if (pattern != null) { Position pos = target.search(pattern); if (pos != null) { navigate(target, SourcePosition.create(pos.getRow(), 0), null); } } } private void navigate(final EditingTarget target, final SourcePosition srcPosition, final SourcePosition srcEndPosition) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { if (navMethod == NavigationMethods.DEBUG_STEP) { target.highlightDebugLocation( srcPosition, srcEndPosition, true); } else if (navMethod == NavigationMethods.DEBUG_END) { target.endDebugHighlighting(); } else { // force highlight mode if requested if (forceHighlightMode) target.forceLineHighlighting(); // now navigate to the new position boolean highlight = navMethod == NavigationMethods.HIGHLIGHT_LINE && !uiPrefs_.highlightSelectedLine().getValue(); target.navigateToPosition(srcPosition, false, highlight); } } }); } }; if (navResult.getType() == NavigationResult.RESULT_RELOCATE) { server_.getSourceDocument(navResult.getDocId(), new ServerRequestCallback<SourceDocument>() { @Override public void onResponseReceived(final SourceDocument doc) { editingTargetAction.execute(addTab(doc, OPEN_REPLAY)); } @Override public void onError(ServerError error) { globalDisplay_.showErrorMessage("Document Tab Move Failed", "Couldn't move the tab to this window: \n" + error.getMessage()); } }); return; } final CommandWithArg<FileSystemItem> action = new CommandWithArg<FileSystemItem>() { @Override public void execute(FileSystemItem file) { openFile(file, fileType, editingTargetAction); } }; // If this is a debug navigation, we only want to treat this as a full // file open if the file isn't already open; otherwise, we can just // highlight in place. if (isDebugNavigation) { setPendingDebugSelection(); for (int i = 0; i < editors_.size(); i++) { EditingTarget target = editors_.get(i); String path = target.getPath(); if (path != null && path.equalsIgnoreCase(file.getPath())) { // the file's open; just update its highlighting if (navMethod == NavigationMethods.DEBUG_END) { target.endDebugHighlighting(); } else { view_.selectTab(i); editingTargetAction.execute(target); } return; } } // If we're here, the target file wasn't open in an editor. Don't // open a file just to turn off debug highlighting in the file! if (navMethod == NavigationMethods.DEBUG_END) return; } // Warning: event.getFile() can be null (e.g. new Sweave document) if (file != null && file.getLength() < 0) { statQueue_.add(new StatFileEntry(file, action)); if (statQueue_.size() == 1) processStatQueue(); } else { action.execute(file); } } private void processStatQueue() { if (statQueue_.isEmpty()) return; final StatFileEntry entry = statQueue_.peek(); final Command processNextEntry = new Command() { @Override public void execute() { statQueue_.remove(); if (!statQueue_.isEmpty()) processStatQueue(); } }; server_.stat(entry.file.getPath(), new ServerRequestCallback<FileSystemItem>() { @Override public void onResponseReceived(FileSystemItem response) { processNextEntry.execute(); entry.action.execute(response); } @Override public void onError(ServerError error) { processNextEntry.execute(); // Couldn't stat the file? Proceed anyway. If the file doesn't // exist, we'll let the downstream code be the one to show the // error. entry.action.execute(entry.file); } }); } private void openFile(FileSystemItem file) { openFile(file, fileTypeRegistry_.getTextTypeForFile(file)); } private void openFile(FileSystemItem file, TextFileType fileType) { openFile(file, fileType, new CommandWithArg<EditingTarget>() { @Override public void execute(EditingTarget arg) { } }); } private void openFile(final FileSystemItem file, final TextFileType fileType, final CommandWithArg<EditingTarget> executeOnSuccess) { // add this work to the queue openFileQueue_.add(new OpenFileEntry(file, fileType, executeOnSuccess)); // begin queue processing if it's the only work in the queue if (openFileQueue_.size() == 1) processOpenFileQueue(); } private void processOpenFileQueue() { // no work to do if (openFileQueue_.isEmpty()) return; // find the first work unit final OpenFileEntry entry = openFileQueue_.peek(); // define command to advance queue final Command processNextEntry = new Command() { @Override public void execute() { openFileQueue_.remove(); if (!openFileQueue_.isEmpty()) processOpenFileQueue(); } }; openFile(entry.file, entry.fileType, new ResultCallback<EditingTarget, ServerError>() { @Override public void onSuccess(EditingTarget target) { processNextEntry.execute(); if (entry.executeOnSuccess != null) entry.executeOnSuccess.execute(target); } @Override public void onFailure(ServerError error) { String message = error.getUserMessage(); // see if a special message was provided JSONValue errValue = error.getClientInfo(); if (errValue != null) { JSONString errMsg = errValue.isString(); if (errMsg != null) message = errMsg.stringValue(); } globalDisplay_.showMessage(GlobalDisplay.MSG_ERROR, "Error while opening file", message); processNextEntry.execute(); } }); } private void openNotebook( final FileSystemItem rmdFile, final SourceDocumentResult doc, final ResultCallback<EditingTarget, ServerError> resultCallback) { if (!StringUtil.isNullOrEmpty(doc.getDocPath())) { // this happens if we created the R Markdown file, or if the R Markdown // file on disk matched the one inside the notebook openFileFromServer(rmdFile, FileTypeRegistry.RMARKDOWN, resultCallback); } else if (!StringUtil.isNullOrEmpty(doc.getDocId())) { // this happens when we have to open an untitled buffer for the the // notebook (usually because the of a conflict between the Rmd on disk // and the one in the .nb.html file) server_.getSourceDocument(doc.getDocId(), new ServerRequestCallback<SourceDocument>() { @Override public void onResponseReceived(SourceDocument doc) { // create the editor EditingTarget target = addTab(doc, OPEN_INTERACTIVE); // show a warning bar if (target instanceof TextEditingTarget) { ((TextEditingTarget) target).showWarningMessage( "This notebook has the same name as an R Markdown " + "file, but doesn't match it."); } resultCallback.onSuccess(target); } @Override public void onError(ServerError error) { globalDisplay_.showErrorMessage( "Notebook Open Failed", "This notebook could not be opened. " + "If the error persists, try removing the " + "accompanying R Markdown file. \n\n" + error.getMessage()); resultCallback.onFailure(error); } }); } } private void openNotebook(final FileSystemItem rnbFile, final TextFileType fileType, final ResultCallback<EditingTarget, ServerError> resultCallback) { // construct path to .Rmd final String rnbPath = rnbFile.getPath(); final String rmdPath = FilePathUtils.filePathSansExtension(rnbPath) + ".Rmd"; final FileSystemItem rmdFile = FileSystemItem.createFile(rmdPath); // if we already have associated .Rmd file open, then just edit it // TODO: should we perform conflict resolution here as well? if (openFileAlreadyOpen(rmdFile, resultCallback)) return; // ask the server to extract the .Rmd, then open that Command extractRmdCommand = new Command() { @Override public void execute() { server_.extractRmdFromNotebook( rnbPath, new ServerRequestCallback<SourceDocumentResult>() { @Override public void onResponseReceived(SourceDocumentResult doc) { openNotebook(rmdFile, doc, resultCallback); } @Override public void onError(ServerError error) { globalDisplay_.showErrorMessage("Notebook Open Failed", "This notebook could not be opened. \n\n" + error.getMessage()); resultCallback.onFailure(error); } }); } }; dependencyManager_.withRMarkdown("R Notebook", "Using R Notebooks", extractRmdCommand); } private boolean openFileAlreadyOpen(final FileSystemItem file, final ResultCallback<EditingTarget, ServerError> resultCallback) { // check to see if any local editors have the file open for (int i = 0; i < editors_.size(); i++) { EditingTarget target = editors_.get(i); String thisPath = target.getPath(); if (thisPath != null && thisPath.equalsIgnoreCase(file.getPath())) { view_.selectTab(i); pMruList_.get().add(thisPath); if (resultCallback != null) resultCallback.onSuccess(target); return true; } } return false; } // top-level wrapper for opening files. takes care of: // - making sure the view is visible // - checking whether it is already open and re-selecting its tab // - prohibit opening very large files (>500KB) // - confirmation of opening large files (>100KB) // - finally, actually opening the file from the server // via the call to the lower level openFile method private void openFile(final FileSystemItem file, final TextFileType fileType, final ResultCallback<EditingTarget, ServerError> resultCallback) { ensureVisible(true); if (fileType.isRNotebook()) { openNotebook(file, fileType, resultCallback); return; } if (file == null) { newDoc(fileType, resultCallback); return; } if (openFileAlreadyOpen(file, resultCallback)) return; EditingTarget target = editingTargetSource_.getEditingTarget(fileType); if (file.getLength() > target.getFileSizeLimit()) { if (resultCallback != null) resultCallback.onCancelled(); showFileTooLargeWarning(file, target.getFileSizeLimit()); } else if (file.getLength() > target.getLargeFileSize()) { confirmOpenLargeFile(file, new Operation() { public void execute() { openFileFromServer(file, fileType, resultCallback); } }, new Operation() { public void execute() { // user (wisely) cancelled if (resultCallback != null) resultCallback.onCancelled(); } }); } else { openFileFromServer(file, fileType, resultCallback); } } private void showFileTooLargeWarning(FileSystemItem file, long sizeLimit) { StringBuilder msg = new StringBuilder(); msg.append("The file '" + file.getName() + "' is too "); msg.append("large to open in the source editor (the file is "); msg.append(StringUtil.formatFileSize(file.getLength()) + " and the "); msg.append("maximum file size is "); msg.append(StringUtil.formatFileSize(sizeLimit) + ")"); globalDisplay_.showMessage(GlobalDisplay.MSG_WARNING, "Selected File Too Large", msg.toString()); } private void confirmOpenLargeFile(FileSystemItem file, Operation openOperation, Operation cancelOperation) { StringBuilder msg = new StringBuilder(); msg.append("The source file '" + file.getName() + "' is large ("); msg.append(StringUtil.formatFileSize(file.getLength()) + ") "); msg.append("and may take some time to open. "); msg.append("Are you sure you want to continue opening it?"); globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_WARNING, "Confirm Open", msg.toString(), openOperation, false); // 'No' is default } private void openFileFromServer( final FileSystemItem file, final TextFileType fileType, final ResultCallback<EditingTarget, ServerError> resultCallback) { final Command dismissProgress = globalDisplay_.showProgress( "Opening file..."); server_.openDocument( file.getPath(), fileType.getTypeId(), uiPrefs_.defaultEncoding().getValue(), new ServerRequestCallback<SourceDocument>() { @Override public void onError(ServerError error) { dismissProgress.execute(); pMruList_.get().remove(file.getPath()); Debug.logError(error); if (resultCallback != null) resultCallback.onFailure(error); } @Override public void onResponseReceived(SourceDocument document) { dismissProgress.execute(); pMruList_.get().add(document.getPath()); EditingTarget target = addTab(document, OPEN_INTERACTIVE); if (resultCallback != null) resultCallback.onSuccess(target); } }); } Widget createWidget(EditingTarget target) { return target.asWidget(); } private EditingTarget addTab(SourceDocument doc, int mode) { return addTab(doc, false, mode); } private EditingTarget addTab(SourceDocument doc, boolean atEnd, int mode) { // by default, add at the tab immediately after the current tab return addTab(doc, atEnd ? null : getPhysicalTabIndex() + 1, mode); } private EditingTarget addTab(SourceDocument doc, Integer position, int mode) { final String defaultNamePrefix = editingTargetSource_.getDefaultNamePrefix(doc); final EditingTarget target = editingTargetSource_.getEditingTarget( doc, fileContext_, new Provider<String>() { public String get() { return getNextDefaultName(defaultNamePrefix); } }); final Widget widget = createWidget(target); if (position == null) { editors_.add(target); } else { // we're inserting into an existing permuted tabset -- push aside // any tabs physically to the right of this tab editors_.add(position, target); for (int i = 0; i < tabOrder_.size(); i++) { int pos = tabOrder_.get(i); if (pos >= position) tabOrder_.set(i, pos + 1); } // add this tab in its "natural" position tabOrder_.add(position, position); } view_.addTab(widget, target.getIcon(), target.getId(), target.getName().getValue(), target.getTabTooltip(), // used as tooltip, if non-null position, true); fireDocTabsChanged(); target.getName().addValueChangeHandler(new ValueChangeHandler<String>() { public void onValueChange(ValueChangeEvent<String> event) { view_.renameTab(widget, target.getIcon(), event.getValue(), target.getPath()); fireDocTabsChanged(); } }); view_.setDirty(widget, target.dirtyState().getValue()); target.dirtyState().addValueChangeHandler(new ValueChangeHandler<Boolean>() { public void onValueChange(ValueChangeEvent<Boolean> event) { view_.setDirty(widget, event.getValue()); manageCommands(); } }); target.addEnsureVisibleHandler(new EnsureVisibleHandler() { public void onEnsureVisible(EnsureVisibleEvent event) { view_.selectTab(widget); } }); target.addCloseHandler(new CloseHandler<Void>() { public void onClose(CloseEvent<Void> voidCloseEvent) { view_.closeTab(widget, false); } }); events_.fireEvent(new SourceDocAddedEvent(doc, mode)); // adding a tab may enable commands that are only available when // multiple documents are open; if this is the second document, go check if (editors_.size() == 2) manageMultiTabCommands(); // if the target had an editing session active, attempt to resume it if (doc.getCollabParams() != null) target.beginCollabSession(doc.getCollabParams()); return target; } private String getNextDefaultName(String defaultNamePrefix) { if (StringUtil.isNullOrEmpty(defaultNamePrefix)) { defaultNamePrefix = "Untitled"; } int max = 0; for (EditingTarget target : editors_) { String name = target.getName().getValue(); max = Math.max(max, getUntitledNum(name, defaultNamePrefix)); } return defaultNamePrefix + (max + 1); } private native final int getUntitledNum(String name, String prefix) /*-{ var match = (new RegExp("^" + prefix + "([0-9]{1,5})$")).exec(name); if (!match) return 0; return parseInt(match[1]); }-*/; public void onInsertSource(final InsertSourceEvent event) { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget && commands_.executeCode().isEnabled()) { TextEditingTarget textEditor = (TextEditingTarget) activeEditor_; textEditor.insertCode(event.getCode(), event.isBlock()); } else { newDoc(FileTypeRegistry.R, new ResultCallback<EditingTarget, ServerError>() { public void onSuccess(EditingTarget arg) { ((TextEditingTarget)arg).insertCode(event.getCode(), event.isBlock()); } }); } } public void onTabClosing(final TabClosingEvent event) { EditingTarget target = editors_.get(event.getTabIndex()); if (!target.onBeforeDismiss()) event.cancel(); } @Override public void onTabClose(TabCloseEvent event) { // can't proceed if there is no active editor if (activeEditor_ == null) return; if (event.getTabIndex() >= editors_.size()) return; // Seems like this should never happen...? final String activeEditorId = activeEditor_.getId(); if (editors_.get(event.getTabIndex()).getId().equals(activeEditorId)) { // scan the source navigation history for an entry that can // be used as the next active tab (anything that doesn't have // the same document id as the currently active tab) SourceNavigation srcNav = sourceNavigationHistory_.scanBack( new SourceNavigationHistory.Filter() { public boolean includeEntry(SourceNavigation navigation) { return !navigation.getDocumentId().equals(activeEditorId); } }); // see if the source navigation we found corresponds to an active // tab -- if it does then set this on the event if (srcNav != null) { for (int i=0; i<editors_.size(); i++) { if (srcNav.getDocumentId().equals(editors_.get(i).getId())) { view_.selectTab(i); break; } } } } } private void closeTabIndex(int idx, boolean closeDocument) { EditingTarget target = editors_.remove(idx); tabOrder_.remove(new Integer(idx)); for (int i = 0; i < tabOrder_.size(); i++) { if (tabOrder_.get(i) > idx) { tabOrder_.set(i, tabOrder_.get(i) - 1); } } target.onDismiss(closeDocument ? EditingTarget.DISMISS_TYPE_CLOSE : EditingTarget.DISMISS_TYPE_MOVE); if (activeEditor_ == target) { activeEditor_.onDeactivate(); activeEditor_ = null; } if (closeDocument) { events_.fireEvent(new DocTabClosedEvent(target.getId())); server_.closeDocument(target.getId(), new VoidServerRequestCallback()); } manageCommands(); fireDocTabsChanged(); if (view_.getTabCount() == 0) { sourceNavigationHistory_.clear(); events_.fireEvent(new LastSourceDocClosedEvent()); } } public void onTabClosed(TabClosedEvent event) { closeTabIndex(event.getTabIndex(), !suspendDocumentClose_); } @Override public void onTabReorder(TabReorderEvent event) { syncTabOrder(); // sanity check: make sure we're moving from a valid location and to a // valid location if (event.getOldPos() < 0 || event.getOldPos() >= tabOrder_.size() || event.getNewPos() < 0 || event.getNewPos() >= tabOrder_.size()) { return; } // remove the tab from its old position int idx = tabOrder_.get(event.getOldPos()); tabOrder_.remove(new Integer(idx)); // force type box // add it to its new position tabOrder_.add(event.getNewPos(), idx); // sort the document IDs and send to the server ArrayList<String> ids = new ArrayList<String>(); for (int i = 0; i < tabOrder_.size(); i++) { ids.add(editors_.get(tabOrder_.get(i)).getId()); } server_.setDocOrder(ids, new VoidServerRequestCallback()); // activate the tab setPhysicalTabIndex(event.getNewPos()); fireDocTabsChanged(); } private void syncTabOrder() { // ensure the tab order is synced to the list of editors for (int i = tabOrder_.size(); i < editors_.size(); i++) { tabOrder_.add(i); } for (int i = editors_.size(); i < tabOrder_.size(); i++) { tabOrder_.remove(i); } } private void fireDocTabsChanged() { if (!initialized_) return; // ensure we have a tab order (we want the popup list to match the order // of the tabs) syncTabOrder(); String[] ids = new String[editors_.size()]; ImageResource[] icons = new ImageResource[editors_.size()]; String[] names = new String[editors_.size()]; String[] paths = new String[editors_.size()]; for (int i = 0; i < ids.length; i++) { EditingTarget target = editors_.get(tabOrder_.get(i)); ids[i] = target.getId(); icons[i] = target.getIcon(); names[i] = target.getName().getValue(); paths[i] = target.getPath(); } events_.fireEvent(new DocTabsChangedEvent(ids, icons, names, paths)); view_.manageChevronVisibility(); } public void onSelection(SelectionEvent<Integer> event) { if (activeEditor_ != null) activeEditor_.onDeactivate(); activeEditor_ = null; if (event.getSelectedItem() >= 0) { activeEditor_ = editors_.get(event.getSelectedItem()); activeEditor_.onActivate(); // let any listeners know this tab was activated events_.fireEvent(new DocTabActivatedEvent( activeEditor_.getPath(), activeEditor_.getId())); // don't send focus to the tab if we're expecting a debug selection // event if (initialized_ && !isDebugSelectionPending()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { if (activeEditor_ != null) activeEditor_.focus(); } }); } else if (isDebugSelectionPending()) { // we're debugging, so send focus to the console instead of the // editor commands_.activateConsole().execute(); clearPendingDebugSelection(); } } if (initialized_) manageCommands(); } private void manageCommands() { boolean hasDocs = editors_.size() > 0; commands_.closeSourceDoc().setEnabled(hasDocs); commands_.closeAllSourceDocs().setEnabled(hasDocs); commands_.nextTab().setEnabled(hasDocs); commands_.previousTab().setEnabled(hasDocs); commands_.firstTab().setEnabled(hasDocs); commands_.lastTab().setEnabled(hasDocs); commands_.switchToTab().setEnabled(hasDocs); commands_.setWorkingDirToActiveDoc().setEnabled(hasDocs); HashSet<AppCommand> newCommands = activeEditor_ != null ? activeEditor_.getSupportedCommands() : new HashSet<AppCommand>(); HashSet<AppCommand> commandsToEnable = new HashSet<AppCommand>(newCommands); commandsToEnable.removeAll(activeCommands_); HashSet<AppCommand> commandsToDisable = new HashSet<AppCommand>(activeCommands_); commandsToDisable.removeAll(newCommands); for (AppCommand command : commandsToEnable) { command.setEnabled(true); command.setVisible(true); } for (AppCommand command : commandsToDisable) { command.setEnabled(false); command.setVisible(false); } // commands which should always be visible even when disabled commands_.saveSourceDoc().setVisible(true); commands_.saveSourceDocAs().setVisible(true); commands_.printSourceDoc().setVisible(true); commands_.setWorkingDirToActiveDoc().setVisible(true); commands_.debugBreakpoint().setVisible(true); // manage synctex commands manageSynctexCommands(); // manage vcs commands manageVcsCommands(); // manage save and save all manageSaveCommands(); // manage source navigation manageSourceNavigationCommands(); // manage RSConnect commands manageRSConnectCommands(); // manage R Markdown commands manageRMarkdownCommands(); // manage multi-tab commands manageMultiTabCommands(); activeCommands_ = newCommands; // give the active editor a chance to manage commands if (activeEditor_ != null) activeEditor_.manageCommands(); assert verifyNoUnsupportedCommands(newCommands) : "Unsupported commands detected (please add to Source.dynamicCommands_)"; } private void manageMultiTabCommands() { boolean hasMultipleDocs = editors_.size() > 1; // special case--these editing targets always support popout, but it's // nonsensical to show it if it's the only tab in a satellite; hide it in // this case if (commands_.popoutDoc().isEnabled() && activeEditor_ != null && (activeEditor_ instanceof TextEditingTarget || activeEditor_ instanceof CodeBrowserEditingTarget) && !SourceWindowManager.isMainSourceWindow()) { commands_.popoutDoc().setVisible(hasMultipleDocs); } commands_.closeOtherSourceDocs().setEnabled(hasMultipleDocs); } private void manageSynctexCommands() { // synctex commands are enabled if we have synctex for the active editor boolean synctexAvailable = synctex_.isSynctexAvailable(); if (synctexAvailable) { if ((activeEditor_ != null) && (activeEditor_.getPath() != null) && activeEditor_.canCompilePdf()) { synctexAvailable = synctex_.isSynctexAvailable(); } else { synctexAvailable = false; } } synctex_.enableCommands(synctexAvailable); } private void manageVcsCommands() { // manage availablity of vcs commands boolean vcsCommandsEnabled = session_.getSessionInfo().isVcsEnabled() && (activeEditor_ != null) && (activeEditor_.getPath() != null) && activeEditor_.getPath().startsWith( session_.getSessionInfo().getActiveProjectDir().getPath()); commands_.vcsFileLog().setVisible(vcsCommandsEnabled); commands_.vcsFileLog().setEnabled(vcsCommandsEnabled); commands_.vcsFileDiff().setVisible(vcsCommandsEnabled); commands_.vcsFileDiff().setEnabled(vcsCommandsEnabled); commands_.vcsFileRevert().setVisible(vcsCommandsEnabled); commands_.vcsFileRevert().setEnabled(vcsCommandsEnabled); if (vcsCommandsEnabled) { String name = FileSystemItem.getNameFromPath(activeEditor_.getPath()); commands_.vcsFileDiff().setMenuLabel("_Diff \"" + name + "\""); commands_.vcsFileLog().setMenuLabel("_Log of \"" + name +"\""); commands_.vcsFileRevert().setMenuLabel("_Revert \"" + name + "\"..."); } boolean isGithubRepo = session_.getSessionInfo().isGithubRepository(); if (vcsCommandsEnabled && isGithubRepo) { String name = FileSystemItem.getNameFromPath(activeEditor_.getPath()); commands_.vcsViewOnGitHub().setVisible(true); commands_.vcsViewOnGitHub().setEnabled(true); commands_.vcsViewOnGitHub().setMenuLabel( "_View \"" + name + "\" on GitHub"); commands_.vcsBlameOnGitHub().setVisible(true); commands_.vcsBlameOnGitHub().setEnabled(true); commands_.vcsBlameOnGitHub().setMenuLabel( "_Blame \"" + name + "\" on GitHub"); } else { commands_.vcsViewOnGitHub().setVisible(false); commands_.vcsViewOnGitHub().setEnabled(false); commands_.vcsBlameOnGitHub().setVisible(false); commands_.vcsBlameOnGitHub().setEnabled(false); } } private void manageRSConnectCommands() { boolean rsCommandsAvailable = SessionUtils.showPublishUi(session_, uiPrefs_) && (activeEditor_ != null) && (activeEditor_.getPath() != null) && ((activeEditor_.getExtendedFileType() != null && activeEditor_.getExtendedFileType() .startsWith(SourceDocument.XT_SHINY_PREFIX)) || (activeEditor_.getExtendedFileType() == SourceDocument.XT_RMARKDOWN)); commands_.rsconnectDeploy().setVisible(rsCommandsAvailable); if (activeEditor_ != null) commands_.rsconnectDeploy().setLabel( activeEditor_.getExtendedFileType() != null && activeEditor_.getExtendedFileType() .startsWith(SourceDocument.XT_SHINY_PREFIX) ? "Publish Application..." : "Publish Document..."); commands_.rsconnectConfigure().setVisible(rsCommandsAvailable); } private void manageRMarkdownCommands() { boolean rmdCommandsAvailable = session_.getSessionInfo().getRMarkdownPackageAvailable() && (activeEditor_ != null) && activeEditor_.getExtendedFileType() == SourceDocument.XT_RMARKDOWN; commands_.editRmdFormatOptions().setVisible(rmdCommandsAvailable); commands_.editRmdFormatOptions().setEnabled(rmdCommandsAvailable); } private void manageSaveCommands() { boolean saveEnabled = (activeEditor_ != null) && activeEditor_.isSaveCommandActive(); commands_.saveSourceDoc().setEnabled(saveEnabled); manageSaveAllCommand(); } private void manageSaveAllCommand() { // if one document is dirty then we are enabled for (EditingTarget target : editors_) { if (target.isSaveCommandActive()) { commands_.saveAllSourceDocs().setEnabled(true); return; } } // not one was dirty, disabled commands_.saveAllSourceDocs().setEnabled(false); } private boolean verifyNoUnsupportedCommands(HashSet<AppCommand> commands) { HashSet<AppCommand> temp = new HashSet<AppCommand>(commands); temp.removeAll(dynamicCommands_); return temp.size() == 0; } private void pasteFileContentsAtCursor(final String path, final String encoding) { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { final TextEditingTarget target = (TextEditingTarget) activeEditor_; server_.getFileContents(path, encoding, new ServerRequestCallback<String>() { @Override public void onResponseReceived(String content) { target.insertCode(content, false); } @Override public void onError(ServerError error) { Debug.logError(error); } }); } } private void pasteRCodeExecutionResult(final String code) { server_.executeRCode(code, new ServerRequestCallback<String>() { @Override public void onResponseReceived(String output) { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget editor = (TextEditingTarget) activeEditor_; editor.insertCode(output, false); } } @Override public void onError(ServerError error) { Debug.logError(error); } }); } private void reflowText() { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget editor = (TextEditingTarget) activeEditor_; editor.reflowText(); } } private void reindent() { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget editor = (TextEditingTarget) activeEditor_; editor.getDocDisplay().reindent(); } } private void editFile(final String path) { server_.ensureFileExists( path, new ServerRequestCallback<Boolean>() { @Override public void onResponseReceived(Boolean success) { if (success) { FileSystemItem file = FileSystemItem.createFile(path); openFile(file); } } @Override public void onError(ServerError error) { Debug.logError(error); } }); } private void showHelpAtCursor() { if (activeEditor_ != null && activeEditor_ instanceof TextEditingTarget) { TextEditingTarget editor = (TextEditingTarget) activeEditor_; editor.showHelpAtCursor(); } } public void onFileEdit(FileEditEvent event) { if (SourceWindowManager.isMainSourceWindow()) { fileTypeRegistry_.editFile(event.getFile()); } } public void onBeforeShow(BeforeShowEvent event) { if (view_.getTabCount() == 0 && newTabPending_ == 0) { // Avoid scenarios where the Source tab comes up but no tabs are // in it. (But also avoid creating an extra source tab when there // were already new tabs about to be created!) onNewSourceDoc(); } } @Handler public void onSourceNavigateBack() { if (!sourceNavigationHistory_.isForwardEnabled()) { if (activeEditor_ != null) activeEditor_.recordCurrentNavigationPosition(); } SourceNavigation navigation = sourceNavigationHistory_.goBack(); if (navigation != null) attemptSourceNavigation(navigation, commands_.sourceNavigateBack()); } @Handler public void onSourceNavigateForward() { SourceNavigation navigation = sourceNavigationHistory_.goForward(); if (navigation != null) attemptSourceNavigation(navigation, commands_.sourceNavigateForward()); } private void attemptSourceNavigation(final SourceNavigation navigation, final AppCommand retryCommand) { // see if we can navigate by id String docId = navigation.getDocumentId(); final EditingTarget target = getEditingTargetForId(docId); if (target != null) { // check for navigation to the current position -- in this // case execute the retry command if ( (target == activeEditor_) && target.isAtSourceRow(navigation.getPosition())) { if (retryCommand.isEnabled()) retryCommand.execute(); } else { suspendSourceNavigationAdding_ = true; try { view_.selectTab(target.asWidget()); target.restorePosition(navigation.getPosition()); } finally { suspendSourceNavigationAdding_ = false; } } } // check for code browser navigation else if ((navigation.getPath() != null) && navigation.getPath().startsWith(CodeBrowserEditingTarget.PATH)) { activateCodeBrowser( navigation.getPath(), false, new SourceNavigationResultCallback<CodeBrowserEditingTarget>( navigation.getPosition(), retryCommand)); } // check for file path navigation else if ((navigation.getPath() != null) && !navigation.getPath().startsWith(DataItem.URI_PREFIX) && !navigation.getPath().startsWith(ObjectExplorerHandle.URI_PREFIX)) { FileSystemItem file = FileSystemItem.createFile(navigation.getPath()); TextFileType fileType = fileTypeRegistry_.getTextTypeForFile(file); // open the file and restore the position openFile(file, fileType, new SourceNavigationResultCallback<EditingTarget>( navigation.getPosition(), retryCommand)); } else { // couldn't navigate to this item, retry if (retryCommand.isEnabled()) retryCommand.execute(); } } private void manageSourceNavigationCommands() { commands_.sourceNavigateBack().setEnabled( sourceNavigationHistory_.isBackEnabled()); commands_.sourceNavigateForward().setEnabled( sourceNavigationHistory_.isForwardEnabled()); } @Override public void onCodeBrowserNavigation(final CodeBrowserNavigationEvent event) { // if this isn't the main source window, don't handle server-dispatched // code browser events if (event.serverDispatched() && !SourceWindowManager.isMainSourceWindow()) { return; } tryExternalCodeBrowser(event.getFunction(), event, new Command() { @Override public void execute() { if (event.getDebugPosition() != null) { setPendingDebugSelection(); } activateCodeBrowser( CodeBrowserEditingTarget.getCodeBrowserPath(event.getFunction()), !event.serverDispatched(), new ResultCallback<CodeBrowserEditingTarget,ServerError>() { @Override public void onSuccess(CodeBrowserEditingTarget target) { target.showFunction(event.getFunction()); if (event.getDebugPosition() != null) { highlightDebugBrowserPosition(target, event.getDebugPosition(), event.getExecuting()); } } }); } }); } @Override public void onCodeBrowserFinished(final CodeBrowserFinishedEvent event) { tryExternalCodeBrowser(event.getFunction(), event, new Command() { @Override public void execute() { final String path = CodeBrowserEditingTarget.getCodeBrowserPath( event.getFunction()); for (int i = 0; i < editors_.size(); i++) { if (editors_.get(i).getPath() == path) { view_.closeTab(i, false); return; } } } }); } @Override public void onCodeBrowserHighlight(final CodeBrowserHighlightEvent event) { tryExternalCodeBrowser(event.getFunction(), event, new Command() { @Override public void execute() { setPendingDebugSelection(); activateCodeBrowser( CodeBrowserEditingTarget.getCodeBrowserPath(event.getFunction()), false, new ResultCallback<CodeBrowserEditingTarget,ServerError>() { @Override public void onSuccess(CodeBrowserEditingTarget target) { // if we just stole this code browser from another window, // we may need to repopulate it if (StringUtil.isNullOrEmpty(target.getContext())) target.showFunction(event.getFunction()); highlightDebugBrowserPosition(target, event.getDebugPosition(), true); } }); } }); } private void tryExternalCodeBrowser(SearchPathFunctionDefinition func, CrossWindowEvent<?> event, Command withLocalCodeBrowser) { final String path = CodeBrowserEditingTarget.getCodeBrowserPath(func); NavigationResult result = windowManager_.navigateToCodeBrowser( path, event); if (result.getType() != NavigationResult.RESULT_NAVIGATED) { withLocalCodeBrowser.execute(); } } private void highlightDebugBrowserPosition(CodeBrowserEditingTarget target, DebugFilePosition pos, boolean executing) { target.highlightDebugLocation(SourcePosition.create( pos.getLine(), pos.getColumn() - 1), SourcePosition.create( pos.getEndLine(), pos.getEndColumn() + 1), executing); } private void activateCodeBrowser( final String codeBrowserPath, boolean replaceIfActive, final ResultCallback<CodeBrowserEditingTarget,ServerError> callback) { // first check to see if this request can be fulfilled with an existing // code browser tab for (int i = 0; i < editors_.size(); i++) { if (editors_.get(i).getPath() == codeBrowserPath) { // select the tab ensureVisible(false); view_.selectTab(i); // callback callback.onSuccess((CodeBrowserEditingTarget) editors_.get(i)); // satisfied request return; } } // then check to see if the active editor is a code browser -- if it is, // we'll use it as is, replacing its contents if (replaceIfActive && activeEditor_ != null && activeEditor_ instanceof CodeBrowserEditingTarget) { events_.fireEvent(new CodeBrowserCreatedEvent(activeEditor_.getId(), codeBrowserPath)); callback.onSuccess((CodeBrowserEditingTarget) activeEditor_); return; } // create a new one newDoc(FileTypeRegistry.CODEBROWSER, new ResultCallback<EditingTarget, ServerError>() { @Override public void onSuccess(EditingTarget arg) { events_.fireEvent(new CodeBrowserCreatedEvent( arg.getId(), codeBrowserPath)); callback.onSuccess( (CodeBrowserEditingTarget)arg); } @Override public void onFailure(ServerError error) { callback.onFailure(error); } @Override public void onCancelled() { callback.onCancelled(); } }); } private boolean isDebugSelectionPending() { return debugSelectionTimer_ != null; } private void clearPendingDebugSelection() { if (debugSelectionTimer_ != null) { debugSelectionTimer_.cancel(); debugSelectionTimer_ = null; } } private void setPendingDebugSelection() { if (!isDebugSelectionPending()) { debugSelectionTimer_ = new Timer() { public void run() { debugSelectionTimer_ = null; } }; debugSelectionTimer_.schedule(250); } } private class SourceNavigationResultCallback<T extends EditingTarget> extends ResultCallback<T,ServerError> { public SourceNavigationResultCallback(SourcePosition restorePosition, AppCommand retryCommand) { suspendSourceNavigationAdding_ = true; restorePosition_ = restorePosition; retryCommand_ = retryCommand; } @Override public void onSuccess(final T target) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { try { target.restorePosition(restorePosition_); } finally { suspendSourceNavigationAdding_ = false; } } }); } @Override public void onFailure(ServerError info) { suspendSourceNavigationAdding_ = false; if (retryCommand_.isEnabled()) retryCommand_.execute(); } @Override public void onCancelled() { suspendSourceNavigationAdding_ = false; } private final SourcePosition restorePosition_; private final AppCommand retryCommand_; } @Override public void onSourceExtendedTypeDetected(SourceExtendedTypeDetectedEvent e) { // set the extended type of the specified source file for (EditingTarget editor : editors_) { if (editor.getId().equals(e.getDocId())) { editor.adaptToExtendedFileType(e.getExtendedType()); break; } } } @Override public void onSnippetsChanged(SnippetsChangedEvent event) { SnippetHelper.onSnippetsChanged(event); } // when tabs have been reordered in the session, the physical layout of the // tabs doesn't match the logical order of editors_. it's occasionally // necessary to get or set the tabs by their physical order. public int getPhysicalTabIndex() { int idx = view_.getActiveTabIndex(); if (idx < tabOrder_.size()) { idx = tabOrder_.indexOf(idx); } return idx; } public void setPhysicalTabIndex(int idx) { if (idx < tabOrder_.size()) { idx = tabOrder_.get(idx); } view_.selectTab(idx); } public EditingTarget getActiveEditor() { return activeEditor_; } public void onOpenProfileEvent(OpenProfileEvent event) { onShowProfiler(event); } private void inEditorForPath(String path, OperationWithInput<EditingTarget> onEditorLocated) { for (int i = 0; i < editors_.size(); i++) { String editorPath = editors_.get(i).getPath(); if (editorPath != null && editorPath.equals(path)) { onEditorLocated.execute(editors_.get(i)); break; } } } private void inEditorForId(String id, OperationWithInput<EditingTarget> onEditorLocated) { for (int i = 0; i < editors_.size(); i++) { String editorId = editors_.get(i).getId(); if (editorId != null && editorId.equals(id)) { onEditorLocated.execute(editors_.get(i)); break; } } } private void dispatchEditorEvent(final String id, final CommandWithArg<DocDisplay> command) { InputEditorDisplay console = consoleEditorProvider_.getConsoleEditor(); boolean isConsoleEvent = false; if (console != null) { isConsoleEvent = (StringUtil.isNullOrEmpty(id) && console.isFocused()) || "#console".equals(id); } if (isConsoleEvent) { command.execute((DocDisplay) console); } else { withTarget(id, new CommandWithArg<TextEditingTarget>() { @Override public void execute(TextEditingTarget target) { command.execute(target.getDocDisplay()); } }); } } @Override public void onSetSelectionRanges(final SetSelectionRangesEvent event) { dispatchEditorEvent(event.getData().getId(), new CommandWithArg<DocDisplay>() { @Override public void execute(DocDisplay docDisplay) { JsArray<Range> ranges = event.getData().getRanges(); if (ranges.length() == 0) return; AceEditor editor = (AceEditor) docDisplay; editor.setSelectionRanges(ranges); } }); } @Override public void onGetEditorContext(GetEditorContextEvent event) { GetEditorContextEvent.Data data = event.getData(); int type = data.getType(); if (type == GetEditorContextEvent.TYPE_ACTIVE_EDITOR) { if (consoleEditorHadFocusLast() || activeEditor_ == null) type = GetEditorContextEvent.TYPE_CONSOLE_EDITOR; else type = GetEditorContextEvent.TYPE_SOURCE_EDITOR; } if (type == GetEditorContextEvent.TYPE_CONSOLE_EDITOR) { InputEditorDisplay editor = consoleEditorProvider_.getConsoleEditor(); if (editor != null && editor instanceof DocDisplay) { getEditorContext("#console", "", (DocDisplay) editor); return; } } else if (type == GetEditorContextEvent.TYPE_SOURCE_EDITOR) { EditingTarget target = activeEditor_; if (target != null && target instanceof TextEditingTarget) { getEditorContext( target.getId(), target.getPath(), ((TextEditingTarget) target).getDocDisplay()); return; } } // We need to ensure a 'getEditorContext' event is always // returned as we have a 'wait-for' event on the server side server_.getEditorContextCompleted( GetEditorContextEvent.SelectionData.create(), new VoidServerRequestCallback()); } @Override public void onReplaceRanges(final ReplaceRangesEvent event) { dispatchEditorEvent(event.getData().getId(), new CommandWithArg<DocDisplay>() { @Override public void execute(DocDisplay docDisplay) { doReplaceRanges(event, docDisplay); } }); } private void doReplaceRanges(ReplaceRangesEvent event, DocDisplay docDisplay) { JsArray<ReplacementData> data = event.getData().getReplacementData(); for (int i = 0; i < data.length(); i++) { ReplacementData el = data.get(i); Range range = el.getRange(); String text = el.getText(); // A null range at this point is a proxy to use the current selection if (range == null) range = docDisplay.getSelectionRange(); docDisplay.replaceRange(range, text); } docDisplay.focus(); } private class OpenFileEntry { public OpenFileEntry(FileSystemItem fileIn, TextFileType fileTypeIn, CommandWithArg<EditingTarget> executeIn) { file = fileIn; fileType = fileTypeIn; executeOnSuccess = executeIn; } public final FileSystemItem file; public final TextFileType fileType; public final CommandWithArg<EditingTarget> executeOnSuccess; } private class StatFileEntry { public StatFileEntry(FileSystemItem fileIn, CommandWithArg<FileSystemItem> actionIn) { file = fileIn; action = actionIn; } public final FileSystemItem file; public final CommandWithArg<FileSystemItem> action; } final Queue<StatFileEntry> statQueue_ = new LinkedList<StatFileEntry>(); final Queue<OpenFileEntry> openFileQueue_ = new LinkedList<OpenFileEntry>(); ArrayList<EditingTarget> editors_ = new ArrayList<EditingTarget>(); ArrayList<Integer> tabOrder_ = new ArrayList<Integer>(); private EditingTarget activeEditor_; private final Commands commands_; private final Display view_; private final SourceServerOperations server_; private final EditingTargetSource editingTargetSource_; private final FileTypeRegistry fileTypeRegistry_; private final GlobalDisplay globalDisplay_; private final WorkbenchContext workbenchContext_; private final FileDialogs fileDialogs_; private final RemoteFileSystemContext fileContext_; private final TextEditingTargetRMarkdownHelper rmarkdown_; private final EventBus events_; private final Session session_; private final Synctex synctex_; private final Provider<FileMRUList> pMruList_; private final UIPrefs uiPrefs_; private final ConsoleEditorProvider consoleEditorProvider_; private final RnwWeaveRegistry rnwWeaveRegistry_; private HashSet<AppCommand> activeCommands_ = new HashSet<AppCommand>(); private final HashSet<AppCommand> dynamicCommands_; private final SourceNavigationHistory sourceNavigationHistory_ = new SourceNavigationHistory(30); private final SourceVimCommands vimCommands_; private boolean suspendSourceNavigationAdding_; private boolean suspendDocumentClose_ = false; private static final String MODULE_SOURCE = "source-pane"; private static final String KEY_ACTIVETAB = "activeTab"; private boolean initialized_; private Timer debugSelectionTimer_ = null; private final SourceWindowManager windowManager_; // If positive, a new tab is about to be created private int newTabPending_; private DependencyManager dependencyManager_; public final static int TYPE_FILE_BACKED = 0; public final static int TYPE_UNTITLED = 1; public final static int OPEN_INTERACTIVE = 0; public final static int OPEN_REPLAY = 1; }