Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@
description="Configure JHelper"
icon="/name/admitriev/jhelper/icons/settings.png"
/>
<action id="name.admitriev.jhelper.actions.UnarchiveTaskAction"
class="name.admitriev.jhelper.actions.UnarchiveTaskAction"
text="Restore Tasks"
icon="/name/admitriev/jhelper/icons/unarchive.png"
description="Restore directory with tasks or a single task back to the former place">
<add-to-group group-id="ProjectViewPopupMenu" anchor="first"/>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is possible to add it to menu only for files for which it make sense (e.g cpp&xml & directories)? and ideally only inside project that work with JHelper

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but it puts a big overhead if we recursively check all the directories and inner files on every menu popup opening. To optimize, we can mark parent directories after the creation of a new JHelper config or a newly archived file, but that looks a bit complicated for now :) What do you think?

As a workaround, we can remove those buttons from the popup menu completely (a user can add them there manually anyway) and have them available via Ctrl+Shift+A

P.S. Thanks a lot for the very quick reply and for the awesome JHeleper plugin!!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably would not check recursively, but I'd only add it for cpp&xml files and for directories (if that's possible). Others are guaranteed to not do anything.

Also, I think that adding it to the first position in the menu may be a little to aggressive and we should add it a bit lower.

Workaround you mentioned is always an option, it's what is done with other actions anyway

</action>
<action id="name.admitriev.jhelper.actions.ArchiveTaskAction"
class="name.admitriev.jhelper.actions.ArchiveTaskAction"
text="Archive Tasks"
icon="/name/admitriev/jhelper/icons/archive.png"
description="Archives all tasks in directory recursively or a single task if selected">
<add-to-group group-id="ProjectViewPopupMenu" anchor="first"/>
</action>
<action
id="name.admitriev.jhelper.actions.AddTaskAction"
class="name.admitriev.jhelper.actions.AddTaskAction"
Expand Down
Binary file added resources/name/admitriev/jhelper/icons/archive.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/name/admitriev/jhelper/icons/unarchive.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
186 changes: 186 additions & 0 deletions src/name/admitriev/jhelper/actions/ArchiveTaskAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package name.admitriev.jhelper.actions;

import com.intellij.execution.RunManagerEx;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.impl.RunManagerImpl;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.vfs.*;
import name.admitriev.jhelper.components.Configurator;
import name.admitriev.jhelper.configuration.TaskConfiguration;
import name.admitriev.jhelper.exceptions.NotificationException;
import name.admitriev.jhelper.ui.Notificator;
import net.egork.chelper.util.FileUtilities;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Objects;

public class ArchiveTaskAction extends BaseAction {
@Override
public void performAction(@NotNull AnActionEvent e) {
Project project = e.getProject();
if (project == null) {
throw new NotificationException("No project found", "Are you in any project?");
}
VirtualFile file = e.getDataContext().getData(CommonDataKeys.VIRTUAL_FILE);
if (file == null) {
throw new NotificationException("No task or directory with tasks selected", "To archive a solution or a directory you should select it first");
}
VirtualFile projectBaseDir = ProjectUtil.guessProjectDir(project);
if (projectBaseDir == null) {
throw new NotificationException("Unable to find base directory of project", "Make sure you are not in default project");
}
Configurator configurator = project.getService(Configurator.class);
String archiveDirectory = configurator.getState().getArchiveDirectory();
ApplicationManager.getApplication().runWriteAction(new RecursiveArchiver(project, projectBaseDir, archiveDirectory, file));
}

private static final class RecursiveArchiver implements Runnable {
final Project project;
final VirtualFile projectBaseDir;
final String archiveDirectory;
final VirtualFile selectedFile;
final ArrayList<String> pathToFile;
final String curDate;
int archivedCount = 0;

public RecursiveArchiver(@NotNull Project project, @NotNull VirtualFile projectBaseDir, @NotNull String archiveDirectoryRelativePath, @NotNull VirtualFile selectedFile) {
this.project = project;
this.projectBaseDir = projectBaseDir;
this.archiveDirectory = archiveDirectoryRelativePath;
this.selectedFile = selectedFile;
this.pathToFile = new ArrayList<>();
this.curDate = DateTimeFormatter.ofPattern("dd_MM_yyyy_HH:mm:ss").format(LocalDateTime.now());
}

@Override
public void run() {
try {
VfsUtil.createDirectoryIfMissing(archiveDirectory);
} catch (IOException ex) {
throw new NotificationException("Unable to create archive directory", "Root of archive directory does not exist and failed to be created");
}
VfsUtilCore.visitChildrenRecursively(selectedFile, new VirtualFileVisitor<>() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it work ok if there are symlinks inside the directory? If there's a symlink to somewhere out of project, e.g /, will it be able to wipe all my cpp's ? What about recursive symlinks?

@Override
public boolean visitFile(@NotNull VirtualFile file) {
if (file.isDirectory()) {
pathToFile.add(file.getName());
return true;
}
handleSourceFile(file);
return false;
}

@Override
public void afterChildrenVisited(@NotNull VirtualFile file) {
pathToFile.remove(pathToFile.size() - 1);
}
});
if (archivedCount > 0) {
LocalFileSystem.getInstance().refresh(true); // to notify Vfs about new files created in archive
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we notify fs if something was created even if there was exception in the middle?

Notificator.showNotification(
"All tasks in " + selectedFile.getName() + " were successfully archived",
NotificationType.INFORMATION
);
DeleteTaskAction.selectSomeTaskConfiguration(RunManagerEx.getInstanceEx(project));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange that we are going to different action here. Probably need to extract this soewhere. IDEUtils maybe

}
}

private void handleSourceFile(@NotNull VirtualFile file) {
RunnerAndConfigurationSettings taskConfigurationAndSettings = findJHelperRCForFile(file, project);
if (taskConfigurationAndSettings == null) {
return;
}
TaskConfiguration taskConfiguration = ((TaskConfiguration) taskConfigurationAndSettings.getConfiguration());
VirtualFile directory = getFinalArchiveDirectoryOfFile(file);
saveCodeToArchive(file, directory, taskConfiguration.getClassName() + "_" + curDate + ".cpp");
saveRCToArchive(taskConfiguration, directory, taskConfiguration.getClassName() + "_" + curDate + ".xml");
deleteFileWithCode(file);
deleteRC(taskConfigurationAndSettings);
++archivedCount;
}

private void deleteRC(RunnerAndConfigurationSettings taskConfigurationAndSettings) {
RunManagerEx runManager = RunManagerEx.getInstanceEx(project);
runManager.removeConfiguration(taskConfigurationAndSettings);
}

private void deleteFileWithCode(@NotNull VirtualFile file) {
try {
file.delete(this);
} catch (IOException e) {
throw new NotificationException("Archiving of the task failed", "Unable to delete sources of the task, caused by " + e.getMessage());
}
}

private void saveCodeToArchive(@NotNull VirtualFile fileWithCode, VirtualFile directoryInArchiveForTheTask, String archiveTaskFileName) {
try {
VfsUtil.copyFile(this, fileWithCode, directoryInArchiveForTheTask, archiveTaskFileName);
} catch (IOException e) {
throw new NotificationException("Archiving of the task failed", "Archiving of the code of the solution failed, caused by " + e.getMessage());
}
}

private void saveRCToArchive(TaskConfiguration taskConfiguration, VirtualFile directoryInArchiveForTheTask, String archiveRCFileName) {
try {
String rcArchiveFqn = directoryInArchiveForTheTask.getPath() + "/" + archiveRCFileName;
Element rc = new Element("RC");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like here we do something very similar to what IDE does for us when saving configurations(in .idea/runConfigurations/). Maybe it's possible and makes sense to reuse it somehow?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd spell RC fully in the XML

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tried to do this via their API, but it is not obvious how to make the platform "forget" about the RC stored in a separate file before deleting them. They have a handy method for that in platform com.intellij.execution.impl.RunManagerImpl#removeConfigurations$intellij_platform_execution_impl but it is private.

Alternatively, we can get a path to the file where our configuration is currently stored and just copy the file back and forth. But that seems to be worse since the platform may update the place where RCs are stored in the future and that will break restoring.

Do you see any other options to reuse the platform?

I agree about the spelling, I will fix it

Document doc = new Document(rc);
taskConfiguration.writeExternal(rc);
XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());
xmlOutputter.output(doc, new FileOutputStream(rcArchiveFqn));
} catch (IOException e) {
throw new NotificationException("Archiving of the task failed", "Unable to archive the task, caused by " + e.getMessage());
}
}

@NotNull
private VirtualFile getFinalArchiveDirectoryOfFile(@NotNull VirtualFile file) {
String relativePathToParentInArchive;
if (pathToFile.isEmpty()) {
relativePathToParentInArchive = archiveDirectory + "/" + file.getParent().getName();
} else {
relativePathToParentInArchive = archiveDirectory + "/" + String.join("/", pathToFile);
}
VirtualFile parent;
try {
parent = VfsUtil.createDirectoryIfMissing(Paths.get(projectBaseDir.getPath(), relativePathToParentInArchive).toString());
} catch (IOException e) {
throw new NotificationException("Archiving of the task failed", "Unable to create directory in archive for the task, cased by " + e.getMessage());
}
return parent == null ? Objects.requireNonNull(projectBaseDir.findFileByRelativePath(relativePathToParentInArchive)) : parent;
}
}

private static @Nullable RunnerAndConfigurationSettings findJHelperRCForFile(@NotNull VirtualFile file, @NotNull Project project) {
RunManagerImpl runManager = RunManagerImpl.getInstanceImpl(project);
for (RunnerAndConfigurationSettings configuration : runManager.getAllSettings()) {
RunConfiguration rc = configuration.getConfiguration();
if (rc instanceof TaskConfiguration) {
TaskConfiguration task = (TaskConfiguration) rc;
String pathToClassFile = task.getCppPath();
VirtualFile expectedFie = FileUtilities.getFile(project, pathToClassFile);
if (file.equals(expectedFie)) {
return configuration;
}
}
}
return null;
}
}
2 changes: 1 addition & 1 deletion src/name/admitriev/jhelper/actions/DeleteTaskAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void run() {
);
}

private static void selectSomeTaskConfiguration(RunManagerEx runManager) {
public static void selectSomeTaskConfiguration(RunManagerEx runManager) {
for (RunnerAndConfigurationSettings settings : runManager.getAllSettings()) {
if (settings.getConfiguration() instanceof TaskConfiguration) {
runManager.setSelectedConfiguration(settings);
Expand Down
158 changes: 158 additions & 0 deletions src/name/admitriev/jhelper/actions/UnarchiveTaskAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package name.admitriev.jhelper.actions;

import com.intellij.execution.RunManager;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.psi.PsiManager;
import com.jetbrains.cidr.lang.psi.OCFile;
import name.admitriev.jhelper.IDEUtils;
import name.admitriev.jhelper.configuration.TaskConfiguration;
import name.admitriev.jhelper.configuration.TaskConfigurationType;
import name.admitriev.jhelper.exceptions.NotificationException;
import name.admitriev.jhelper.ui.UIUtils;
import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Objects;

public class UnarchiveTaskAction extends BaseAction {
@Override
protected void performAction(AnActionEvent e) {
Project project = e.getProject();
if (project == null) {
throw new NotificationException("No project found", "Are you in any project?");
}
VirtualFile file = e.getDataContext().getData(CommonDataKeys.VIRTUAL_FILE);
if (file == null) {
throw new NotificationException("No task or directory with tasks selected", "To unarchive a solution or a directory you should select it first");
}
VirtualFile projectBaseDir = ProjectUtil.guessProjectDir(project);
if (projectBaseDir == null) {
throw new NotificationException("Unable to find base directory of project", "If you are in default project then switch to the normal one");
}
ApplicationManager.getApplication().runWriteAction(new RecursiveUnarchiver(project, projectBaseDir, file));
}

private static final class RecursiveUnarchiver implements Runnable {
final Project project;
final VirtualFile projectBaseDir;
final VirtualFile selectedFile;
VirtualFile lastGeneratedFile;

public RecursiveUnarchiver(@NotNull Project project, @NotNull VirtualFile projectBaseDirPath, @NotNull VirtualFile selectedFile) {
this.project = project;
this.projectBaseDir = projectBaseDirPath;
this.selectedFile = selectedFile;
}

@Override
public void run() {
VfsUtilCore.visitChildrenRecursively(selectedFile, new VirtualFileVisitor<>() {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
if (file.isDirectory()) {
return true;
}
handleSourceFile(file);
return false;
}
});
UIUtils.openMethodInEditor(project, (OCFile) Objects.requireNonNull(PsiManager.getInstance(project).findFile(lastGeneratedFile)), "solve");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not wise to throw here if there's no solve method

IDEUtils.reloadProject(project);
}

private void handleSourceFile(@NotNull VirtualFile file) {
Pair<VirtualFile, VirtualFile> xmlAndCppToUnarchive = getXmlAndCppToUnarchive(file);
if (xmlAndCppToUnarchive == null) {
return;
}
VirtualFile xmlRC = xmlAndCppToUnarchive.first;
VirtualFile cppFile = xmlAndCppToUnarchive.second;
RunnerAndConfigurationSettings configuration = restoreTaskSettings(file, xmlRC);
TaskConfiguration taskConfiguration = ((TaskConfiguration) configuration.getConfiguration());

VirtualFile directory = getUnarchivedDirectoryOfTask(taskConfiguration.getCppPath());
try {
lastGeneratedFile = VfsUtil.copyFile(this, cppFile, directory, Paths.get(taskConfiguration.getCppPath()).getFileName().toString());
} catch (IOException e) {
throw new NotificationException("Restoring of task " + file.getNameWithoutExtension() + " failed", "Unable to restore source of the solution, caused by " + e.getMessage());
}
configuration.storeInDotIdeaFolder();
RunManager manager = RunManager.getInstance(project);
manager.addConfiguration(configuration);

try {
xmlRC.delete(this);
cppFile.delete(this);
} catch (IOException e) {
throw new NotificationException("Restoring of task " + file.getNameWithoutExtension() + " failed",
"Unable to delete archived rc and solution, caused by " + e.getMessage());
}
}

private RunnerAndConfigurationSettings restoreTaskSettings(@NotNull VirtualFile file, VirtualFile xmlRC) {
TaskConfigurationType taskConfigurationType = new TaskConfigurationType();
TaskConfiguration taskConfiguration = ((TaskConfiguration) taskConfigurationType.createTemplateConfiguration(project));
SAXBuilder saxBuilder = new SAXBuilder();
File inputFile = new File(xmlRC.getPath());
Document document;
try {
document = saxBuilder.build(inputFile);
} catch (JDOMException | IOException e) {
throw new NotificationException("Restoring of task " + file.getNameWithoutExtension() + " failed", "Unable to restore RC, caused by " + e.getMessage());
}
RunManager manager = RunManager.getInstance(project);
taskConfiguration.readExternal(document.getRootElement());
ConfigurationFactory factory = taskConfigurationType.getConfigurationFactories()[0];
return manager.createConfiguration(taskConfiguration, factory);
}

private VirtualFile getUnarchivedDirectoryOfTask(String relativeCppPath) {
VirtualFile directory;
try {
directory = VfsUtil.createDirectoryIfMissing(Paths.get(projectBaseDir.getPath(), relativeCppPath).getParent().toString());
} catch (IOException e) {
throw new NotificationException("Unarchive of the task failed", "Unable to create directory for the task, cased by " + e.getMessage());
}
return directory == null ? Objects.requireNonNull(projectBaseDir.findFileByRelativePath(Paths.get(relativeCppPath).getParent().toString())) : directory;
}

/* file can be either xml with RC or cpp with solution */
private static @Nullable Pair<VirtualFile, VirtualFile> getXmlAndCppToUnarchive(@NotNull VirtualFile file) {
String prefix = file.getNameWithoutExtension();
VirtualFile xml = null;
VirtualFile cpp = null;
for (VirtualFile otherFile : file.getParent().getChildren()) {
if (otherFile.getNameWithoutExtension().equals(prefix)) {
String extension = otherFile.getExtension();
if (extension != null && extension.equals("cpp")) {
cpp = otherFile;
}
if (extension != null && extension.equals("xml")) {
xml = otherFile;
}
}
}
if (xml == null || cpp == null) {
return null;
}
return new Pair<>(xml, cpp);
}
}
}
Loading