/* * Projects.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.projects; import org.rstudio.core.client.Debug; import org.rstudio.core.client.SerializedCommand; import org.rstudio.core.client.SerializedCommandQueue; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.command.Handler; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.regex.Pattern; import org.rstudio.core.client.widget.MessageDialog; import org.rstudio.core.client.widget.Operation; import org.rstudio.core.client.widget.ProgressIndicator; import org.rstudio.core.client.widget.ProgressOperationWithInput; import org.rstudio.studio.client.application.ApplicationQuit; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.application.model.ApplicationServerOperations; import org.rstudio.studio.client.application.model.RVersionSpec; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.SimpleRequestCallback; import org.rstudio.studio.client.common.console.ConsoleProcess; import org.rstudio.studio.client.common.console.ProcessExitEvent; import org.rstudio.studio.client.common.vcs.GitServerOperations; import org.rstudio.studio.client.common.vcs.VCSConstants; import org.rstudio.studio.client.common.vcs.VcsCloneOptions; import org.rstudio.studio.client.packrat.model.PackratServerOperations; import org.rstudio.studio.client.projects.events.OpenProjectErrorEvent; import org.rstudio.studio.client.projects.events.OpenProjectErrorHandler; import org.rstudio.studio.client.projects.events.OpenProjectFileEvent; import org.rstudio.studio.client.projects.events.OpenProjectFileHandler; import org.rstudio.studio.client.projects.events.OpenProjectNewWindowEvent; import org.rstudio.studio.client.projects.events.OpenProjectNewWindowHandler; import org.rstudio.studio.client.projects.events.SwitchToProjectEvent; import org.rstudio.studio.client.projects.events.SwitchToProjectHandler; import org.rstudio.studio.client.projects.events.NewProjectEvent; import org.rstudio.studio.client.projects.events.OpenProjectEvent; import org.rstudio.studio.client.projects.model.NewProjectContext; import org.rstudio.studio.client.projects.model.NewProjectInput; import org.rstudio.studio.client.projects.model.NewProjectResult; import org.rstudio.studio.client.projects.model.OpenProjectParams; import org.rstudio.studio.client.projects.model.ProjectsServerOperations; import org.rstudio.studio.client.projects.model.RProjectOptions; import org.rstudio.studio.client.projects.ui.newproject.NewProjectWizard; import org.rstudio.studio.client.projects.ui.prefs.ProjectPreferencesDialog; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.server.Void; import org.rstudio.studio.client.server.VoidServerRequestCallback; import org.rstudio.studio.client.server.remote.RResult; import org.rstudio.studio.client.workbench.WorkbenchContext; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.events.SessionInitEvent; import org.rstudio.studio.client.workbench.events.SessionInitHandler; 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.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.views.vcs.common.ConsoleProgressDialog; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.Command; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @Singleton public class Projects implements OpenProjectFileHandler, SwitchToProjectHandler, OpenProjectErrorHandler, OpenProjectNewWindowHandler, NewProjectEvent.Handler, OpenProjectEvent.Handler { public interface Binder extends CommandBinder<Commands, Projects> {} @Inject public Projects(GlobalDisplay globalDisplay, final Session session, Provider<ProjectMRUList> pMRUList, SharedProject sharedProject, RemoteFileSystemContext fsContext, ApplicationQuit applicationQuit, ProjectsServerOperations projServer, PackratServerOperations packratServer, ApplicationServerOperations appServer, GitServerOperations gitServer, EventBus eventBus, Binder binder, final Commands commands, ProjectOpener opener, Provider<ProjectPreferencesDialog> pPrefDialog, Provider<WorkbenchContext> pWorkbenchContext, Provider<UIPrefs> pUIPrefs) { globalDisplay_ = globalDisplay; eventBus_ = eventBus; pMRUList_ = pMRUList; applicationQuit_ = applicationQuit; projServer_ = projServer; packratServer_ = packratServer; appServer_ = appServer; gitServer_ = gitServer; fsContext_ = fsContext; session_ = session; pWorkbenchContext_ = pWorkbenchContext; pPrefDialog_ = pPrefDialog; pUIPrefs_ = pUIPrefs; opener_ = opener; binder.bind(commands, this); eventBus.addHandler(OpenProjectErrorEvent.TYPE, this); eventBus.addHandler(SwitchToProjectEvent.TYPE, this); eventBus.addHandler(OpenProjectFileEvent.TYPE, this); eventBus.addHandler(OpenProjectNewWindowEvent.TYPE, this); eventBus.addHandler(NewProjectEvent.TYPE, this); eventBus.addHandler(OpenProjectEvent.TYPE, this); eventBus.addHandler(SessionInitEvent.TYPE, new SessionInitHandler() { public void onSessionInit(SessionInitEvent sie) { SessionInfo sessionInfo = session.getSessionInfo(); // ensure mru is initialized ProjectMRUList mruList = pMRUList_.get(); // enable/disable commands String activeProjectFile = sessionInfo.getActiveProjectFile(); boolean hasProject = activeProjectFile != null; commands.closeProject().setEnabled(hasProject); commands.projectOptions().setEnabled(hasProject); if (!hasProject) { commands.setWorkingDirToProjectDir().remove(); commands.showDiagnosticsProject().remove(); } boolean enableProjectSharing = hasProject && sessionInfo.projectSupportsSharing(); commands.shareProject().setEnabled(enableProjectSharing); commands.shareProject().setVisible(enableProjectSharing); // remove version control commands if necessary if (!sessionInfo.isVcsEnabled()) { commands.activateVcs().remove(); commands.layoutZoomVcs().remove(); commands.vcsCommit().remove(); commands.vcsShowHistory().remove(); commands.vcsPull().remove(); commands.vcsPush().remove(); commands.vcsCleanup().remove(); } else { commands.activateVcs().setMenuLabel( "Show _" + sessionInfo.getVcsName()); commands.layoutZoomVcs().setMenuLabel( "Zoom _" + sessionInfo.getVcsName()); // customize for svn if necessary if (sessionInfo.getVcsName().equals(VCSConstants.SVN_ID)) { commands.vcsPush().remove(); commands.vcsPull().setButtonLabel("Update"); commands.vcsPull().setMenuLabel("_Update"); } // customize for git if necessary if (sessionInfo.getVcsName().equals(VCSConstants.GIT_ID)) { commands.vcsCleanup().remove(); } } // disable the open project in new window if necessary if (!Desktop.isDesktop() || !sessionInfo.getMultiSession()) commands.openProjectInNewWindow().remove(); // maintain mru if (hasProject) mruList.add(activeProjectFile); } }); } @Handler public void onClearRecentProjects() { // Clear the contents of the most rencently used list of projects pMRUList_.get().clear(); } @Handler public void onNewProject() { handleNewProject(false, true); } private void handleNewProject(boolean forceSaveAll, final boolean allowOpenInNewWindow) { // first resolve the quit context (potentially saving edited documents // and determining whether to save the R environment on exit) applicationQuit_.prepareForQuit("Save Current Workspace", forceSaveAll, new ApplicationQuit.QuitContext() { @Override public void onReadyToQuit(final boolean saveChanges) { projServer_.getNewProjectContext( new SimpleRequestCallback<NewProjectContext>() { @Override public void onResponseReceived(NewProjectContext context) { NewProjectWizard wiz = new NewProjectWizard( session_.getSessionInfo(), pUIPrefs_.get(), pWorkbenchContext_.get(), new NewProjectInput( FileSystemItem.createDir( pUIPrefs_.get().defaultProjectLocation().getValue()), context ), allowOpenInNewWindow, new ProgressOperationWithInput<NewProjectResult>() { @Override public void execute(NewProjectResult newProject, ProgressIndicator indicator) { indicator.onCompleted(); createNewProject(newProject, saveChanges); } }); wiz.showModal(); } }); } }); } @Override public void onNewProjectEvent(NewProjectEvent event) { handleNewProject(event.getForceSaveAll(), event.getAllowOpenInNewWindow()); } private void createNewProject(final NewProjectResult newProject, final boolean saveChanges) { // This gets a little crazy. We have several pieces of asynchronous logic // that each may or may not need to be executed, depending on the type // of project being created and on whether the previous pieces of logic // succeed. Plus we have this ProgressIndicator that needs to be fed // properly. final ProgressIndicator indicator = globalDisplay_.getProgressIndicator( "Error Creating Project"); // Here's the command queue that will hold the various operations. final SerializedCommandQueue createProjectCmds = new SerializedCommandQueue(); // WARNING: When calling addCommand, BE SURE TO PASS FALSE as the second // argument, to delay running of the commands until they are all // scheduled. // First, attempt to update the default project location pref createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { UIPrefs uiPrefs = pUIPrefs_.get(); // update default project location pref if necessary if ((newProject.getNewDefaultProjectLocation() != null) || (newProject.getCreateGitRepo() != uiPrefs.newProjGitInit().getValue())) { indicator.onProgress("Saving defaults..."); if (newProject.getNewDefaultProjectLocation() != null) { uiPrefs.defaultProjectLocation().setGlobalValue( newProject.getNewDefaultProjectLocation()); } if (newProject.getCreateGitRepo() != uiPrefs.newProjGitInit().getValue()) { uiPrefs.newProjGitInit().setGlobalValue( newProject.getCreateGitRepo()); } if (newProject.getUsePackrat() != uiPrefs.newProjUsePackrat().getValue()) { uiPrefs.newProjUsePackrat().setGlobalValue( newProject.getUsePackrat()); } // call the server -- in all cases continue on with // creating the project (swallow errors updating the pref) projServer_.setUiPrefs( session_.getSessionInfo().getUiPrefs(), new VoidServerRequestCallback(indicator) { @Override public void onResponseReceived(Void response) { continuation.execute(); } @Override public void onError(ServerError error) { super.onError(error); continuation.execute(); } }); } else { continuation.execute(); } } }, false); // Next, if necessary, clone a repo if (newProject.getVcsCloneOptions() != null) { createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { VcsCloneOptions cloneOptions = newProject.getVcsCloneOptions(); if (cloneOptions.getVcsName().equals((VCSConstants.GIT_ID))) indicator.onProgress("Cloning Git repository..."); else indicator.onProgress("Checking out SVN repository..."); gitServer_.vcsClone( cloneOptions, new ServerRequestCallback<ConsoleProcess>() { @Override public void onResponseReceived(ConsoleProcess proc) { final ConsoleProgressDialog consoleProgressDialog = new ConsoleProgressDialog(proc, gitServer_); consoleProgressDialog.showModal(); proc.addProcessExitHandler(new ProcessExitEvent.Handler() { @Override public void onProcessExit(ProcessExitEvent event) { if (event.getExitCode() == 0) { consoleProgressDialog.hide(); continuation.execute(); } else { indicator.onCompleted(); } } }); } @Override public void onError(ServerError error) { Debug.logError(error); indicator.onError(error.getUserMessage()); } }); } }, false); } // Next, create the project itself -- depending on the type, this // could involve creating an R package, or Shiny application, and so on. createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { // Validate the package name if we're creating a package if (newProject.getNewPackageOptions() != null) { final String packageName = newProject.getNewPackageOptions().getPackageName(); if (!PACKAGE_NAME_PATTERN.test(packageName)) { indicator.onError( "Invalid package name '" + packageName + "': " + "package names must start with a letter, and contain " + "only letters and numbers." ); return; } } indicator.onProgress("Creating project..."); if (newProject.getNewPackageOptions() == null) { projServer_.createProject( newProject.getProjectFile(), newProject.getNewPackageOptions(), newProject.getNewShinyAppOptions(), newProject.getProjectTemplateOptions(), new VoidServerRequestCallback(indicator) { @Override public void onSuccess() { continuation.execute(); } }); } else { String projectFile = newProject.getProjectFile(); String packageDirectory = projectFile.substring(0, projectFile.lastIndexOf('/')); projServer_.packageSkeleton( newProject.getNewPackageOptions().getPackageName(), packageDirectory, newProject.getNewPackageOptions().getCodeFiles(), newProject.getNewPackageOptions().getUsingRcpp(), new ServerRequestCallback<RResult<Void>>() { @Override public void onResponseReceived(RResult<Void> response) { if (response.failed()) indicator.onError(response.errorMessage()); else continuation.execute(); } @Override public void onError(ServerError error) { Debug.logError(error); indicator.onError(error.getUserMessage()); } }); } } }, false); // Next, initialize a git repo if requested if (newProject.getCreateGitRepo()) { createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { indicator.onProgress("Initializing git repository..."); String projDir = FileSystemItem.createFile( newProject.getProjectFile()).getParentPathString(); gitServer_.gitInitRepo( projDir, new VoidServerRequestCallback(indicator) { @Override public void onSuccess() { continuation.execute(); } @Override public void onFailure() { continuation.execute(); } }); } }, false); } // Generate a new packrat project if (newProject.getUsePackrat()) { createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { indicator.onProgress("Initializing packrat project..."); String projDir = FileSystemItem.createFile( newProject.getProjectFile() ).getParentPathString(); packratServer_.packratBootstrap( projDir, false, new VoidServerRequestCallback(indicator) { @Override public void onSuccess() { continuation.execute(); } }); } }, false); } if (newProject.getOpenInNewWindow()) { createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(final Command continuation) { FileSystemItem project = FileSystemItem.createFile( newProject.getProjectFile()); if (Desktop.isDesktop()) { Desktop.getFrame().openProjectInNewWindow(project.getPath()); continuation.execute(); } else { indicator.onProgress("Preparing to open project..."); serverOpenProjectInNewWindow(project, newProject.getRVersion(), continuation); } } }, false); } // If we get here, dismiss the progress indicator createProjectCmds.addCommand(new SerializedCommand() { @Override public void onExecute(Command continuation) { indicator.onCompleted(); if (!newProject.getOpenInNewWindow()) { applicationQuit_.performQuit( saveChanges, newProject.getProjectFile(), newProject.getRVersion()); } continuation.execute(); } }, false); // Now set it all in motion! createProjectCmds.run(); } @Handler public void onOpenProject() { showOpenProjectDialog(ProjectOpener.PROJECT_TYPE_FILE); } @Override public void onOpenProjectEvent(OpenProjectEvent event) { showOpenProjectDialog(ProjectOpener.PROJECT_TYPE_FILE, event.getForceSaveAll(), event.getAllowOpenInNewWindow()); } @Handler public void onOpenProjectInNewWindow() { showOpenProjectDialog(ProjectOpener.PROJECT_TYPE_FILE, false, new ProgressOperationWithInput<OpenProjectParams>() { @Override public void execute(final OpenProjectParams input, ProgressIndicator indicator) { indicator.onCompleted(); if (input == null) return; eventBus_.fireEvent( new OpenProjectNewWindowEvent( input.getProjectFile().getPath(), input.getRVersion())); } }); } @Handler public void onOpenSharedProject() { showOpenProjectDialog(ProjectOpener.PROJECT_TYPE_SHARED); } @Override public void onOpenProjectNewWindow(OpenProjectNewWindowEvent event) { // call the desktop to open the project (since it is // a conventional foreground gui application it has // less chance of running afowl of desktop app creation // & activation restrictions) FileSystemItem project = FileSystemItem.createFile(event.getProject()); if (Desktop.isDesktop()) Desktop.getFrame().openProjectInNewWindow(project.getPath()); else serverOpenProjectInNewWindow(project, event.getRVersion(), null); } @Handler public void onCloseProject() { // first resolve the quit context (potentially saving edited documents // and determining whether to save the R environment on exit) applicationQuit_.prepareForQuit("Close Project", new ApplicationQuit.QuitContext() { public void onReadyToQuit(final boolean saveChanges) { applicationQuit_.performQuit(saveChanges, NONE); }}); } @Handler public void onProjectOptions() { showProjectOptions(ProjectPreferencesDialog.GENERAL); } @Handler public void onProjectSweaveOptions() { showProjectOptions(ProjectPreferencesDialog.SWEAVE); } @Handler public void onBuildToolsProjectSetup() { // check whether there is a project active if (!hasActiveProject()) { globalDisplay_.showMessage( MessageDialog.INFO, "No Active Project", "Build tools can only be configured from within an " + "RStudio project."); } else { showProjectOptions(ProjectPreferencesDialog.BUILD); } } @Handler public void onVersionControlProjectSetup() { // check whether there is a project active if (!hasActiveProject()) { globalDisplay_.showMessage( MessageDialog.INFO, "No Active Project", "Version control features can only be accessed from within an " + "RStudio project. Note that if you have an existing directory " + "under version control you can associate an RStudio project " + "with that directory using the New Project dialog."); } else { showProjectOptions(ProjectPreferencesDialog.VCS); } } @Handler public void onPackratBootstrap() { showProjectOptions(ProjectPreferencesDialog.PACKRAT); } @Handler public void onPackratOptions() { showProjectOptions(ProjectPreferencesDialog.PACKRAT); } public void showProjectOptions(final int initialPane) { final ProgressIndicator indicator = globalDisplay_.getProgressIndicator( "Error Reading Options"); indicator.onProgress("Reading options..."); projServer_.readProjectOptions(new SimpleRequestCallback<RProjectOptions>() { @Override public void onResponseReceived(RProjectOptions options) { indicator.onCompleted(); ProjectPreferencesDialog dlg = pPrefDialog_.get(); dlg.initialize(options); dlg.activatePane(initialPane); dlg.showModal(); }}); } @Override public void onOpenProjectFile(final OpenProjectFileEvent event) { // project options for current project FileSystemItem projFile = event.getFile(); if (projFile.getPath().equals( session_.getSessionInfo().getActiveProjectFile())) { onProjectOptions(); return; } // prompt to confirm String projectPath = projFile.getParentPathString(); globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_QUESTION, "Confirm Open Project", "Do you want to open the project " + projectPath + "?", new Operation() { public void execute() { switchToProject(event.getFile().getPath()); } }, true); } @Override public void onSwitchToProject(final SwitchToProjectEvent event) { switchToProject(event.getProject(), event.getForceSaveAll()); } @Override public void onOpenProjectError(OpenProjectErrorEvent event) { // show error dialog String msg = "Project '" + event.getProject() + "' " + "could not be opened: " + event.getMessage(); globalDisplay_.showErrorMessage("Error Opening Project", msg); // remove from mru list pMRUList_.get().remove(event.getProject()); } private boolean hasActiveProject() { return session_.getSessionInfo().getActiveProjectFile() != null; } private void switchToProject(String projectFilePath) { switchToProject(projectFilePath, false); } private void switchToProject(final String projectFilePath, final boolean forceSaveAll) { // validate that the switch will actually work projServer_.validateProjectPath( projectFilePath, new SimpleRequestCallback<Boolean>() { @Override public void onResponseReceived(Boolean valid) { if (valid) { applicationQuit_.prepareForQuit("Switch Projects", forceSaveAll, new ApplicationQuit.QuitContext() { public void onReadyToQuit(final boolean saveChanges) { applicationQuit_.performQuit(saveChanges, projectFilePath); } }); } else { // show error dialog String msg = "Project '" + projectFilePath + "' " + "does not exist (it has been moved or deleted)"; globalDisplay_.showErrorMessage("Error Opening Project", msg); // remove from mru list pMRUList_.get().remove(projectFilePath); } } }); } private void showOpenProjectDialog( int defaultType, boolean allowOpenInNewWindow, ProgressOperationWithInput<OpenProjectParams> onCompleted) { opener_.showOpenProjectDialog(fsContext_, projServer_, "~", defaultType, allowOpenInNewWindow, onCompleted); } @Handler public void onShowDiagnosticsProject() { final ProgressIndicator indicator = globalDisplay_.getProgressIndicator("Lint"); indicator.onProgress("Analyzing project sources..."); projServer_.analyzeProject(new ServerRequestCallback<Void>() { @Override public void onResponseReceived(Void response) { indicator.onCompleted(); } @Override public void onError(ServerError error) { Debug.logError(error); indicator.onCompleted(); } }); } private void serverOpenProjectInNewWindow(FileSystemItem project, RVersionSpec rVersion, final Command onSuccess) { appServer_.getNewSessionUrl( GWT.getHostPageBaseURL(), true, project.getParentPathString(), rVersion, new SimpleRequestCallback<String>() { @Override public void onResponseReceived(String url) { if (onSuccess != null) onSuccess.execute(); globalDisplay_.openWindow(url); } }); } private void showOpenProjectDialog(final int projectType) { showOpenProjectDialog(projectType, false, true); } private void showOpenProjectDialog(final int projectType, boolean forceSaveAll, final boolean allowOpenInNewWindow) { // first resolve the quit context (potentially saving edited documents // and determining whether to save the R environment on exit) applicationQuit_.prepareForQuit("Switch Projects", forceSaveAll, new ApplicationQuit.QuitContext() { public void onReadyToQuit(final boolean saveChanges) { showOpenProjectDialog(projectType, allowOpenInNewWindow, new ProgressOperationWithInput<OpenProjectParams>() { @Override public void execute(final OpenProjectParams input, ProgressIndicator indicator) { indicator.onCompleted(); if (input == null || input.getProjectFile() == null) return; if (input.inNewSession()) { // open new window if requested eventBus_.fireEvent( new OpenProjectNewWindowEvent( input.getProjectFile().getPath(), input.getRVersion())); } else { // perform quit applicationQuit_.performQuit(saveChanges, input.getProjectFile().getPath()); } } }); } }); } private final Provider<ProjectMRUList> pMRUList_; private final ApplicationQuit applicationQuit_; private final ProjectsServerOperations projServer_; private final PackratServerOperations packratServer_; private final ApplicationServerOperations appServer_; private final GitServerOperations gitServer_; private final RemoteFileSystemContext fsContext_; private final GlobalDisplay globalDisplay_; private final EventBus eventBus_; private final Session session_; private final Provider<WorkbenchContext> pWorkbenchContext_; private final Provider<ProjectPreferencesDialog> pPrefDialog_; private final Provider<UIPrefs> pUIPrefs_; private final ProjectOpener opener_; public static final String NONE = "none"; public static final Pattern PACKAGE_NAME_PATTERN = Pattern.create("^[a-zA-Z][a-zA-Z0-9.]*$", ""); }