fixed EDU-301 and some minor changes
[idea/community.git] / python / edu / learn-python / src / com / jetbrains / python / edu / actions / StudyCheckAction.java
1 package com.jetbrains.python.edu.actions;
2
3 import com.intellij.execution.ExecutionException;
4 import com.intellij.execution.process.CapturingProcessHandler;
5 import com.intellij.execution.process.ProcessOutput;
6 import com.intellij.ide.projectView.ProjectView;
7 import com.intellij.openapi.actionSystem.ActionManager;
8 import com.intellij.openapi.actionSystem.AnActionEvent;
9 import com.intellij.openapi.application.ApplicationManager;
10 import com.intellij.openapi.command.CommandProcessor;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.editor.Document;
13 import com.intellij.openapi.editor.Editor;
14 import com.intellij.openapi.fileEditor.FileDocumentManager;
15 import com.intellij.openapi.fileEditor.FileEditor;
16 import com.intellij.openapi.fileEditor.FileEditorManager;
17 import com.intellij.openapi.options.ShowSettingsUtil;
18 import com.intellij.openapi.progress.ProgressIndicator;
19 import com.intellij.openapi.progress.ProgressManager;
20 import com.intellij.openapi.progress.TaskInfo;
21 import com.intellij.openapi.project.DumbAwareAction;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.projectRoots.Sdk;
24 import com.intellij.openapi.ui.MessageType;
25 import com.intellij.openapi.ui.popup.Balloon;
26 import com.intellij.openapi.ui.popup.BalloonBuilder;
27 import com.intellij.openapi.ui.popup.JBPopupFactory;
28 import com.intellij.openapi.util.Disposer;
29 import com.intellij.openapi.util.Pair;
30 import com.intellij.openapi.vfs.VirtualFile;
31 import com.intellij.openapi.wm.IdeFocusManager;
32 import com.intellij.openapi.wm.IdeFrame;
33 import com.intellij.openapi.wm.WindowManager;
34 import com.intellij.openapi.wm.ex.StatusBarEx;
35 import com.intellij.openapi.wm.ex.WindowManagerEx;
36 import com.jetbrains.python.edu.StudyDocumentListener;
37 import com.jetbrains.python.edu.StudyState;
38 import com.jetbrains.python.edu.StudyTestRunner;
39 import com.jetbrains.python.edu.StudyUtils;
40 import com.jetbrains.python.edu.course.StudyStatus;
41 import com.jetbrains.python.edu.course.Task;
42 import com.jetbrains.python.edu.course.TaskFile;
43 import com.jetbrains.python.edu.course.TaskWindow;
44 import com.jetbrains.python.edu.editor.StudyEditor;
45 import org.jetbrains.annotations.NotNull;
46
47 import javax.swing.*;
48 import javax.swing.event.HyperlinkEvent;
49 import javax.swing.event.HyperlinkListener;
50 import java.awt.*;
51 import java.io.IOException;
52 import java.util.List;
53 import java.util.Map;
54
55 public class StudyCheckAction extends DumbAwareAction {
56
57   private static final Logger LOG = Logger.getInstance(StudyCheckAction.class.getName());
58   private static final String ANSWERS_POSTFIX = "_answers.py";
59   public static final String ACTION_ID = "CheckAction";
60   public static final String SHORTCUT = "ctrl alt pressed ENTER";
61
62
63   private static void flushWindows(@NotNull final Task task, @NotNull final VirtualFile taskDir) {
64     for (Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) {
65       String name = entry.getKey();
66       TaskFile taskFile = entry.getValue();
67       VirtualFile virtualFile = taskDir.findChild(name);
68       if (virtualFile == null) {
69         continue;
70       }
71       StudyUtils.flushWindows(taskFile, virtualFile);
72     }
73   }
74
75   private static void deleteWindowDescriptions(@NotNull final Task task, @NotNull final VirtualFile taskDir) {
76     for (Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) {
77       String name = entry.getKey();
78       VirtualFile virtualFile = taskDir.findChild(name);
79       if (virtualFile == null) {
80         continue;
81       }
82       String windowsFileName = virtualFile.getNameWithoutExtension() + "_windows";
83       final VirtualFile windowsFile = taskDir.findChild(windowsFileName);
84       if (windowsFile != null) {
85         ApplicationManager.getApplication().runWriteAction(new Runnable() {
86           @Override
87           public void run() {
88             StudyUtils.deleteFile(windowsFile);
89           }
90         });
91       }
92     }
93   }
94
95   private static void drawAllTaskWindows(@NotNull final Project project, @NotNull final Task task, @NotNull final VirtualFile taskDir) {
96     for (Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) {
97       String name = entry.getKey();
98       TaskFile taskFile = entry.getValue();
99       VirtualFile virtualFile = taskDir.findChild(name);
100       if (virtualFile == null) {
101         continue;
102       }
103       FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(virtualFile);
104       if (fileEditor instanceof StudyEditor) {
105         StudyEditor studyEditor = (StudyEditor)fileEditor;
106         taskFile.drawAllWindows(studyEditor.getEditor());
107       }
108     }
109   }
110
111
112   public void check(@NotNull final Project project) {
113     ApplicationManager.getApplication().runWriteAction(new Runnable() {
114       @Override
115       public void run() {
116         CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
117           @Override
118           public void run() {
119             final StudyEditor selectedEditor = StudyEditor.getSelectedStudyEditor(project);
120             if (selectedEditor == null) return;
121             final StudyState studyState = new StudyState(selectedEditor);
122             if (!studyState.isValid()) {
123               LOG.error("StudyCheckAction was invokes outside study editor");
124               return;
125             }
126             final IdeFrame frame = ((WindowManagerEx)WindowManager.getInstance()).findFrameFor(project);
127             final StatusBarEx statusBar = frame == null ? null : (StatusBarEx)frame.getStatusBar();
128             if (statusBar != null) {
129               final List<Pair<TaskInfo, ProgressIndicator>> processes = statusBar.getBackgroundProcesses();
130               if (!processes.isEmpty()) return;
131             }
132
133             final Task task = studyState.getTask();
134             final VirtualFile taskDir = studyState.getTaskDir();
135             flushWindows(task, taskDir);
136             Sdk sdk = StudyUtils.findPythonSdk(project);
137             if (sdk == null) {
138               createNoPythonInterpreterPopUp(project);
139               return;
140             }
141             final StudyRunAction runAction = (StudyRunAction)ActionManager.getInstance().getAction(StudyRunAction.ACTION_ID);
142             if (runAction == null) {
143               return;
144             }
145             runAction.run(project, sdk);
146             ApplicationManager.getApplication().invokeLater(new Runnable() {
147               @Override
148               public void run() {
149                 IdeFocusManager.getInstance(project).requestFocus(studyState.getEditor().getComponent(), true);
150               }
151             });
152             final StudyTestRunner testRunner = new StudyTestRunner(task, taskDir);
153             Process testProcess = null;
154             try {
155               testProcess = testRunner.createCheckProcess(project, studyState.getVirtualFile().getPath());
156             }
157             catch (ExecutionException e) {
158               LOG.error(e);
159             }
160             if (testProcess == null) {
161               return;
162             }
163             selectedEditor.getCheckButton().setEnabled(false);
164             ProgressManager.getInstance().run(getCheckTask(studyState, testRunner, testProcess, project, selectedEditor));
165           }
166         });
167       }
168     });
169   }
170
171   @NotNull
172   private com.intellij.openapi.progress.Task.Backgroundable getCheckTask(final StudyState studyState,
173                                                                          final StudyTestRunner testRunner,
174                                                                          final Process testProcess,
175                                                                          @NotNull final Project project,
176                                                                          final StudyEditor selectedEditor) {
177     final Task task = studyState.getTask();
178     final VirtualFile taskDir = studyState.getTaskDir();
179     final StudyStatus statusBeforeCheck = task.getStatus();
180     return new com.intellij.openapi.progress.Task.Backgroundable(project, "Checking task", true) {
181       @Override
182       public void onSuccess() {
183         StudyUtils.updateStudyToolWindow(project);
184         drawAllTaskWindows(project, task, taskDir);
185         ProjectView.getInstance(project).refresh();
186         deleteWindowDescriptions(task, taskDir);
187         selectedEditor.getCheckButton().setEnabled(true);
188       }
189
190       @Override
191       public void onCancel() {
192         StudyStatus currentStatus = task.getStatus();
193         task.setStatus(statusBeforeCheck, currentStatus);
194         deleteWindowDescriptions(task, taskDir);
195         selectedEditor.getCheckButton().setEnabled(true);
196       }
197
198       @Override
199       public void run(@NotNull ProgressIndicator indicator) {
200         final Map<String, TaskFile> taskFiles = task.getTaskFiles();
201         final CapturingProcessHandler handler = new CapturingProcessHandler(testProcess);
202         final ProcessOutput output = handler.runProcessWithProgressIndicator(indicator);
203         if (indicator.isCanceled()) {
204           ApplicationManager.getApplication().invokeLater(new Runnable() {
205             @Override
206             public void run() {
207               showTestResultPopUp("Tests check cancelled.", MessageType.WARNING.getPopupBackground(), project);
208             }
209           });
210           return;
211         }
212         final String failedMessage = testRunner.getTestsOutput(output);
213         if (StudyTestRunner.TEST_OK.equals(failedMessage)) {
214           task.setStatus(StudyStatus.Solved, statusBeforeCheck);
215           ApplicationManager.getApplication().invokeLater(new Runnable() {
216             @Override
217             public void run() {
218               showTestResultPopUp("Congratulations!", MessageType.INFO.getPopupBackground(), project);
219             }
220           });
221         }
222         else {
223           ApplicationManager.getApplication().invokeLater(new Runnable() {
224             @Override
225             public void run() {
226               if (taskDir == null) return;
227               task.setStatus(StudyStatus.Failed, statusBeforeCheck);
228               for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) {
229                 final String name = entry.getKey();
230                 final TaskFile taskFile = entry.getValue();
231                 if (taskFile.getTaskWindows().size() < 2) {
232                   taskFile.setStatus(StudyStatus.Failed, StudyStatus.Unchecked);
233                   continue;
234                 }
235                 CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
236                   @Override
237                   public void run() {
238                     ApplicationManager.getApplication().runWriteAction(new Runnable() {
239                       @Override
240                       public void run() {
241                         runSmartTestProcess(taskDir, testRunner, name, taskFile, project);
242                       }
243                     });
244                   }
245                 });
246               }
247               showTestResultPopUp(failedMessage, MessageType.ERROR.getPopupBackground(), project);
248               navigateToFailedTaskWindow(studyState, task, taskDir, project);
249             }
250           });
251         }
252       }
253     };
254   }
255
256   private static void createNoPythonInterpreterPopUp(@NotNull final Project project) {
257     String text = "<html>No Python interpreter configured for the project<br><a href=\"\">Configure interpreter</a></html>";
258     BalloonBuilder balloonBuilder =
259       JBPopupFactory.getInstance()
260         .createHtmlTextBalloonBuilder(text, null, MessageType.WARNING.getPopupBackground(), new HyperlinkListener() {
261           @Override
262           public void hyperlinkUpdate(HyperlinkEvent event) {
263             if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
264               ApplicationManager.getApplication().invokeLater(new Runnable() {
265                 @Override
266                 public void run() {
267                   ShowSettingsUtil.getInstance()
268                     .showSettingsDialog(project, "Project Interpreter");
269                 }
270               });
271             }
272           }
273         });
274     balloonBuilder.setHideOnLinkClick(true);
275     final Balloon balloon = balloonBuilder.createBalloon();
276     showCheckPopUp(project, balloon);
277   }
278
279   private static void navigateToFailedTaskWindow(@NotNull final StudyState studyState,
280                                                  @NotNull final Task task,
281                                                  @NotNull final VirtualFile taskDir,
282                                                  @NotNull final Project project) {
283     TaskFile selectedTaskFile = studyState.getTaskFile();
284     Editor editor = studyState.getEditor();
285     TaskFile taskFileToNavigate = selectedTaskFile;
286     VirtualFile fileToNavigate = studyState.getVirtualFile();
287     if (!selectedTaskFile.hasFailedTaskWindows()) {
288       for (Map.Entry<String, TaskFile> entry : task.getTaskFiles().entrySet()) {
289         String name = entry.getKey();
290         TaskFile taskFile = entry.getValue();
291         if (taskFile.hasFailedTaskWindows()) {
292           taskFileToNavigate = taskFile;
293           VirtualFile virtualFile = taskDir.findChild(name);
294           if (virtualFile == null) {
295             continue;
296           }
297           FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(virtualFile);
298           if (fileEditor instanceof StudyEditor) {
299             StudyEditor studyEditor = (StudyEditor)fileEditor;
300             editor = studyEditor.getEditor();
301           }
302           fileToNavigate = virtualFile;
303           break;
304         }
305       }
306     }
307     FileEditorManager.getInstance(project).openFile(fileToNavigate, true);
308     final Editor editorToNavigate = editor;
309     ApplicationManager.getApplication().invokeLater(new Runnable() {
310       @Override
311       public void run() {
312         IdeFocusManager.getInstance(project).requestFocus(editorToNavigate.getContentComponent(), true);
313       }
314     });
315     taskFileToNavigate.navigateToFirstFailedTaskWindow(editor);
316   }
317
318   private void runSmartTestProcess(@NotNull final VirtualFile taskDir,
319                                    @NotNull final StudyTestRunner testRunner,
320                                    final String taskFileName,
321                                    @NotNull final TaskFile taskFile,
322                                    @NotNull final Project project) {
323     final TaskFile answerTaskFile = new TaskFile();
324     final VirtualFile virtualFile = taskDir.findChild(taskFileName);
325     if (virtualFile == null) {
326       return;
327     }
328     final VirtualFile answerFile = getCopyWithAnswers(taskDir, virtualFile, taskFile, answerTaskFile);
329     for (final TaskWindow taskWindow : answerTaskFile.getTaskWindows()) {
330       final Document document = FileDocumentManager.getInstance().getDocument(virtualFile);
331       if (document == null) {
332         continue;
333       }
334       if (!taskWindow.isValid(document)) {
335         continue;
336       }
337       taskWindow.smartCheck(project, answerFile, answerTaskFile, taskFile, testRunner, virtualFile, document);
338     }
339     StudyUtils.deleteFile(answerFile);
340   }
341
342   private VirtualFile getCopyWithAnswers(@NotNull final VirtualFile taskDir,
343                                          @NotNull final VirtualFile file,
344                                          @NotNull final TaskFile source,
345                                          @NotNull final TaskFile target) {
346     VirtualFile copy = null;
347     try {
348
349       copy = file.copy(this, taskDir, file.getNameWithoutExtension() + ANSWERS_POSTFIX);
350       final FileDocumentManager documentManager = FileDocumentManager.getInstance();
351       final Document document = documentManager.getDocument(copy);
352       if (document != null) {
353         TaskFile.copy(source, target);
354         StudyDocumentListener listener = new StudyDocumentListener(target);
355         document.addDocumentListener(listener);
356         for (TaskWindow taskWindow : target.getTaskWindows()) {
357           if (!taskWindow.isValid(document)) {
358             continue;
359           }
360           final int start = taskWindow.getRealStartOffset(document);
361           final int end = start + taskWindow.getLength();
362           final String text = taskWindow.getPossibleAnswer();
363           document.replaceString(start, end, text);
364         }
365         ApplicationManager.getApplication().runWriteAction(new Runnable() {
366           @Override
367           public void run() {
368             documentManager.saveDocument(document);
369           }
370         });
371       }
372     }
373     catch (IOException e) {
374       LOG.error(e);
375     }
376     return copy;
377   }
378
379   private static void showTestResultPopUp(final String text, Color color, @NotNull final Project project) {
380     BalloonBuilder balloonBuilder =
381       JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text, null, color, null);
382     final Balloon balloon = balloonBuilder.createBalloon();
383     showCheckPopUp(project, balloon);
384   }
385
386   /**
387    * shows pop up in the center of "check task" button in study editor
388    */
389   private static void showCheckPopUp(@NotNull final Project project, @NotNull final Balloon balloon) {
390     StudyEditor studyEditor = StudyEditor.getSelectedStudyEditor(project);
391     assert studyEditor != null;
392     JButton checkButton = studyEditor.getCheckButton();
393     balloon.showInCenterOf(checkButton);
394     Disposer.register(project, balloon);
395   }
396
397   @Override
398   public void actionPerformed(@NotNull AnActionEvent e) {
399     Project project = e.getProject();
400     if (project != null) {
401       check(project);
402     }
403   }
404 }