Java tutorial
/* * Source.java * * Copyright (C) 2009-15 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.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.*; 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.Timer; 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.Desktop; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.common.FileDialogs; 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.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.OpenSourceFileEvent.NavigationMethod; import org.rstudio.studio.client.common.filetypes.events.OpenSourceFileHandler; import org.rstudio.studio.client.common.rnw.RnwWeave; import org.rstudio.studio.client.common.rnw.RnwWeaveRegistry; import org.rstudio.studio.client.common.synctex.Synctex; import org.rstudio.studio.client.common.synctex.events.SynctexStatusChangedEvent; import org.rstudio.studio.client.rmarkdown.model.RMarkdownContext; 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.FileMRUList; import org.rstudio.studio.client.workbench.WorkbenchContext; import org.rstudio.studio.client.workbench.commands.Commands; 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.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.data.events.ViewDataEvent; import org.rstudio.studio.client.workbench.views.data.events.ViewDataHandler; import org.rstudio.studio.client.workbench.views.output.find.events.FindInFilesEvent; 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.profiler.ProfilerEditingTarget; import org.rstudio.studio.client.workbench.views.source.editors.profiler.model.ProfilerContents; 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.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.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.RdShellResult; import org.rstudio.studio.client.workbench.views.source.model.SourceDocument; 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; 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 { public interface Display extends IsWidget, HasTabClosingHandlers, HasTabCloseHandlers, HasTabClosedHandlers, HasTabReorderHandlers, HasBeforeSelectionHandlers<Integer>, HasSelectionHandlers<Integer> { void addTab(Widget widget, ImageResource icon, String name, String tooltip, 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 setDirty(Widget widget, boolean dirty); void manageChevronVisibility(); void showOverflowPopup(); 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, RnwWeaveRegistry rnwWeaveRegistry) { 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; rnwWeaveRegistry_ = rnwWeaveRegistry; 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.executePreviousChunks()); dynamicCommands_.add(commands.executeCurrentChunk()); dynamicCommands_.add(commands.executeNextChunk()); dynamicCommands_.add(commands.sourceActiveDocument()); dynamicCommands_.add(commands.sourceActiveDocumentWithEcho()); dynamicCommands_.add(commands.markdownHelp()); dynamicCommands_.add(commands.usingRMarkdownHelp()); dynamicCommands_.add(commands.authoringRPresentationsHelp()); 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.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.rcppHelp()); 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.insertRoxygenSkeleton()); 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(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); } }); } }); 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()); } }); events.addHandler(SourceFileSavedEvent.TYPE, new SourceFileSavedHandler() { public void onSourceFileSaved(SourceFileSavedEvent event) { pMruList_.get().add(event.getPath()); } }); 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(); } }); restoreDocuments(session); new IntStateValue(MODULE_SOURCE, KEY_ACTIVETAB, 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(); } }; uiPrefs_.verticallyAlignArgumentIndent().bind(new CommandWithArg<Boolean>() { @Override public void execute(Boolean arg) { AceEditorNative.setVerticallyAlignFunctionArgs(arg); } }); // adjust shortcuts when vim mode changes uiPrefs_.useVimMode().bind(new CommandWithArg<Boolean>() { @Override public void execute(Boolean arg) { ShortcutManager.INSTANCE .setEditorMode(arg ? KeyboardShortcut.MODE_VIM : KeyboardShortcut.MODE_NONE); } }); 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 docs openProjectDocs(session); // add vim commands initVimCommands(); } private void initVimCommands() { vimCommands_.save(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); } 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++) { addTab(docs.get(i)); } } 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) { 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); } }); } public void onShowData(ShowDataEvent event) { 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); } }); } @Handler public void onShowProfiler() { // first try to activate existing for (int idx = 0; idx < editors_.size(); idx++) { String path = editors_.get(idx).getPath(); if (ProfilerEditingTarget.PATH.equals(path)) { ensureVisible(false); view_.selectTab(idx); return; } } // create new profiler ensureVisible(true); server_.newDocument(FileTypeRegistry.PROFILER.getTypeId(), null, (JsObject) ProfilerContents.createDefault().cast(), new SimpleRequestCallback<SourceDocument>("Show Profiler") { @Override public void onResponseReceived(SourceDocument response) { addTab(response); } }); } @Handler public void onNewSourceDoc() { newDoc(FileTypeRegistry.R, null); } @Handler public void onNewTextDoc() { newDoc(FileTypeRegistry.TEXT, null); } @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(); } @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() { 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_.withRMarkdownPackage("Creating R Markdown documents", false, new CommandWithArg<RMarkdownContext>() { @Override public void execute(RMarkdownContext context) { new NewRMarkdownDialog(context, workbenchContext_, uiPrefs_.documentAuthor().getGlobalValue(), 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); } } }).showModal(); } }); } 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); 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() { if (activeEditor_ == null) { newDoc(FileTypeRegistry.R, new ResultCallback<EditingTarget, ServerError>() { @Override public void onSuccess(EditingTarget target) { activeEditor_ = target; doActivateSource(); } }); } else { doActivateSource(); } } private void doActivateSource() { ensureVisible(false); if (activeEditor_ != null) { activeEditor_.focus(); activeEditor_.ensureCursorVisible(); } } @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() { if (view_.getTabCount() == 0) return; ensureVisible(false); int index = getPhysicalTabIndex(); if (index >= 1) setPhysicalTabIndex(index - 1); } @Handler public void onNextTab() { if (view_.getTabCount() == 0) return; ensureVisible(false); int index = getPhysicalTabIndex(); if (index < view_.getTabCount() - 1) setPhysicalTabIndex(index + 1); } @Handler public void onLastTab() { if (view_.getTabCount() == 0) return; ensureVisible(false); if (view_.getTabCount() > 0) setPhysicalTabIndex(view_.getTabCount() - 1); } @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); } public void closeAllSourceDocs(String caption, Command onCompleted) { // collect up a list of dirty documents ArrayList<EditingTarget> dirtyTargets = new ArrayList<EditingTarget>(); for (EditingTarget target : editors_) 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) { view_.closeTab(target.asWidget(), false, continuation); } }); } }; // save targets saveEditingTargetsWithPrompt(caption, dirtyTargets, CommandUtil.join(closeAllTabsCommand, onCompleted), null); } private boolean isUnsavedFileBackedTarget(EditingTarget target) { return target.dirtyState().getValue() && (target.getPath() != null); } public ArrayList<UnsavedChangesTarget> getUnsavedChanges() { ArrayList<UnsavedChangesTarget> targets = new ArrayList<UnsavedChangesTarget>(); for (EditingTarget target : editors_) if (isUnsavedFileBackedTarget(target)) targets.add(target); return targets; } public void saveAllUnsaved(Command onCompleted) { saveChanges(getUnsavedChanges(), onCompleted); } public void saveWithPrompt(UnsavedChangesTarget target, Command onCompleted, Command onCancelled) { EditingTarget editingTarget = getEditingTargetForId(target.getId()); if (editingTarget != null) editingTarget.saveWithPrompt(onCompleted, onCancelled); } public void handleUnsavedChangesBeforeExit(ArrayList<UnsavedChangesTarget> saveTargets, final Command onCompleted) { // first handle saves, then revert unsaved, then callback on completed saveChanges(saveTargets, new Command() { @Override public void execute() { // revert unsaved revertUnsavedTargets(onCompleted); } }); } 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 (isUnsavedFileBackedTarget(target)) 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 onOpenSourceFile(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(), NavigationMethod.HighlightLine, 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 NavigationMethod navMethod, final boolean forceHighlightMode) { final boolean isDebugNavigation = navMethod == NavigationMethod.DebugStep || navMethod == NavigationMethod.DebugEnd; 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 != NavigationMethod.DebugEnd) 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 == NavigationMethod.DebugStep) { target.highlightDebugLocation(srcPosition, srcEndPosition, true); } else if (navMethod == NavigationMethod.DebugEnd) { target.endDebugHighlighting(); } else { // force highlight mode if requested if (forceHighlightMode) target.forceLineHighlighting(); // now navigate to the new position boolean highlight = navMethod == NavigationMethod.HighlightLine && !uiPrefs_.highlightSelectedLine().getValue(); target.navigateToPosition(srcPosition, false, highlight); } } }); } }; 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 == NavigationMethod.DebugEnd) { 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 == NavigationMethod.DebugEnd) return; } // Warning: event.getFile() can be null (e.g. new Sweave document) if (file != null && file.getLength() < 0) { // If the file has no size info, stat the file from the server. This // is to prevent us from opening large files accidentally. server_.stat(file.getPath(), new ServerRequestCallback<FileSystemItem>() { @Override public void onResponseReceived(FileSystemItem response) { action.execute(response); } @Override public void onError(ServerError error) { // 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. action.execute(file); } }); } else { action.execute(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) { openFile(file, fileType, new ResultCallback<EditingTarget, ServerError>() { @Override public void onSuccess(EditingTarget target) { if (executeOnSuccess != null) 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); } }); } // 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 (file == null) { newDoc(fileType, resultCallback); return; } 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; } } 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); if (resultCallback != null) resultCallback.onSuccess(target); } }); } private EditingTarget addTab(SourceDocument doc) { final EditingTarget target = editingTargetSource_.getEditingTarget(doc, fileContext_, new Provider<String>() { public String get() { return getNextDefaultName(); } }); final Widget widget = target.asWidget(); editors_.add(target); view_.addTab(widget, target.getIcon(), target.getName().getValue(), target.getTabTooltip(), // used as tooltip, if non-null 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); } }); return target; } private String getNextDefaultName() { int max = 0; for (EditingTarget target : editors_) { String name = target.getName().getValue(); max = Math.max(max, getUntitledNum(name)); } return "Untitled" + (max + 1); } private native final int getUntitledNum(String name) /*-{ var match = /^Untitled([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; } } } } } public void onTabClosed(TabClosedEvent event) { EditingTarget target = editors_.remove(event.getTabIndex()); tabOrder_.remove(new Integer(event.getTabIndex())); for (int i = 0; i < tabOrder_.size(); i++) { if (tabOrder_.get(i) > event.getTabIndex()) { tabOrder_.set(i, tabOrder_.get(i) - 1); } } target.onDismiss(); if (activeEditor_ == target) { activeEditor_.onDeactivate(); activeEditor_ = null; } server_.closeDocument(target.getId(), new VoidServerRequestCallback()); manageCommands(); fireDocTabsChanged(); if (view_.getTabCount() == 0) { sourceNavigationHistory_.clear(); events_.fireEvent(new LastSourceDocClosedEvent()); } } @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()); 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(); // 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()) { 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(); activeCommands_ = newCommands; assert verifyNoUnsupportedCommands( newCommands) : "Unsupported commands detected (please add to Source.dynamicCommands_)"; } 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 shinyCommandsAvailable = SessionUtils.showPublishUi(session_, uiPrefs_) && (activeEditor_ != null) && (activeEditor_.getPath() != null) && ((activeEditor_.getExtendedFileType() == "shiny")); commands_.rsconnectDeploy().setVisible(shinyCommandsAvailable); commands_.rsconnectConfigure().setVisible(shinyCommandsAvailable); } private void manageRMarkdownCommands() { boolean rmdCommandsAvailable = session_.getSessionInfo().getRMarkdownPackageAvailable() && (activeEditor_ != null) && activeEditor_.getExtendedFileType() == "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) { 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().equals(CodeBrowserEditingTarget.PATH)) { activateCodeBrowser(new SourceNavigationResultCallback<CodeBrowserEditingTarget>( navigation.getPosition(), retryCommand)); } // check for file path navigation else if ((navigation.getPath() != null) && !navigation.getPath().startsWith(DataItem.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 (event.getDebugPosition() != null) { setPendingDebugSelection(); } activateCodeBrowser(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) { int codeBrowserTabIndex = indexOfCodeBrowserTab(); if (codeBrowserTabIndex >= 0) { view_.closeTab(codeBrowserTabIndex, false); return; } } @Override public void onCodeBrowserHighlight(final CodeBrowserHighlightEvent event) { // no need to highlight if we don't have a code browser tab to highlight if (indexOfCodeBrowserTab() < 0) return; setPendingDebugSelection(); activateCodeBrowser(new ResultCallback<CodeBrowserEditingTarget, ServerError>() { @Override public void onSuccess(CodeBrowserEditingTarget target) { highlightDebugBrowserPosition(target, event.getDebugPosition(), true); } }); } 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); } // returns the index of the tab currently containing the code browser, or // -1 if the code browser tab isn't currently open; private int indexOfCodeBrowserTab() { // see if there is an existing target to use for (int idx = 0; idx < editors_.size(); idx++) { String path = editors_.get(idx).getPath(); if (CodeBrowserEditingTarget.PATH.equals(path)) { return idx; } } return -1; } private void activateCodeBrowser(final ResultCallback<CodeBrowserEditingTarget, ServerError> callback) { int codeBrowserTabIndex = indexOfCodeBrowserTab(); if (codeBrowserTabIndex >= 0) { ensureVisible(false); view_.selectTab(codeBrowserTabIndex); // callback callback.onSuccess((CodeBrowserEditingTarget) editors_.get(codeBrowserTabIndex)); // satisfied request return; } // create a new one newDoc(FileTypeRegistry.CODEBROWSER, new ResultCallback<EditingTarget, ServerError>() { @Override public void onSuccess(EditingTarget arg) { 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_; } 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 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 static final String MODULE_SOURCE = "source-pane"; private static final String KEY_ACTIVETAB = "activeTab"; private boolean initialized_; private Timer debugSelectionTimer_ = null; // If positive, a new tab is about to be created private int newTabPending_; }