Snapshot idea/138.1696 from git://git.jetbrains.org/idea/community.git

Change-Id: I50c97b83a815ce635e49a38380ba5b8765e4b16a
diff --git a/python/edu/learn-python/gen/icons/StudyIcons.java b/python/edu/learn-python/gen/icons/StudyIcons.java
new file mode 100644
index 0000000..2840910
--- /dev/null
+++ b/python/edu/learn-python/gen/icons/StudyIcons.java
@@ -0,0 +1,29 @@
+package icons;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.*;
+
+/**
+ * NOTE THIS FILE IS AUTO-GENERATED
+ * DO NOT EDIT IT BY HAND, run build/scripts/icons.gant instead
+ */
+public class StudyIcons {
+  private static Icon load(String path) {
+    return IconLoader.getIcon(path, StudyIcons.class);
+  }
+
+  public static final Icon Add = load("/icons/com/jetbrains/python/edu/add.png"); // 16x16
+  public static final Icon Checked = load("/icons/com/jetbrains/python/edu/checked.png"); // 32x32
+  public static final Icon Failed = load("/icons/com/jetbrains/python/edu/failed.png"); // 32x32
+  public static final Icon Next = load("/icons/com/jetbrains/python/edu/next.png"); // 24x24
+  public static final Icon Playground = load("/icons/com/jetbrains/python/edu/playground.png"); // 32x28
+  public static final Icon Prev = load("/icons/com/jetbrains/python/edu/prev.png"); // 24x24
+  public static final Icon Refresh = load("/icons/com/jetbrains/python/edu/refresh.png"); // 16x16
+  public static final Icon Refresh24 = load("/icons/com/jetbrains/python/edu/refresh24.png"); // 24x24
+  public static final Icon Resolve = load("/icons/com/jetbrains/python/edu/resolve.png"); // 24x24
+  public static final Icon Run = load("/icons/com/jetbrains/python/edu/Run.png"); // 24x24
+  public static final Icon ShowHint = load("/icons/com/jetbrains/python/edu/showHint.png"); // 24x24
+  public static final Icon Unchecked = load("/icons/com/jetbrains/python/edu/unchecked.png"); // 32x32
+  public static final Icon WatchInput = load("/icons/com/jetbrains/python/edu/WatchInput.png"); // 24x24
+}
diff --git a/python/edu/learn-python/learn-python.iml b/python/edu/learn-python/learn-python.iml
new file mode 100644
index 0000000..bd539d1
--- /dev/null
+++ b/python/edu/learn-python/learn-python.iml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="module" module-name="platform-impl" />
+    <orderEntry type="module" module-name="python-community" />
+    <orderEntry type="module" module-name="lang-impl" />
+    <orderEntry type="library" name="gson" level="project" />
+    <orderEntry type="library" name="JUnit4" level="project" />
+  </component>
+</module>
+
diff --git a/python/edu/learn-python/resources/META-INF/plugin.xml b/python/edu/learn-python/resources/META-INF/plugin.xml
new file mode 100644
index 0000000..ec828eb
--- /dev/null
+++ b/python/edu/learn-python/resources/META-INF/plugin.xml
@@ -0,0 +1,73 @@
+<!--suppress XmlUnboundNsPrefix -->
+<idea-plugin version="2">
+  <id>com.jetbrains.python.edu.learn-python</id>
+  <name>Educational plugin for PyCharm</name>
+  <version>1.0</version>
+  <vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
+
+  <description><![CDATA[
+
+      ]]></description>
+
+  <change-notes><![CDATA[
+
+      ]]>
+  </change-notes>
+
+  <!--depends>com.intellij.modules.python</depends-->
+
+  <!-- please see http://confluence.jetbrains.net/display/IDEADEV/Plugin+Compatibility+with+IntelliJ+Platform+Products
+       on how to target different products -->
+
+  <depends>com.intellij.modules.lang</depends>
+  <depends>com.intellij.modules.python</depends>
+  <application-components>
+  </application-components>
+
+  <project-components>
+    <component>
+      <implementation-class>com.jetbrains.python.edu.StudyTaskManager</implementation-class>
+      <interface-class>com.jetbrains.python.edu.StudyTaskManager</interface-class>
+    </component>
+  </project-components>
+
+  <application-components>
+    <component>
+      <implementation-class>com.jetbrains.python.edu.StudyInitialConfigurator</implementation-class>
+      <headless-implementation-class/>
+    </component>
+  </application-components>
+
+  <actions>
+    <action id="CheckAction" class="com.jetbrains.python.edu.actions.StudyCheckAction" text="check"
+            description="Runs tests for current tasks" icon="/icons/icon.jpg">
+    </action>
+    <action id="PrevWindowAction" class="com.jetbrains.python.edu.actions.StudyPrevWindowAction" text="PrevWindowAction" description="prev">
+    </action>
+
+    <action id="NextWindow" class="com.jetbrains.python.edu.actions.StudyNextWindowAction" text="NextWindowAction" description="next">
+    </action>
+    <action id="NextTaskAction" class="com.jetbrains.python.edu.actions.StudyNextStudyTaskAction" text="NextTaskAction" description="Next Task"/>
+    <action id="PreviousTaskAction" class="com.jetbrains.python.edu.actions.StudyPreviousStudyTaskAction" text="PreviousTaskAction"
+            description="Previous Task"/>
+    <action id="RefreshTaskAction" class="com.jetbrains.python.edu.actions.StudyRefreshTaskAction" text="RefreshTaskAction"
+            description="Refresh current task"/>
+    <action id="WatchInputAction" class="com.jetbrains.python.edu.actions.StudyEditInputAction" text="WatchInputAction"
+            description="watch input"/>
+    <action id="StudyRunAction" class="com.jetbrains.python.edu.actions.StudyRunAction" text="StudyRunAction" description="run your code"/>
+    <action id="ShowHintAction" class="com.jetbrains.python.edu.actions.StudyShowHintAction" text="Show hint"
+            description="show hint">
+      <add-to-group group-id="MainToolBar" anchor="last"/>
+    </action>
+  </actions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <toolWindow id="Course Description" anchor="right" factoryClass="com.jetbrains.python.edu.ui.StudyToolWindowFactory" conditionClass="com.jetbrains.python.edu.ui.StudyCondition"/>
+    <fileEditorProvider implementation="com.jetbrains.python.edu.editor.StudyFileEditorProvider"/>
+    <directoryProjectGenerator implementation="com.jetbrains.python.edu.StudyDirectoryProjectGenerator"/>
+    <treeStructureProvider implementation="com.jetbrains.python.edu.projectView.StudyTreeStructureProvider"/>
+    <highlightErrorFilter implementation="com.jetbrains.python.edu.StudyHighlightErrorFilter"/>
+    <applicationService serviceInterface="com.intellij.openapi.fileEditor.impl.EditorEmptyTextPainter"
+        serviceImplementation="com.jetbrains.python.edu.StudyInstructionPainter" overrides="true"/>
+  </extensions>
+</idea-plugin>
\ No newline at end of file
diff --git a/python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py b/python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py
new file mode 100644
index 0000000..c17e6cd
--- /dev/null
+++ b/python/edu/learn-python/resources/com/jetbrains/python/edu/user_tester.py
@@ -0,0 +1,58 @@
+import sys
+import imp
+import os
+import subprocess
+
+USER_TESTS = "userTests"
+
+TEST_FAILED = "FAILED"
+
+TEST_PASSED = "PASSED"
+
+INPUT = "input"
+OUTPUT = "output"
+
+
+def get_index(logical_name, full_name):
+    logical_name_len = len(logical_name)
+    if full_name[:logical_name_len] == logical_name:
+        return int(full_name[logical_name_len])
+    return -1
+
+
+def process_user_tests(file_path):
+    user_tests = []
+    imp.load_source('user_file', file_path)
+    user_tests_dir_path = os.path.abspath(os.path.join(file_path, os.pardir, USER_TESTS))
+    user_test_files = os.listdir(user_tests_dir_path)
+    for user_file in user_test_files:
+        index = get_index(INPUT, user_file)
+        if index == -1:
+            continue
+        output = OUTPUT + str(index)
+        if output in user_test_files:
+            input_path = os.path.abspath(os.path.join(user_tests_dir_path, user_file))
+            output_path = os.path.abspath(os.path.join(user_tests_dir_path, output))
+            user_tests.append((input_path, output_path, index))
+    return sorted(user_tests, key=(lambda x: x[2]))
+
+
+def run_user_test(python, executable_path):
+    user_tests = process_user_tests(executable_path)
+    for test in user_tests:
+        input, output, index = test
+        test_output = subprocess.check_output([python, executable_path, input])
+        expected_output = open(output).read()
+        test_status = TEST_PASSED if test_output == expected_output else TEST_FAILED
+        print "TEST" + str(index) + " " + test_status
+        print "OUTPUT:"
+        print test_output + "\n"
+        if test_status == TEST_FAILED:
+            print "EXPECTED OUTPUT:"
+            print expected_output + "\n"
+
+
+if __name__ == "__main__":
+    python = sys.argv[1]
+    executable_path = sys.argv[2]
+    run_user_test(python , executable_path)
\ No newline at end of file
diff --git a/python/edu/learn-python/resources/courses/introduction_course.zip b/python/edu/learn-python/resources/courses/introduction_course.zip
new file mode 100644
index 0000000..f3b24f2
--- /dev/null
+++ b/python/edu/learn-python/resources/courses/introduction_course.zip
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png
new file mode 100644
index 0000000..27a6e36
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/Run.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png
new file mode 100644
index 0000000..4992191
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/WatchInput.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png
new file mode 100644
index 0000000..9494f2d
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/add.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png
new file mode 100644
index 0000000..4105a01
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/checked.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png
new file mode 100644
index 0000000..e2aaa55
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/failed.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg
new file mode 100644
index 0000000..3a9716e
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/icon.jpg
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png
new file mode 100644
index 0000000..dd1a5d9
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/next.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png
new file mode 100644
index 0000000..d12a751
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/playground.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png
new file mode 100644
index 0000000..0656f81
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/prev.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png
new file mode 100644
index 0000000..d595f6b
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png
new file mode 100644
index 0000000..218f075
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/refresh24.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png
new file mode 100644
index 0000000..7ef960b
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png
new file mode 100644
index 0000000..99aaa1d
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/resolve_dark.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png
new file mode 100644
index 0000000..f10fd56
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/showHint.png
Binary files differ
diff --git a/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png
new file mode 100644
index 0000000..2145982
--- /dev/null
+++ b/python/edu/learn-python/resources/icons/com/jetbrains/python/edu/unchecked.png
Binary files differ
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java
new file mode 100644
index 0000000..d4831d9
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDirectoryProjectGenerator.java
@@ -0,0 +1,393 @@
+package com.jetbrains.python.edu;
+
+import com.google.gson.*;
+import com.google.gson.stream.JsonReader;
+import com.intellij.facet.ui.FacetEditorValidator;
+import com.intellij.facet.ui.FacetValidatorsManager;
+import com.intellij.facet.ui.ValidationResult;
+import com.intellij.lang.javascript.boilerplate.GithubDownloadUtil;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.platform.DirectoryProjectGenerator;
+import com.intellij.platform.templates.github.GeneratorException;
+import com.intellij.platform.templates.github.ZipUtil;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.CourseInfo;
+import com.jetbrains.python.edu.ui.StudyNewProjectPanel;
+import com.jetbrains.python.newProject.PythonProjectGenerator;
+import icons.StudyIcons;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class StudyDirectoryProjectGenerator extends PythonProjectGenerator implements DirectoryProjectGenerator {
+  private static final Logger LOG = Logger.getInstance(StudyDirectoryProjectGenerator.class.getName());
+  private static final String REPO_URL = "https://github.com/JetBrains/pycharm-courses/archive/master.zip";
+  private static final String USER_NAME = "PyCharm";
+  private static final String COURSE_META_FILE = "course.json";
+  private static final String COURSE_NAME_ATTRIBUTE = "name";
+  private static final Pattern CACHE_PATTERN = Pattern.compile("(name=(.*)) (path=(.*course.json)) (author=(.*)) (description=(.*))");
+  private static final String REPOSITORY_NAME = "pycharm-courses";
+  public static final String AUTHOR_ATTRIBUTE = "author";
+  private final File myCoursesDir = new File(PathManager.getConfigPath(), "courses");
+  private static final String CACHE_NAME = "courseNames.txt";
+  private Map<CourseInfo, File> myCourses = new HashMap<CourseInfo, File>();
+  private File mySelectedCourseFile;
+  private Project myProject;
+  public ValidationResult myValidationResult = new ValidationResult("selected course is not valid");
+
+  @Nls
+  @NotNull
+  @Override
+  public String getName() {
+    return "Study project";
+  }
+
+
+  public void setCourses(Map<CourseInfo, File> courses) {
+    myCourses = courses;
+  }
+
+  /**
+   * Finds selected course in courses by name.
+   *
+   * @param courseName name of selected course
+   */
+  public void setSelectedCourse(@NotNull CourseInfo courseName) {
+    File courseFile = myCourses.get(courseName);
+    if (courseFile == null) {
+      LOG.error("invalid course in list");
+    }
+    mySelectedCourseFile = courseFile;
+  }
+
+  /**
+   * Adds course to courses specified in params
+   *
+   * @param courseDir must be directory containing course file
+   * @return added course name or null if course is invalid
+   */
+  @Nullable
+  private static CourseInfo addCourse(Map<CourseInfo, File> courses, File courseDir) {
+    if (courseDir.isDirectory()) {
+      File[] courseFiles = courseDir.listFiles(new FilenameFilter() {
+        @Override
+        public boolean accept(File dir, String name) {
+          return name.equals(COURSE_META_FILE);
+        }
+      });
+      if (courseFiles.length != 1) {
+        LOG.info("User tried to add course with more than one or without course files");
+        return null;
+      }
+      File courseFile = courseFiles[0];
+      CourseInfo courseInfo = getCourseInfo(courseFile);
+      if (courseInfo != null) {
+        courses.put(courseInfo, courseFile);
+      }
+      return courseInfo;
+    }
+    return null;
+  }
+
+
+  /**
+   * Adds course from zip archive to courses
+   *
+   * @return added course name or null if course is invalid
+   */
+  @Nullable
+  public CourseInfo addLocalCourse(String zipFilePath) {
+    File file = new File(zipFilePath);
+    try {
+      String fileName = file.getName();
+      String unzippedName = fileName.substring(0, fileName.indexOf("."));
+      File courseDir = new File(myCoursesDir, unzippedName);
+      ZipUtil.unzip(null, courseDir, file, null, null, true);
+      CourseInfo courseName = addCourse(myCourses, courseDir);
+      flushCache();
+      return courseName;
+    }
+    catch (IOException e) {
+      LOG.error("Failed to unzip course archive");
+      LOG.error(e);
+    }
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Object showGenerationSettings(VirtualFile baseDir) throws ProcessCanceledException {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Icon getLogo() {
+    return StudyIcons.Playground;
+  }
+
+
+  @Override
+  public void generateProject(@NotNull final Project project, @NotNull final VirtualFile baseDir,
+                              @Nullable Object settings, @NotNull Module module) {
+
+    myProject = project;
+    Reader reader = null;
+    try {
+      reader = new InputStreamReader(new FileInputStream(mySelectedCourseFile));
+      Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+      Course course = gson.fromJson(reader, Course.class);
+      course.init(false);
+      course.create(baseDir, new File(mySelectedCourseFile.getParent()));
+      course.setResourcePath(mySelectedCourseFile.getAbsolutePath());
+      VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
+      StudyTaskManager.getInstance(project).setCourse(course);
+    }
+    catch (FileNotFoundException e) {
+      LOG.error(e);
+    }
+    finally {
+      StudyUtils.closeSilently(reader);
+    }
+  }
+
+  /**
+   * Downloads courses from {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#REPO_URL}
+   * and unzips them into {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCoursesDir}
+   */
+
+  public void downloadAndUnzip(boolean needProgressBar) {
+    File outputFile = new File(PathManager.getConfigPath(), "courses.zip");
+    try {
+      if (!needProgressBar) {
+        GithubDownloadUtil.downloadAtomically(null, REPO_URL,
+                                              outputFile, USER_NAME, REPOSITORY_NAME);
+      }
+      else {
+        GithubDownloadUtil.downloadContentToFileWithProgressSynchronously(myProject, REPO_URL, "downloading courses", outputFile, USER_NAME,
+                                                                          REPOSITORY_NAME, false);
+      }
+      if (outputFile.exists()) {
+        ZipUtil.unzip(null, myCoursesDir, outputFile, null, null, true);
+        if (!outputFile.delete()) {
+          LOG.error("Failed to delete", outputFile.getName());
+        }
+        File[] files = myCoursesDir.listFiles();
+        if (files != null) {
+          for (File file : files) {
+            String fileName = file.getName();
+            if (StudyUtils.isZip(fileName)) {
+              ZipUtil.unzip(null, new File(myCoursesDir, fileName.substring(0, fileName.indexOf("."))), file, null, null, true);
+              if (!file.delete()) {
+                LOG.error("Failed to delete", fileName);
+              }
+            }
+          }
+        }
+      } else {
+        LOG.debug("failed to download course");
+      }
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+    catch (GeneratorException e) {
+      LOG.error(e);
+    }
+  }
+
+  public Map<CourseInfo, File> getLoadedCourses() {
+    return myCourses;
+  }
+
+  /**
+   * Parses courses located in {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCoursesDir}
+   * to {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#myCourses}
+   *
+   * @return map with course names and course files location
+   */
+  public Map<CourseInfo, File> loadCourses() {
+    Map<CourseInfo, File> courses = new HashMap<CourseInfo, File>();
+    if (myCoursesDir.exists()) {
+      File[] courseDirs = myCoursesDir.listFiles(new FileFilter() {
+        @Override
+        public boolean accept(File pathname) {
+          return pathname.isDirectory();
+        }
+      });
+      for (File courseDir : courseDirs) {
+        addCourse(courses, courseDir);
+      }
+    }
+    return courses;
+  }
+
+  /**
+   * Parses course json meta file and finds course name
+   *
+   * @return information about course or null if course file is invalid
+   */
+  @Nullable
+  private static CourseInfo getCourseInfo(File courseFile) {
+    CourseInfo courseInfo = null;
+    BufferedReader reader = null;
+    try {
+      if (courseFile.getName().equals(COURSE_META_FILE)) {
+        reader = new BufferedReader(new InputStreamReader(new FileInputStream(courseFile)));
+        JsonReader r = new JsonReader(reader);
+        JsonParser parser = new JsonParser();
+        JsonElement el = parser.parse(r);
+        String courseName = el.getAsJsonObject().get(COURSE_NAME_ATTRIBUTE).getAsString();
+        String courseAuthor = el.getAsJsonObject().get(AUTHOR_ATTRIBUTE).getAsString();
+        String courseDescription = el.getAsJsonObject().get("description").getAsString();
+        courseInfo = new CourseInfo(courseName, courseAuthor, courseDescription);
+      }
+    }
+    catch (Exception e) {
+      //error will be shown in UI
+    }
+    finally {
+      StudyUtils.closeSilently(reader);
+    }
+    return courseInfo;
+  }
+
+  @NotNull
+  @Override
+  public ValidationResult validate(@NotNull String s) {
+    return myValidationResult;
+  }
+
+  public void setValidationResult(ValidationResult validationResult) {
+    myValidationResult = validationResult;
+  }
+
+  /**
+   * @return courses from memory or from cash file or parses course directory
+   */
+  public Map<CourseInfo, File> getCourses() {
+    if (!myCourses.isEmpty()) {
+      return myCourses;
+    }
+    if (myCoursesDir.exists()) {
+      File cacheFile = new File(myCoursesDir, CACHE_NAME);
+      if (cacheFile.exists()) {
+        myCourses = getCoursesFromCache(cacheFile);
+        if (!myCourses.isEmpty()) {
+          return myCourses;
+        }
+      }
+      myCourses = loadCourses();
+      if (!myCourses.isEmpty()) {
+        return myCourses;
+      }
+    }
+    downloadAndUnzip(false);
+    myCourses = loadCourses();
+    flushCache();
+    return myCourses;
+  }
+
+  /**
+   * Writes courses to cash file {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#CACHE_NAME}
+   */
+  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+  public void flushCache() {
+    File cashFile = new File(myCoursesDir, CACHE_NAME);
+    PrintWriter writer = null;
+    try {
+      if (!cashFile.exists()) {
+        final boolean created = cashFile.createNewFile();
+        if (!created) {
+          LOG.error("Cannot flush courses cache. Can't create " + CACHE_NAME + " file");
+          return;
+        }
+      }
+      writer = new PrintWriter(cashFile);
+      for (Map.Entry<CourseInfo, File> course : myCourses.entrySet()) {
+        CourseInfo courseInfo = course.getKey();
+        String line = String
+          .format("name=%s path=%s author=%s description=%s", courseInfo.getName(), course.getValue(), courseInfo.getAuthor(),
+                  courseInfo.getDescription());
+        writer.println(line);
+      }
+    }
+    catch (FileNotFoundException e) {
+      LOG.error(e);
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+    finally {
+      StudyUtils.closeSilently(writer);
+    }
+  }
+
+  /**
+   * Loads courses from {@link com.jetbrains.python.edu.StudyDirectoryProjectGenerator#CACHE_NAME} file
+   *
+   * @return map of course names and course files
+   */
+  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+  private static Map<CourseInfo, File> getCoursesFromCache(File cashFile) {
+    Map<CourseInfo, File> coursesFromCash = new HashMap<CourseInfo, File>();
+    BufferedReader reader =  null;
+    try {
+       reader = new BufferedReader(new InputStreamReader(new FileInputStream(cashFile)));
+      String line;
+
+      while ((line = reader.readLine()) != null) {
+        Matcher matcher = CACHE_PATTERN.matcher(line);
+        if (matcher.matches()) {
+          String courseName = matcher.group(2);
+          File file = new File(matcher.group(4));
+          String author = matcher.group(6);
+          String description = matcher.group(8);
+          CourseInfo courseInfo = new CourseInfo(courseName, author, description);
+          if (file.exists()) {
+            coursesFromCash.put(courseInfo, file);
+          }
+        }
+      }
+    }
+    catch (FileNotFoundException e) {
+      LOG.error(e);
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+    finally {
+      StudyUtils.closeSilently(reader);
+    }
+    return coursesFromCash;
+  }
+
+  @Nullable
+  @Override
+  public JPanel extendBasePanel() throws ProcessCanceledException {
+    StudyNewProjectPanel settingsPanel = new StudyNewProjectPanel(this);
+    settingsPanel.registerValidators(new FacetValidatorsManager() {
+      public void registerValidator(FacetEditorValidator validator, JComponent... componentsToWatch) {
+        throw new UnsupportedOperationException();
+      }
+      public void validate() {
+        fireStateChanged();
+      }
+    });
+    return settingsPanel.getContentPanel();
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java
new file mode 100644
index 0000000..9fdcf70
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyDocumentListener.java
@@ -0,0 +1,65 @@
+package com.jetbrains.python.edu;
+
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.event.DocumentAdapter;
+import com.intellij.openapi.editor.event.DocumentEvent;
+import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+
+/**
+ * author: liana
+ * data: 7/16/14.
+ * Listens changes in study files and updates
+ * coordinates of all the windows in current task file
+ */
+public class StudyDocumentListener extends DocumentAdapter {
+  private final TaskFile myTaskFile;
+  private int oldLine;
+  private int oldLineStartOffset;
+  private TaskWindow myTaskWindow;
+
+  public StudyDocumentListener(TaskFile taskFile) {
+    myTaskFile = taskFile;
+  }
+
+
+  //remembering old end before document change because of problems
+  // with fragments containing "\n"
+  @Override
+  public void beforeDocumentChange(DocumentEvent e) {
+    int offset = e.getOffset();
+    int oldEnd = offset + e.getOldLength();
+    Document document = e.getDocument();
+    oldLine = document.getLineNumber(oldEnd);
+    oldLineStartOffset = document.getLineStartOffset(oldLine);
+    int line = document.getLineNumber(offset);
+    int offsetInLine = offset - document.getLineStartOffset(line);
+    LogicalPosition pos = new LogicalPosition(line, offsetInLine);
+    myTaskWindow = myTaskFile.getTaskWindow(document, pos);
+
+  }
+
+  @Override
+  public void documentChanged(DocumentEvent e) {
+    if (e instanceof DocumentEventImpl) {
+      DocumentEventImpl event = (DocumentEventImpl)e;
+      Document document = e.getDocument();
+      int offset = e.getOffset();
+      int change = event.getNewLength() - event.getOldLength();
+      if (myTaskWindow != null) {
+        int newLength = myTaskWindow.getLength() + change;
+        myTaskWindow.setLength(newLength <= 0 ? 0 : newLength);
+      }
+      int newEnd = offset + event.getNewLength();
+      int newLine = document.getLineNumber(newEnd);
+      int lineChange = newLine - oldLine;
+      myTaskFile.incrementLines(oldLine + 1, lineChange);
+      int newEndOffsetInLine = offset + e.getNewLength() - document.getLineStartOffset(newLine);
+      int oldEndOffsetInLine = offset + e.getOldLength() - oldLineStartOffset;
+      myTaskFile.updateLine(lineChange, oldLine, newEndOffsetInLine, oldEndOffsetInLine);
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java
new file mode 100644
index 0000000..7565d76
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyEditorFactoryListener.java
@@ -0,0 +1,100 @@
+package com.jetbrains.python.edu;
+
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.event.EditorFactoryEvent;
+import com.intellij.openapi.editor.event.EditorFactoryListener;
+import com.intellij.openapi.editor.event.EditorMouseAdapter;
+import com.intellij.openapi.editor.event.EditorMouseEvent;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.edu.course.StudyStatus;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import org.jetbrains.annotations.NotNull;
+
+import java.awt.*;
+
+
+class StudyEditorFactoryListener implements EditorFactoryListener {
+
+  /**
+   * draws selected task window if there is one located in mouse position
+   */
+  private static class WindowSelectionListener extends EditorMouseAdapter {
+    private final TaskFile myTaskFile;
+
+    WindowSelectionListener(TaskFile taskFile) {
+      myTaskFile = taskFile;
+    }
+
+    @Override
+    public void mouseClicked(EditorMouseEvent e) {
+      Editor editor = e.getEditor();
+      Point point = e.getMouseEvent().getPoint();
+      LogicalPosition pos = editor.xyToLogicalPosition(point);
+      TaskWindow taskWindow = myTaskFile.getTaskWindow(editor.getDocument(), pos);
+      if (taskWindow != null) {
+        myTaskFile.setSelectedTaskWindow(taskWindow);
+        taskWindow.draw(editor, taskWindow.getStatus() != StudyStatus.Solved, true);
+      }
+      else {
+        myTaskFile.drawAllWindows(editor);
+      }
+    }
+  }
+
+  @Override
+  public void editorCreated(@NotNull final EditorFactoryEvent event) {
+    final Editor editor = event.getEditor();
+
+    final Project project = editor.getProject();
+    if (project == null) {
+      return;
+    }
+    ApplicationManager.getApplication().invokeLater(
+      new Runnable() {
+        @Override
+        public void run() {
+          ApplicationManager.getApplication().runWriteAction(new Runnable() {
+            @Override
+            public void run() {
+              Document document = editor.getDocument();
+              VirtualFile openedFile = FileDocumentManager.getInstance().getFile(document);
+              if (openedFile != null) {
+                StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+                TaskFile taskFile = taskManager.getTaskFile(openedFile);
+                if (taskFile != null) {
+                  taskFile.navigateToFirstTaskWindow(editor);
+                  editor.addEditorMouseListener(new WindowSelectionListener(taskFile));
+                  StudyDocumentListener listener = new StudyDocumentListener(taskFile);
+                  StudyEditor.addDocumentListener(document, listener);
+                  document.addDocumentListener(listener);
+                  taskFile.drawAllWindows(editor);
+                }
+              }
+            }
+          });
+        }
+      }
+    );
+  }
+
+  @Override
+  public void editorReleased(@NotNull EditorFactoryEvent event) {
+    Editor editor = event.getEditor();
+    Document document = editor.getDocument();
+    StudyDocumentListener listener = StudyEditor.getListener(document);
+    if (listener != null) {
+      document.removeDocumentListener(listener);
+      StudyEditor.removeListener(document);
+    }
+    editor.getMarkupModel().removeAllHighlighters();
+    editor.getSelectionModel().removeSelection();
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java
new file mode 100644
index 0000000..1377128
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyHighlightErrorFilter.java
@@ -0,0 +1,23 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.codeInsight.highlighting.HighlightErrorFilter;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiErrorElement;
+import org.jetbrains.annotations.NotNull;
+import com.jetbrains.python.edu.course.TaskFile;
+
+/**
+ * author: liana
+ * data: 7/23/14.
+ */
+public class StudyHighlightErrorFilter extends HighlightErrorFilter {
+  @Override
+  public boolean shouldHighlightErrorElement(@NotNull final PsiErrorElement element) {
+    VirtualFile file = element.getContainingFile().getVirtualFile();
+    Project project = element.getProject();
+    StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+    TaskFile taskFile = taskManager.getTaskFile(file);
+    return taskFile == null;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java
new file mode 100644
index 0000000..34776f3
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInitialConfigurator.java
@@ -0,0 +1,67 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.codeInsight.CodeInsightSettings;
+import com.intellij.ide.RecentProjectsManagerBase;
+import com.intellij.ide.ui.UISettings;
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.project.ex.ProjectManagerEx;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.platform.templates.github.ZipUtil;
+import com.intellij.util.PathUtil;
+import com.intellij.util.messages.MessageBus;
+import org.jetbrains.annotations.NonNls;
+
+import java.io.File;
+import java.io.IOException;
+
+@SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "UtilityClassWithPublicConstructor"})
+public class StudyInitialConfigurator {
+  private static final Logger LOG = Logger.getInstance(StudyInitialConfigurator.class.getName()
+  );
+  @NonNls private static final String CONFIGURED = "StudyPyCharm.InitialConfiguration";
+
+
+  /**
+   * @noinspection UnusedParameters
+   */
+  public StudyInitialConfigurator(MessageBus bus,
+                                  UISettings uiSettings,
+                                  CodeInsightSettings codeInsightSettings,
+                                  final PropertiesComponent propertiesComponent,
+                                  FileTypeManager fileTypeManager,
+                                  final ProjectManagerEx projectManager,
+                                  RecentProjectsManagerBase recentProjectsManager) {
+    if (!propertiesComponent.getBoolean(CONFIGURED, false)) {
+      final File file = new File(getCoursesRoot(), "introduction_course.zip");
+      final File newCourses = new File(PathManager.getConfigPath(), "courses");
+      try {
+        FileUtil.createDirectory(newCourses);
+        String fileName = file.getName();
+        String unzippedName = fileName.substring(0, fileName.indexOf("."));
+        File courseDir = new File(newCourses, unzippedName);
+        ZipUtil.unzip(null, courseDir, file, null, null, true);
+
+      }
+      catch (IOException e) {
+        LOG.warn("Couldn't copy bundled courses " + e);
+      }
+    }
+  }
+
+  public static File getCoursesRoot() {
+    @NonNls String jarPath = PathUtil.getJarPathForClass(StudyInitialConfigurator.class);
+    if (jarPath.endsWith(".jar")) {
+      final File jarFile = new File(jarPath);
+
+
+      File pluginBaseDir = jarFile.getParentFile();
+      return new File(pluginBaseDir, "courses");
+    }
+
+    return new File(jarPath , "courses");
+  }
+
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java
new file mode 100644
index 0000000..4f34bfb
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyInstructionPainter.java
@@ -0,0 +1,47 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.openapi.fileEditor.impl.EditorEmptyTextPainter;
+import com.intellij.openapi.fileEditor.impl.EditorsSplitters;
+import com.intellij.openapi.util.Couple;
+import com.intellij.ui.Gray;
+import com.intellij.ui.JBColor;
+import com.intellij.util.PairFunction;
+import com.intellij.util.ui.GraphicsUtil;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.ui.StudyCondition;
+
+import java.awt.*;
+
+/**
+ * author: liana
+ * data: 7/29/14.
+ */
+public class StudyInstructionPainter extends EditorEmptyTextPainter {
+  @Override
+  public void paintEmptyText(final EditorsSplitters splitters, Graphics g) {
+    if (!StudyCondition.VALUE) {
+      super.paintEmptyText(splitters, g);
+      return;
+    }
+    boolean isDarkBackground = UIUtil.isUnderDarcula();
+    UIUtil.applyRenderingHints(g);
+    GraphicsUtil.setupAntialiasing(g, true, false);
+    g.setColor(new JBColor(isDarkBackground ? Gray._230 : Gray._80, Gray._160));
+    g.setFont(UIUtil.getLabelFont().deriveFont(isDarkBackground ? 24f : 20f));
+
+    UIUtil.TextPainter painter = new UIUtil.TextPainter().withLineSpacing(1.5f);
+
+    painter.appendLine("PyCharm Educational Edition").underlined(new JBColor(Gray._150, Gray._180));
+    painter.appendLine("Navigate to the next task window with Ctrl + Enter").smaller().withBullet();
+    painter.appendLine("Navigate between task windows with Ctrl + < and Ctrl + >").smaller().withBullet();
+    painter.appendLine("Get hint for the task window using Ctrl + 7").smaller().withBullet();
+    painter.appendLine("To see your progress open the 'Course Description' panel").smaller().withBullet();
+                       painter.draw(g, new PairFunction<Integer, Integer, Couple<Integer>>() {
+                         @Override
+                         public Couple<Integer> fun(Integer width, Integer height) {
+                           Dimension s = splitters.getSize();
+                           return Couple.of((s.width - width) / 2, (s.height - height) / 2);
+                         }
+                       });
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java
new file mode 100644
index 0000000..38f1c2f
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyResourceManger.java
@@ -0,0 +1,5 @@
+package com.jetbrains.python.edu;
+
+public interface StudyResourceManger {
+  String USER_TESTER = "user_tester.py";
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java
new file mode 100644
index 0000000..213c1f7
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyTaskManager.java
@@ -0,0 +1,296 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.ide.ui.UISettings;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.actionSystem.ex.AnActionListener;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.*;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.keymap.Keymap;
+import com.intellij.openapi.keymap.KeymapManager;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.DumbAwareRunnable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.StartupManager;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileAdapter;
+import com.intellij.openapi.vfs.VirtualFileEvent;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.wm.*;
+import com.intellij.util.xmlb.XmlSerializer;
+import com.jetbrains.python.edu.actions.StudyNextWindowAction;
+import com.jetbrains.python.edu.actions.StudyPrevWindowAction;
+import com.jetbrains.python.edu.actions.StudyShowHintAction;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.Lesson;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.ui.StudyCondition;
+import com.jetbrains.python.edu.ui.StudyToolWindowFactory;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of class which contains all the information
+ * about study in context of current project
+ */
+
+@State(
+  name = "StudySettings",
+  storages = {
+    @Storage(
+      id = "others",
+      file = "$PROJECT_CONFIG_DIR$/study_project.xml",
+      scheme = StorageScheme.DIRECTORY_BASED
+    )}
+)
+public class StudyTaskManager implements ProjectComponent, PersistentStateComponent<Element>, DumbAware {
+  public static final String COURSE_ELEMENT = "courseElement";
+  private static Map<String, StudyTaskManager> myTaskManagers = new HashMap<String, StudyTaskManager>();
+  private static Map<String, String> myDeletedShortcuts = new HashMap<String, String>();
+  private final Project myProject;
+  private Course myCourse;
+  private FileCreatedListener myListener;
+
+
+  public void setCourse(Course course) {
+    myCourse = course;
+  }
+
+  private StudyTaskManager(@NotNull final Project project) {
+    myTaskManagers.put(project.getBasePath(), this);
+    myProject = project;
+  }
+
+
+  @Nullable
+  public Course getCourse() {
+    return myCourse;
+  }
+
+  @Nullable
+  @Override
+  public Element getState() {
+    Element el = new Element("taskManager");
+    if (myCourse != null) {
+      Element courseElement = new Element(COURSE_ELEMENT);
+      XmlSerializer.serializeInto(myCourse, courseElement);
+      el.addContent(courseElement);
+    }
+    return el;
+  }
+
+  @Override
+  public void loadState(Element el) {
+    myCourse = XmlSerializer.deserialize(el.getChild(COURSE_ELEMENT), Course.class);
+    if (myCourse != null) {
+      myCourse.init(true);
+    }
+  }
+
+  @Override
+  public void projectOpened() {
+    ApplicationManager.getApplication().invokeLater(new DumbAwareRunnable() {
+      @Override
+      public void run() {
+        ApplicationManager.getApplication().runWriteAction(new DumbAwareRunnable() {
+          @Override
+          public void run() {
+            if (myCourse != null) {
+              StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
+                @Override
+                public void run() {
+                  ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.PROJECT_VIEW).show(null);
+                  FileEditor[] editors = FileEditorManager.getInstance(myProject).getSelectedEditors();
+                  if (editors.length > 0) {
+                    JComponent focusedComponent = editors[0].getPreferredFocusedComponent();
+                    if (focusedComponent != null) {
+                      IdeFocusManager.getInstance(myProject).requestFocus(focusedComponent, true);
+                    }
+                  }
+                }
+              });
+              UISettings.getInstance().HIDE_TOOL_STRIPES = false;
+              UISettings.getInstance().fireUISettingsChanged();
+              ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject);
+              String toolWindowId = StudyToolWindowFactory.STUDY_TOOL_WINDOW;
+              //TODO:decide smth with tool window position
+              try {
+                Method method = toolWindowManager.getClass().getDeclaredMethod("registerToolWindow", String.class,
+                                                                               JComponent.class,
+                                                                               ToolWindowAnchor.class,
+                                                                               boolean.class, boolean.class, boolean.class);
+                method.setAccessible(true);
+                method.invoke(toolWindowManager, toolWindowId, null, ToolWindowAnchor.LEFT, true, true, true);
+              }
+              catch (Exception e) {
+                final ToolWindow toolWindow = toolWindowManager.getToolWindow(toolWindowId);
+                if (toolWindow == null)
+                  toolWindowManager.registerToolWindow(toolWindowId, true, ToolWindowAnchor.RIGHT, myProject, true);
+              }
+
+              final ToolWindow studyToolWindow = toolWindowManager.getToolWindow(toolWindowId);
+              if (studyToolWindow != null) {
+                StudyUtils.updateStudyToolWindow(myProject);
+                studyToolWindow.show(null);
+              }
+              addShortcut(StudyNextWindowAction.SHORTCUT, StudyNextWindowAction.ACTION_ID);
+              addShortcut(StudyPrevWindowAction.SHORTCUT, StudyPrevWindowAction.ACTION_ID);
+              addShortcut(StudyShowHintAction.SHORTCUT, StudyShowHintAction.ACTION_ID);
+              addShortcut(StudyNextWindowAction.SHORTCUT2, StudyNextWindowAction.ACTION_ID);
+            }
+          }
+        });
+      }
+    });
+  }
+
+
+  private static void addShortcut(@NotNull final String shortcutString, @NotNull final String actionIdString) {
+    Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
+    Shortcut studyActionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcutString), null);
+    String[] actionsIds = keymap.getActionIds(studyActionShortcut);
+    for (String actionId : actionsIds) {
+      myDeletedShortcuts.put(actionId, shortcutString);
+      keymap.removeShortcut(actionId, studyActionShortcut);
+    }
+    keymap.addShortcut(actionIdString, studyActionShortcut);
+  }
+
+  @Override
+  public void projectClosed() {
+    StudyCondition.VALUE = false;
+    if (myCourse != null) {
+      ToolWindowManager.getInstance(myProject).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW).getContentManager()
+        .removeAllContents(false);
+      if (!myDeletedShortcuts.isEmpty()) {
+        for (Map.Entry<String, String> shortcut : myDeletedShortcuts.entrySet()) {
+          Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
+          Shortcut actionShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(shortcut.getValue()), null);
+          keymap.addShortcut(shortcut.getKey(), actionShortcut);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void initComponent() {
+    EditorFactory.getInstance().addEditorFactoryListener(new StudyEditorFactoryListener(), myProject);
+    ActionManager.getInstance().addAnActionListener(new AnActionListener() {
+      @Override
+      public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
+        AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
+        for (AnAction newAction : newGroupActions) {
+          if (newAction == action) {
+            myListener =  new FileCreatedListener();
+            VirtualFileManager.getInstance().addVirtualFileListener(myListener);
+            break;
+          }
+        }
+      }
+
+      @Override
+      public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
+        AnAction[] newGroupActions = ((ActionGroup)ActionManager.getInstance().getAction("NewGroup")).getChildren(null);
+        for (AnAction newAction : newGroupActions) {
+          if (newAction == action) {
+            VirtualFileManager.getInstance().removeVirtualFileListener(myListener);
+          }
+        }
+      }
+
+      @Override
+      public void beforeEditorTyping(char c, DataContext dataContext) {
+
+      }
+    });
+  }
+
+  @Override
+  public void disposeComponent() {
+  }
+
+  @NotNull
+  @Override
+  public String getComponentName() {
+    return "StudyTaskManager";
+  }
+
+  public static StudyTaskManager getInstance(@NotNull final Project project) {
+    StudyTaskManager item = myTaskManagers.get(project.getBasePath());
+    return item != null ? item : new StudyTaskManager(project);
+  }
+
+
+  @Nullable
+  public TaskFile getTaskFile(@NotNull final VirtualFile file) {
+    if (myCourse == null) {
+      return null;
+    }
+    VirtualFile taskDir = file.getParent();
+    if (taskDir != null) {
+      String taskDirName = taskDir.getName();
+      if (taskDirName.contains(Task.TASK_DIR)) {
+        VirtualFile lessonDir = taskDir.getParent();
+        if (lessonDir != null) {
+          String lessonDirName = lessonDir.getName();
+          int lessonIndex = StudyUtils.getIndex(lessonDirName, Lesson.LESSON_DIR);
+          List<Lesson> lessons = myCourse.getLessons();
+          if (!StudyUtils.indexIsValid(lessonIndex, lessons)) {
+            return null;
+          }
+          Lesson lesson = lessons.get(lessonIndex);
+          int taskIndex = StudyUtils.getIndex(taskDirName, Task.TASK_DIR);
+          List<Task> tasks = lesson.getTaskList();
+          if (!StudyUtils.indexIsValid(taskIndex, tasks)) {
+            return null;
+          }
+          Task task = tasks.get(taskIndex);
+          return task.getFile(file.getName());
+        }
+      }
+    }
+    return null;
+  }
+
+  class FileCreatedListener extends VirtualFileAdapter {
+    @Override
+    public void fileCreated(@NotNull VirtualFileEvent event) {
+      VirtualFile createdFile = event.getFile();
+      VirtualFile taskDir = createdFile.getParent();
+      String taskLogicalName = Task.TASK_DIR;
+      if (taskDir != null && taskDir.getName().contains(taskLogicalName)) {
+        int taskIndex = StudyUtils.getIndex(taskDir.getName(), taskLogicalName);
+        VirtualFile lessonDir = taskDir.getParent();
+        String lessonLogicalName = Lesson.LESSON_DIR;
+        if (lessonDir != null && lessonDir.getName().contains(lessonLogicalName)) {
+          int lessonIndex = StudyUtils.getIndex(lessonDir.getName(), lessonLogicalName);
+          if (myCourse != null) {
+            List<Lesson> lessons = myCourse.getLessons();
+            if (StudyUtils.indexIsValid(lessonIndex, lessons)) {
+              Lesson lesson = lessons.get(lessonIndex);
+              List<Task> tasks = lesson.getTaskList();
+              if (StudyUtils.indexIsValid(taskIndex, tasks)) {
+                Task task = tasks.get(taskIndex);
+                TaskFile taskFile = new TaskFile();
+                taskFile.init(task, false);
+                taskFile.setUserCreated(true);
+                task.getTaskFiles().put(createdFile.getName(), taskFile);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java
new file mode 100644
index 0000000..d3ac1da
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/StudyUtils.java
@@ -0,0 +1,151 @@
+package com.jetbrains.python.edu;
+
+import com.intellij.ide.SaveAndSyncHandlerImpl;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.ui.StudyToolWindowFactory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.util.Collection;
+
+public class StudyUtils {
+  private static final Logger LOG = Logger.getInstance(StudyUtils.class.getName());
+  public static void closeSilently(Closeable stream) {
+    if (stream != null) {
+      try {
+        stream.close();
+      }
+      catch (IOException e) {
+        // close silently
+      }
+    }
+  }
+
+  public static boolean isZip(String fileName) {
+    return fileName.contains(".zip");
+  }
+
+  public static <T> T getFirst(Iterable<T> container) {
+    return container.iterator().next();
+  }
+
+  public static boolean indexIsValid(int index, Collection collection) {
+    int size = collection.size();
+    return index >= 0 && index < size;
+  }
+
+  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+  @Nullable
+  public static String getFileText(String parentDir, String fileName, boolean wrapHTML) {
+
+    File inputFile = parentDir !=null ? new File(parentDir, fileName) : new File(fileName);
+    if (!inputFile.exists()) return null;
+    StringBuilder taskText = new StringBuilder();
+    BufferedReader reader = null;
+    try {
+      reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile)));
+      String line;
+      while ((line = reader.readLine()) != null) {
+        taskText.append(line).append("\n");
+        if (wrapHTML) {
+          taskText.append("<br>");
+        }
+      }
+      return wrapHTML ? UIUtil.toHtml(taskText.toString()) : taskText.toString();
+    }
+    catch (IOException e) {
+      LOG.error("Failed to get file text from file " + fileName, e);
+    }
+    finally {
+      closeSilently(reader);
+    }
+    return null;
+  }
+
+  public static void updateAction(AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    presentation.setEnabled(false);
+    Project project = e.getProject();
+    if (project != null) {
+      FileEditor[] editors = FileEditorManager.getInstance(project).getAllEditors();
+      for (FileEditor editor : editors) {
+        if (editor instanceof StudyEditor) {
+          presentation.setEnabled(true);
+        }
+      }
+    }
+  }
+
+  public static void updateStudyToolWindow(Project project) {
+    ToolWindowManager.getInstance(project).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW).getContentManager().removeAllContents(false);
+    StudyToolWindowFactory factory =  new StudyToolWindowFactory();
+    factory.createToolWindowContent(project, ToolWindowManager.getInstance(project).getToolWindow(StudyToolWindowFactory.STUDY_TOOL_WINDOW));
+  }
+
+  public  static void synchronize() {
+    FileDocumentManager.getInstance().saveAllDocuments();
+    SaveAndSyncHandlerImpl.refreshOpenFiles();
+    VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
+  }
+
+  /**
+   * Gets number index in directory names like "task1", "lesson2"
+   *
+   * @param fullName    full name of directory
+   * @param logicalName part of name without index
+   * @return index of object
+   */
+  public static int getIndex(@NotNull final String fullName, @NotNull final String logicalName) {
+    if (!fullName.contains(logicalName)) {
+      throw new IllegalArgumentException();
+    }
+    return Integer.parseInt(fullName.substring(logicalName.length())) - 1;
+  }
+
+  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+  public static VirtualFile flushWindows(Document document, TaskFile taskFile, VirtualFile file) {
+    VirtualFile taskDir = file.getParent();
+    VirtualFile fileWindows = null;
+    if (taskDir != null) {
+      String name = file.getNameWithoutExtension() + "_windows";
+      PrintWriter printWriter = null;
+      try {
+
+        fileWindows = taskDir.createChildData(taskFile, name);
+        printWriter = new PrintWriter(new FileOutputStream(fileWindows.getPath()));
+        for (TaskWindow taskWindow : taskFile.getTaskWindows()) {
+          if (!taskWindow.isValid(document)) {
+            continue;
+          }
+          int start = taskWindow.getRealStartOffset(document);
+          String windowDescription = document.getText(new TextRange(start, start + taskWindow.getLength()));
+          printWriter.println("#study_plugin_window = " + windowDescription);
+        }
+      }
+      catch (IOException e) {
+       LOG.error(e);
+      }
+      finally {
+        closeSilently(printWriter);
+        synchronize();
+      }
+    }
+    return fileWindows;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java
new file mode 100644
index 0000000..f8e10c9
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyCheckAction.java
@@ -0,0 +1,340 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.ide.projectView.ProjectView;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.JBColor;
+import com.jetbrains.python.edu.StudyDocumentListener;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.*;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.sdk.PythonSdkType;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import java.util.List;
+
+public class StudyCheckAction extends DumbAwareAction {
+
+  private static final Logger LOG = Logger.getInstance(StudyCheckAction.class.getName());
+  public static final String PYTHONPATH = "PYTHONPATH";
+
+  static class StudyTestRunner {
+    public static final String TEST_OK = "#study_plugin test OK";
+    private static final String TEST_FAILED = "#study_plugin FAILED + ";
+    private final Task myTask;
+    private final VirtualFile myTaskDir;
+
+    StudyTestRunner(Task task, VirtualFile taskDir) {
+      myTask = task;
+      myTaskDir = taskDir;
+    }
+
+    Process launchTests(Project project, String executablePath) throws ExecutionException {
+      Sdk sdk = PythonSdkType.findPythonSdk(ModuleManager.getInstance(project).getModules()[0]);
+      File testRunner = new File(myTaskDir.getPath(), myTask.getTestFile());
+      GeneralCommandLine commandLine = new GeneralCommandLine();
+      commandLine.setWorkDirectory(myTaskDir.getPath());
+      final Map<String, String> env = commandLine.getEnvironment();
+      final VirtualFile courseDir = project.getBaseDir();
+      if (courseDir != null)
+        env.put(PYTHONPATH, courseDir.getPath());
+      if (sdk != null) {
+        String pythonPath = sdk.getHomePath();
+        if (pythonPath != null) {
+          commandLine.setExePath(pythonPath);
+          commandLine.addParameter(testRunner.getPath());
+          final Course course = StudyTaskManager.getInstance(project).getCourse();
+          assert course != null;
+          commandLine.addParameter(new File(course.getResourcePath()).getParent());
+          commandLine.addParameter(FileUtil.toSystemDependentName(executablePath));
+          return commandLine.createProcess();
+        }
+      }
+      return null;
+    }
+
+
+    String getPassedTests(Process p) {
+      InputStream testOutput = p.getInputStream();
+      BufferedReader testOutputReader = new BufferedReader(new InputStreamReader(testOutput));
+      String line;
+      try {
+        while ((line = testOutputReader.readLine()) != null) {
+          if (line.contains(TEST_FAILED)) {
+             return line.substring(TEST_FAILED.length(), line.length());
+          }
+        }
+      }
+      catch (IOException e) {
+        LOG.error(e);
+      }
+      finally {
+        StudyUtils.closeSilently(testOutputReader);
+      }
+      return TEST_OK;
+    }
+  }
+
+  public void check(@NotNull final Project project) {
+    ApplicationManager.getApplication().runWriteAction(new Runnable() {
+      @Override
+      public void run() {
+        CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
+          @Override
+          public void run() {
+        final Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+        if (selectedEditor != null) {
+          final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+          final VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+          if (openedFile != null) {
+            StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+            final TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+            List<VirtualFile> filesToDelete = new ArrayList<VirtualFile>();
+            if (selectedTaskFile != null) {
+              final VirtualFile taskDir = openedFile.getParent();
+              Task currentTask = selectedTaskFile.getTask();
+              StudyStatus oldStatus = currentTask.getStatus();
+              Map<String, TaskFile> taskFiles = selectedTaskFile.getTask().getTaskFiles();
+              for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) {
+                String name = entry.getKey();
+                TaskFile taskFile = entry.getValue();
+                VirtualFile virtualFile = taskDir.findChild(name);
+                if (virtualFile == null) {
+                  continue;
+                }
+                VirtualFile windowFile = StudyUtils.flushWindows(FileDocumentManager.getInstance().getDocument(virtualFile), taskFile, virtualFile);
+                filesToDelete.add(windowFile);
+                FileDocumentManager.getInstance().saveAllDocuments();
+              }
+
+              StudyRunAction runAction = (StudyRunAction)ActionManager.getInstance().getAction(StudyRunAction.ACTION_ID);
+              if (runAction != null && currentTask.getTaskFiles().size() == 1) {
+                runAction.run(project);
+              }
+              final StudyTestRunner testRunner = new StudyTestRunner(currentTask, taskDir);
+              Process testProcess = null;
+              try {
+                testProcess = testRunner.launchTests(project, openedFile.getPath());
+              }
+              catch (ExecutionException e) {
+                LOG.error(e);
+              }
+              if (testProcess != null) {
+                String failedMessage = testRunner.getPassedTests(testProcess);
+                if (failedMessage.equals(StudyTestRunner.TEST_OK)) {
+                  currentTask.setStatus(StudyStatus.Solved, oldStatus);
+                  StudyUtils.updateStudyToolWindow(project);
+                  selectedTaskFile.drawAllWindows(selectedEditor);
+                  ProjectView.getInstance(project).refresh();
+                  for (VirtualFile file:filesToDelete) {
+                    try {
+                      file.delete(this);
+                    }
+                    catch (IOException e) {
+                      LOG.error(e);
+                    }
+                  }
+                  createTestResultPopUp("Congratulations!", JBColor.GREEN, project);
+                  return;
+                }
+                for (Map.Entry<String, TaskFile> entry : taskFiles.entrySet()) {
+                  String name = entry.getKey();
+                  TaskFile taskFile = entry.getValue();
+                  TaskFile answerTaskFile = new TaskFile();
+                  VirtualFile virtualFile = taskDir.findChild(name);
+                  if (virtualFile == null) {
+                    continue;
+                  }
+                  VirtualFile answerFile = getCopyWithAnswers(taskDir, virtualFile, taskFile, answerTaskFile);
+                  for (TaskWindow taskWindow : answerTaskFile.getTaskWindows()) {
+                    Document document = FileDocumentManager.getInstance().getDocument(virtualFile);
+                    if (document == null) {
+                      continue;
+                    }
+                    if (!taskWindow.isValid(document)) {
+                      continue;
+                    }
+                    check(project, taskWindow, answerFile, answerTaskFile, taskFile, document, testRunner, virtualFile);
+                  }
+                  FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(virtualFile);
+                  Editor editor = null;
+                  if (fileEditor instanceof StudyEditor) {
+                    StudyEditor studyEditor = (StudyEditor) fileEditor;
+                    editor = studyEditor.getEditor();
+                  }
+
+                  if (editor != null) {
+                    taskFile.drawAllWindows(editor);
+                    StudyUtils.synchronize();
+                  }
+                  try {
+                    answerFile.delete(this);
+                  }
+                  catch (IOException e) {
+                    LOG.error(e);
+                  }
+                }
+                for (VirtualFile file:filesToDelete) {
+                  try {
+                    file.delete(this);
+                  }
+                  catch (IOException e) {
+                    LOG.error(e);
+                  }
+                }
+                currentTask.setStatus(StudyStatus.Failed, oldStatus);
+                StudyUtils.updateStudyToolWindow(project);
+                createTestResultPopUp(failedMessage, JBColor.RED, project);
+              }
+            }
+          }
+        }
+
+         }
+      });
+      }
+    });
+  }
+
+  private void check(Project project,
+                     TaskWindow taskWindow,
+                     VirtualFile answerFile,
+                     TaskFile answerTaskFile,
+                     TaskFile usersTaskFile,
+                     Document usersDocument,
+                     StudyTestRunner testRunner,
+                     VirtualFile openedFile) {
+
+    try {
+       VirtualFile windowCopy = answerFile.copy(this, answerFile.getParent(), answerFile.getNameWithoutExtension() + "_window" + taskWindow.getIndex() + ".py");
+      final FileDocumentManager documentManager = FileDocumentManager.getInstance();
+      final Document windowDocument = documentManager.getDocument(windowCopy);
+      if (windowDocument != null) {
+        StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+        Course course = taskManager.getCourse();
+        Task task = usersTaskFile.getTask();
+        int taskNum = task.getIndex() + 1;
+        int lessonNum = task.getLesson().getIndex() + 1;
+        assert course != null;
+        String pathToResource = FileUtil.join(new File(course.getResourcePath()).getParent(), Lesson.LESSON_DIR + lessonNum,  Task.TASK_DIR + taskNum);
+        File resourceFile = new File(pathToResource, windowCopy.getName());
+        FileUtil.copy(new File(pathToResource, openedFile.getName()), resourceFile);
+        TaskFile windowTaskFile = new TaskFile();
+        TaskFile.copy(answerTaskFile, windowTaskFile);
+        StudyDocumentListener listener = new StudyDocumentListener(windowTaskFile);
+        windowDocument.addDocumentListener(listener);
+        int start = taskWindow.getRealStartOffset(windowDocument);
+        int end = start + taskWindow.getLength();
+        TaskWindow userTaskWindow = usersTaskFile.getTaskWindows().get(taskWindow.getIndex());
+        int userStart = userTaskWindow.getRealStartOffset(usersDocument);
+        int userEnd = userStart + userTaskWindow.getLength();
+        String text = usersDocument.getText(new TextRange(userStart, userEnd));
+        windowDocument.replaceString(start, end, text);
+        ApplicationManager.getApplication().runWriteAction(new Runnable() {
+          @Override
+          public void run() {
+            documentManager.saveDocument(windowDocument);
+          }
+        });
+        VirtualFile fileWindows = StudyUtils.flushWindows(windowDocument, windowTaskFile, windowCopy);
+        Process smartTestProcess = testRunner.launchTests(project, windowCopy.getPath());
+        boolean res = testRunner.getPassedTests(smartTestProcess).equals(StudyTestRunner.TEST_OK);
+        userTaskWindow.setStatus(res ? StudyStatus.Solved : StudyStatus.Failed, StudyStatus.Unchecked);
+        windowCopy.delete(this);
+        fileWindows.delete(this);
+        if (!resourceFile.delete()) {
+          LOG.error("failed to delete", resourceFile.getPath());
+        }
+      }
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+    catch (ExecutionException e) {
+      LOG.error(e);
+    }
+  }
+
+
+  private VirtualFile getCopyWithAnswers(final VirtualFile taskDir,
+                                         final VirtualFile file,
+                                         final TaskFile source,
+                                         TaskFile target) {
+    VirtualFile copy = null;
+    try {
+
+      copy = file.copy(this, taskDir, file.getNameWithoutExtension() +"_answers.py");
+      final FileDocumentManager documentManager = FileDocumentManager.getInstance();
+      final Document document = documentManager.getDocument(copy);
+      if (document != null) {
+        TaskFile.copy(source, target);
+        StudyDocumentListener listener = new StudyDocumentListener(target);
+        document.addDocumentListener(listener);
+        for (TaskWindow taskWindow : target.getTaskWindows()) {
+          if (!taskWindow.isValid(document)) {
+            continue;
+          }
+          final int start = taskWindow.getRealStartOffset(document);
+          final int end = start + taskWindow.getLength();
+          final String text = taskWindow.getPossibleAnswer();
+          document.replaceString(start, end, text);
+        }
+        ApplicationManager.getApplication().runWriteAction(new Runnable() {
+          @Override
+          public void run() {
+            documentManager.saveDocument(document);
+          }
+        });
+      }
+    }
+    catch (IOException e) {
+      LOG.error(e);
+    }
+
+
+    return copy;
+  }
+
+  private static void createTestResultPopUp(final String text, Color color, @NotNull final Project project) {
+    BalloonBuilder balloonBuilder =
+      JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text, null, color, null);
+    Balloon balloon = balloonBuilder.createBalloon();
+    StudyEditor studyEditor = StudyEditor.getSelectedStudyEditor(project);
+    assert studyEditor != null;
+    JButton checkButton = studyEditor.getCheckButton();
+    balloon.showInCenterOf(checkButton);
+  }
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      check(project);
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java
new file mode 100644
index 0000000..5b9a6fe
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyEditInputAction.java
@@ -0,0 +1,213 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.ui.UISettings;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupAdapter;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.LightweightWindowEvent;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.IdeFocusManager;
+import com.intellij.ui.tabs.TabInfo;
+import com.intellij.ui.tabs.TabsListener;
+import com.intellij.ui.tabs.impl.JBEditorTabs;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.UserTest;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.ui.StudyTestContentPanel;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StudyEditInputAction extends DumbAwareAction {
+
+  public static final String TEST_TAB_NAME = "test";
+  public static final String USER_TEST_INPUT = "input";
+  public static final String USER_TEST_OUTPUT = "output";
+  private static final Logger LOG = Logger.getInstance(StudyEditInputAction.class.getName());
+  private JBEditorTabs tabbedPane;
+  private Map<TabInfo, UserTest> myEditableTabs = new HashMap<TabInfo, UserTest>();
+
+  public void showInput(final Project project) {
+    final Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+    if (selectedEditor != null) {
+      FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+      final VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+      StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(project);
+      assert openedFile != null;
+      TaskFile taskFile = studyTaskManager.getTaskFile(openedFile);
+      assert taskFile != null;
+      final Task currentTask = taskFile.getTask();
+      tabbedPane = new JBEditorTabs(project, ActionManager.getInstance(), IdeFocusManager.findInstance(), project);
+      tabbedPane.addListener(new TabsListener.Adapter() {
+        @Override
+        public void selectionChanged(TabInfo oldSelection, TabInfo newSelection) {
+          if (newSelection.getIcon() != null) {
+            int tabCount = tabbedPane.getTabCount();
+            VirtualFile taskDir = openedFile.getParent();
+            VirtualFile testsDir = taskDir.findChild(Task.USER_TESTS);
+            assert testsDir != null;
+            UserTest userTest = createUserTest(testsDir, currentTask);
+            userTest.setEditable(true);
+            StudyTestContentPanel testContentPanel = new StudyTestContentPanel(userTest);
+            TabInfo testTab = addTestTab(tabbedPane.getTabCount(), testContentPanel, currentTask, true);
+            myEditableTabs.put(testTab, userTest);
+            tabbedPane.addTabSilently(testTab, tabCount - 1);
+            tabbedPane.select(testTab, true);
+          }
+        }
+      });
+      List<UserTest> userTests = currentTask.getUserTests();
+      int i = 1;
+      for (UserTest userTest : userTests) {
+        String inputFileText = StudyUtils.getFileText(null, userTest.getInput(), false);
+        String outputFileText = StudyUtils.getFileText(null, userTest.getOutput(), false);
+        StudyTestContentPanel myContentPanel = new StudyTestContentPanel(userTest);
+        myContentPanel.addInputContent(inputFileText);
+        myContentPanel.addOutputContent(outputFileText);
+        TabInfo testTab = addTestTab(i, myContentPanel, currentTask, userTest.isEditable());
+        tabbedPane.addTabSilently(testTab, i - 1);
+        if (userTest.isEditable()) {
+          myEditableTabs.put(testTab, userTest);
+        }
+        i++;
+      }
+      TabInfo plusTab = new TabInfo(new JPanel());
+      plusTab.setIcon(StudyIcons.Add);
+      tabbedPane.addTabSilently(plusTab, tabbedPane.getTabCount());
+      final JBPopup hint =
+        JBPopupFactory.getInstance().createComponentPopupBuilder(tabbedPane.getComponent(), tabbedPane.getComponent())
+          .setResizable(true)
+          .setMovable(true)
+          .setRequestFocus(true)
+          .createPopup();
+      StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project);
+      assert selectedStudyEditor != null;
+      hint.showInCenterOf(selectedStudyEditor.getComponent());
+      hint.addListener(new HintClosedListener(currentTask));
+    }
+  }
+
+
+  private static void flushBuffer(@NotNull final StringBuilder buffer, @NotNull final File file) {
+    PrintWriter printWriter = null;
+    try {
+      printWriter = new PrintWriter(new FileOutputStream(file));
+      printWriter.print(buffer.toString());
+    }
+    catch (FileNotFoundException e) {
+      LOG.error(e);
+    }
+    finally {
+      StudyUtils.closeSilently(printWriter);
+    }
+    StudyUtils.synchronize();
+  }
+
+  private static UserTest createUserTest(@NotNull final VirtualFile testsDir, @NotNull final Task currentTask) {
+    UserTest userTest = new UserTest();
+    List<UserTest> userTests = currentTask.getUserTests();
+    int testNum = userTests.size() + 1;
+    String inputName = USER_TEST_INPUT + testNum;
+    File inputFile = new File(testsDir.getPath(), inputName);
+    String outputName = USER_TEST_OUTPUT + testNum;
+    File outputFile = new File(testsDir.getPath(), outputName);
+    userTest.setInput(inputFile.getPath());
+    userTest.setOutput(outputFile.getPath());
+    userTests.add(userTest);
+    return userTest;
+  }
+
+  private TabInfo addTestTab(int nameIndex, final StudyTestContentPanel contentPanel, @NotNull final Task currentTask, boolean toBeClosable) {
+    TabInfo testTab = toBeClosable ? createClosableTab(contentPanel, currentTask) : new TabInfo(contentPanel);
+    return testTab.setText(TEST_TAB_NAME + String.valueOf(nameIndex));
+  }
+
+  private TabInfo createClosableTab(StudyTestContentPanel contentPanel, Task currentTask) {
+    TabInfo closableTab = new TabInfo(contentPanel);
+    final DefaultActionGroup tabActions = new DefaultActionGroup();
+    tabActions.add(new CloseTab(closableTab, currentTask));
+    closableTab.setTabLabelActions(tabActions, ActionPlaces.EDITOR_TAB);
+    return closableTab;
+  }
+
+  public void actionPerformed(AnActionEvent e) {
+    showInput(e.getProject());
+  }
+
+  private class HintClosedListener extends  JBPopupAdapter {
+    private final Task myTask;
+    private HintClosedListener(@NotNull final Task task) {
+      myTask = task;
+    }
+
+    @Override
+    public void onClosed(LightweightWindowEvent event) {
+      for (final UserTest userTest : myTask.getUserTests()) {
+        ApplicationManager.getApplication().runWriteAction(new Runnable() {
+          @Override
+          public void run() {
+            if (userTest.isEditable()) {
+              File inputFile = new File(userTest.getInput());
+              File outputFile = new File(userTest.getOutput());
+              flushBuffer(userTest.getInputBuffer(), inputFile);
+              flushBuffer(userTest.getOutputBuffer(), outputFile);
+            }
+          }
+        });
+      }
+    }
+  }
+
+  private class CloseTab extends AnAction implements DumbAware {
+
+    private final TabInfo myTabInfo;
+    private final Task myTask;
+
+    public CloseTab(final TabInfo info, @NotNull final Task task) {
+      myTabInfo = info;
+      myTask = task;
+    }
+
+    @Override
+    public void update(final AnActionEvent e) {
+      e.getPresentation().setIcon(tabbedPane.isEditorTabs() ? AllIcons.Actions.CloseNew : AllIcons.Actions.Close);
+      e.getPresentation().setHoveredIcon(tabbedPane.isEditorTabs() ? AllIcons.Actions.CloseNewHovered : AllIcons.Actions.CloseHovered);
+      e.getPresentation().setVisible(UISettings.getInstance().SHOW_CLOSE_BUTTON);
+      e.getPresentation().setText("Delete test");
+    }
+
+    @Override
+    public void actionPerformed(final AnActionEvent e) {
+      tabbedPane.removeTab(myTabInfo);
+      UserTest userTest = myEditableTabs.get(myTabInfo);
+      File testInputFile = new File(userTest.getInput());
+      File testOutputFile = new File(userTest.getOutput());
+      if (testInputFile.delete() && testOutputFile.delete()) {
+        StudyUtils.synchronize();
+      } else {
+        LOG.error("failed to delete user tests");
+      }
+      myTask.getUserTests().remove(userTest);
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java
new file mode 100644
index 0000000..81818a9
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextStudyTaskAction.java
@@ -0,0 +1,24 @@
+package com.jetbrains.python.edu.actions;
+
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.course.Task;
+
+import javax.swing.*;
+
+public class StudyNextStudyTaskAction extends StudyTaskNavigationAction {
+
+  @Override
+  protected JButton getButton(StudyEditor selectedStudyEditor) {
+    return selectedStudyEditor.getNextTaskButton();
+  }
+
+  @Override
+  protected String getNavigationFinishedMessage() {
+    return "It's the last task";
+  }
+
+  @Override
+  protected Task getTargetTask(Task sourceTask) {
+    return sourceTask.next();
+  }
+}
\ No newline at end of file
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java
new file mode 100644
index 0000000..595aeef
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyNextWindowAction.java
@@ -0,0 +1,32 @@
+package com.jetbrains.python.edu.actions;
+
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.TaskWindow;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * move caret to next task window
+ */
+public class StudyNextWindowAction extends StudyWindowNavigationAction {
+  public static final String ACTION_ID = "NextWindow";
+  public static final String SHORTCUT = "ctrl pressed PERIOD";
+  public static final String SHORTCUT2 = "ctrl pressed ENTER";
+
+  public StudyNextWindowAction() {
+    super("NextWindowAction", "Select next window", StudyIcons.Next);
+  }
+
+  @Override
+  protected TaskWindow getNextTaskWindow(@NotNull final TaskWindow window) {
+    int index = window.getIndex();
+    List<TaskWindow> windows = window.getTaskFile().getTaskWindows();
+    if (StudyUtils.indexIsValid(index, windows)) {
+      int newIndex = index + 1;
+        return newIndex == windows.size() ? windows.get(0) : windows.get(newIndex);
+    }
+    return null;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java
new file mode 100644
index 0000000..3474561
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPrevWindowAction.java
@@ -0,0 +1,34 @@
+package com.jetbrains.python.edu.actions;
+
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.TaskWindow;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * author: liana
+ * data: 6/30/14.
+ */
+public class StudyPrevWindowAction extends StudyWindowNavigationAction {
+  public static final String ACTION_ID = "PrevWindowAction";
+  public static final String SHORTCUT = "ctrl pressed COMMA";
+
+  public StudyPrevWindowAction() {
+    super("PrevWindowAction", "Select previous window", StudyIcons.Prev);
+  }
+
+
+  @Nullable
+  @Override
+  protected TaskWindow getNextTaskWindow(@NotNull final TaskWindow window) {
+    int prevIndex = window.getIndex() - 1;
+    List<TaskWindow> windows = window.getTaskFile().getTaskWindows();
+    if (StudyUtils.indexIsValid(prevIndex, windows)) {
+      return windows.get(prevIndex);
+    }
+    return null;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java
new file mode 100644
index 0000000..bc26c28
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyPreviousStudyTaskAction.java
@@ -0,0 +1,25 @@
+package com.jetbrains.python.edu.actions;
+
+
+import com.jetbrains.python.edu.editor.StudyEditor;
+import com.jetbrains.python.edu.course.Task;
+
+import javax.swing.*;
+
+public class StudyPreviousStudyTaskAction extends StudyTaskNavigationAction {
+
+  @Override
+  protected JButton getButton(StudyEditor selectedStudyEditor) {
+    return selectedStudyEditor.getPrevTaskButton();
+  }
+
+  @Override
+  protected String getNavigationFinishedMessage() {
+    return "It's already the first task";
+  }
+
+  @Override
+  protected Task getTargetTask(Task sourceTask) {
+    return sourceTask.prev();
+  }
+}
\ No newline at end of file
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java
new file mode 100644
index 0000000..f8abb0b
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRefreshTaskAction.java
@@ -0,0 +1,122 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.ide.projectView.ProjectView;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.IdeFocusManager;
+import com.jetbrains.python.edu.StudyDocumentListener;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.*;
+import com.jetbrains.python.edu.editor.StudyEditor;
+
+import java.io.*;
+
+public class StudyRefreshTaskAction extends DumbAwareAction {
+  private static final Logger LOG = Logger.getInstance(StudyRefreshTaskAction.class.getName());
+
+  public void refresh(final Project project) {
+        ApplicationManager.getApplication().invokeLater(new Runnable() {
+          @Override
+          public void run() {
+            ApplicationManager.getApplication().runWriteAction(new Runnable() {
+              @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
+              @Override
+              public void run() {
+                final Editor editor = StudyEditor.getSelectedEditor(project);
+                assert editor != null;
+                final Document document = editor.getDocument();
+                StudyDocumentListener listener = StudyEditor.getListener(document);
+                if (listener != null) {
+                  document.removeDocumentListener(listener);
+                }
+                final int lineCount = document.getLineCount();
+                if (lineCount != 0) {
+                  CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
+                    @Override
+                    public void run() {
+                      document.deleteString(0, document.getLineEndOffset(lineCount - 1));
+                    }
+                  });
+                }
+                StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+                Course course = taskManager.getCourse();
+                assert course != null;
+                File resourceFile = new File(course.getResourcePath());
+                File resourceRoot = resourceFile.getParentFile();
+                FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+                VirtualFile openedFile = fileDocumentManager.getFile(document);
+                assert openedFile != null;
+                final TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+                assert selectedTaskFile != null;
+                Task currentTask = selectedTaskFile.getTask();
+                String lessonDir = Lesson.LESSON_DIR + String.valueOf(currentTask.getLesson().getIndex() + 1);
+                String taskDir = Task.TASK_DIR + String.valueOf(currentTask.getIndex() + 1);
+                File pattern = new File(new File(new File(resourceRoot, lessonDir), taskDir), openedFile.getName());
+                BufferedReader reader = null;
+                try {
+                  reader = new BufferedReader(new InputStreamReader(new FileInputStream(pattern)));
+                  String line;
+                  StringBuilder patternText = new StringBuilder();
+                  while ((line = reader.readLine()) != null) {
+                    patternText.append(line);
+                    patternText.append("\n");
+                  }
+                  int patternLength = patternText.length();
+                  if (patternText.charAt(patternLength - 1) == '\n') {
+                    patternText.delete(patternLength - 1, patternLength);
+                  }
+                  document.setText(patternText);
+                  StudyStatus oldStatus = currentTask.getStatus();
+                  LessonInfo lessonInfo = currentTask.getLesson().getLessonInfo();
+                  lessonInfo.update(oldStatus, -1);
+                  lessonInfo.update(StudyStatus.Unchecked, +1);
+                  StudyUtils.updateStudyToolWindow(project);
+                  for (TaskWindow taskWindow : selectedTaskFile.getTaskWindows()) {
+                    taskWindow.reset();
+                  }
+                  ProjectView.getInstance(project).refresh();
+                  if (listener != null) {
+                    document.addDocumentListener(listener);
+                  }
+                  selectedTaskFile.drawAllWindows(editor);
+                  IdeFocusManager.getInstance(project).requestFocus(editor.getContentComponent(), true);
+                  selectedTaskFile.navigateToFirstTaskWindow(editor);
+                  BalloonBuilder balloonBuilder =
+                    JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("You can now start again", MessageType.INFO, null);
+                  Balloon balloon = balloonBuilder.createBalloon();
+                  StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project);
+                  assert selectedStudyEditor != null;
+                  balloon.showInCenterOf(selectedStudyEditor.getRefreshButton());
+                }
+                catch (FileNotFoundException e1) {
+                  LOG.error(e1);
+                }
+                catch (IOException e1) {
+                  LOG.error(e1);
+                }
+                finally {
+                  StudyUtils.closeSilently(reader);
+                }
+              }
+            });
+          }
+        });
+  }
+
+  public void actionPerformed(AnActionEvent e) {
+    refresh(e.getProject());
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java
new file mode 100644
index 0000000..71e95de
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyRunAction.java
@@ -0,0 +1,89 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.RunContentExecutor;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.OSProcessHandler;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.sdk.PythonSdkType;
+import com.jetbrains.python.edu.StudyResourceManger;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.editor.StudyEditor;
+
+import java.io.File;
+
+public class StudyRunAction extends DumbAwareAction {
+  private static final Logger LOG = Logger.getInstance(StudyRunAction.class.getName());
+  public static final String ACTION_ID = "StudyRunAction";
+
+  public void run(Project project) {
+    Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+    FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+    assert selectedEditor != null;
+    VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+    StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+    if (openedFile != null && openedFile.getCanonicalPath() != null) {
+      String filePath = openedFile.getCanonicalPath();
+      GeneralCommandLine cmd = new GeneralCommandLine();
+      cmd.setWorkDirectory(openedFile.getParent().getCanonicalPath());
+      Sdk sdk = PythonSdkType.findPythonSdk(ModuleManager.getInstance(project).getModules()[0]);
+      if (sdk != null) {
+        String pythonPath = sdk.getHomePath();
+        if (pythonPath != null) {
+          cmd.setExePath(pythonPath);
+          TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+          assert selectedTaskFile != null;
+          Task currentTask = selectedTaskFile.getTask();
+          if (!currentTask.getUserTests().isEmpty()) {
+            cmd.addParameter(new File(project.getBaseDir().getPath(), StudyResourceManger.USER_TESTER).getPath());
+            cmd.addParameter(pythonPath);
+            cmd.addParameter(filePath);
+            Process p;
+            try {
+              p = cmd.createProcess();
+            }
+            catch (ExecutionException e) {
+              LOG.error(e);
+              return;
+            }
+            ProcessHandler handler = new OSProcessHandler(p);
+
+            RunContentExecutor executor = new RunContentExecutor(project, handler);
+            Disposer.register(project, executor);
+            executor.run();
+            return;
+          }
+          try {
+            cmd.addParameter(filePath);
+            Process p = cmd.createProcess();
+            ProcessHandler handler = new OSProcessHandler(p);
+
+            RunContentExecutor executor = new RunContentExecutor(project, handler);
+            Disposer.register(project, executor);
+            executor.run();
+          }
+
+          catch (ExecutionException e) {
+            LOG.error(e);
+          }
+        }
+      }
+    }
+  }
+
+  public void actionPerformed(AnActionEvent e) {
+    run(e.getProject());
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java
new file mode 100644
index 0000000..1efa908
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyShowHintAction.java
@@ -0,0 +1,95 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.codeInsight.documentation.DocumentationComponent;
+import com.intellij.codeInsight.documentation.DocumentationManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import icons.StudyIcons;
+
+import java.io.File;
+
+public class StudyShowHintAction extends DumbAwareAction {
+  public static final String ACTION_ID = "ShowHintAction";
+  public static final String SHORTCUT = "ctrl pressed 7";
+
+  public StudyShowHintAction() {
+    super("Show hint", "Show hint", StudyIcons.ShowHint);
+  }
+
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      DocumentationManager documentationManager = DocumentationManager.getInstance(project);
+      DocumentationComponent component = new DocumentationComponent(documentationManager);
+      Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+      FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+      assert selectedEditor != null;
+      VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+      if (openedFile != null) {
+        StudyTaskManager taskManager = StudyTaskManager.getInstance(e.getProject());
+        TaskFile taskFile = taskManager.getTaskFile(openedFile);
+        if (taskFile != null) {
+          PsiFile file = PsiManager.getInstance(project).findFile(openedFile);
+          if (file != null) {
+            LogicalPosition pos = selectedEditor.getCaretModel().getLogicalPosition();
+            TaskWindow taskWindow = taskFile.getTaskWindow(selectedEditor.getDocument(), pos);
+            if (taskWindow != null) {
+              String hint = taskWindow.getHint();
+              if (hint == null) {
+                return;
+              }
+              Course course = taskManager.getCourse();
+              if (course != null) {
+                File resourceFile = new File(course.getResourcePath());
+                File resourceRoot = resourceFile.getParentFile();
+                if (resourceRoot != null && resourceRoot.exists()) {
+                  File hintsDir = new File(resourceRoot, Course.HINTS_DIR);
+                  if (hintsDir.exists()) {
+                    String hintText = StudyUtils.getFileText(hintsDir.getAbsolutePath(), hint, true);
+                    if (hintText != null) {
+                      int offset = selectedEditor.getDocument().getLineStartOffset(pos.line) + pos.column;
+                      PsiElement element = file.findElementAt(offset);
+                      if (element != null) {
+                        component.setData(element, hintText, true, null);
+                        final JBPopup popup =
+                          JBPopupFactory.getInstance().createComponentPopupBuilder(component, component)
+                            .setDimensionServiceKey(project, DocumentationManager.JAVADOC_LOCATION_AND_SIZE, false)
+                            .setResizable(true)
+                            .setMovable(true)
+                            .setRequestFocus(true)
+                            .createPopup();
+                        component.setHint(popup);
+                        popup.showInBestPositionFor(selectedEditor);
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    StudyUtils.updateAction(e);
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java
new file mode 100644
index 0000000..b781e7d
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyTaskNavigationAction.java
@@ -0,0 +1,97 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.BalloonBuilder;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Lesson;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.editor.StudyEditor;
+
+import javax.swing.*;
+import java.util.Map;
+
+/**
+ * author: liana
+ * data: 7/21/14.
+ */
+abstract public class StudyTaskNavigationAction extends DumbAwareAction {
+  public void navigateTask(Project project) {
+    Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+    FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+    assert selectedEditor != null;
+    VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+    StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+    assert openedFile != null;
+    TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+    assert selectedTaskFile != null;
+    Task currentTask = selectedTaskFile.getTask();
+    Task nextTask = getTargetTask(currentTask);
+    if (nextTask == null) {
+      BalloonBuilder balloonBuilder =
+        JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(getNavigationFinishedMessage(), MessageType.INFO, null);
+      Balloon balloon = balloonBuilder.createBalloon();
+      StudyEditor selectedStudyEditor = StudyEditor.getSelectedStudyEditor(project);
+      balloon.showInCenterOf(getButton(selectedStudyEditor));
+      return;
+    }
+    for (VirtualFile file : FileEditorManager.getInstance(project).getOpenFiles()) {
+      FileEditorManager.getInstance(project).closeFile(file);
+    }
+    int nextTaskIndex = nextTask.getIndex();
+    int lessonIndex = nextTask.getLesson().getIndex();
+    Map<String, TaskFile> nextTaskFiles = nextTask.getTaskFiles();
+    if (nextTaskFiles.isEmpty()) {
+      return;
+    }
+    VirtualFile projectDir = project.getBaseDir();
+    String lessonDirName = Lesson.LESSON_DIR + String.valueOf(lessonIndex + 1);
+    if (projectDir == null) {
+      return;
+    }
+    VirtualFile lessonDir = projectDir.findChild(lessonDirName);
+    if (lessonDir == null) {
+      return;
+    }
+    String taskDirName = Task.TASK_DIR + String.valueOf(nextTaskIndex + 1);
+    VirtualFile taskDir = lessonDir.findChild(taskDirName);
+    if (taskDir == null) {
+      return;
+    }
+    VirtualFile shouldBeActive = null;
+    for (Map.Entry<String, TaskFile> entry : nextTaskFiles.entrySet()) {
+      String name = entry.getKey();
+      TaskFile taskFile = entry.getValue();
+      VirtualFile vf = taskDir.findChild(name);
+      if (vf != null) {
+        FileEditorManager.getInstance(project).openFile(vf, true);
+        if (!taskFile.getTaskWindows().isEmpty()) {
+          shouldBeActive = vf;
+        }
+      }
+    }
+    if (shouldBeActive != null) {
+      FileEditorManager.getInstance(project).openFile(shouldBeActive, true);
+    }
+  }
+
+  protected abstract JButton getButton(StudyEditor selectedStudyEditor);
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    navigateTask(e.getProject());
+  }
+
+  protected abstract String getNavigationFinishedMessage();
+
+  protected abstract Task getTargetTask(Task sourceTask);
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java
new file mode 100644
index 0000000..8c6b902
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/actions/StudyWindowNavigationAction.java
@@ -0,0 +1,65 @@
+package com.jetbrains.python.edu.actions;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.TaskFile;
+import com.jetbrains.python.edu.course.TaskWindow;
+import com.jetbrains.python.edu.editor.StudyEditor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+abstract public class StudyWindowNavigationAction extends DumbAwareAction {
+
+  public StudyWindowNavigationAction(String actionId, String description, Icon icon) {
+    super(actionId, description, icon);
+  }
+
+  public void navigateWindow(@NotNull final Project project) {
+      Editor selectedEditor = StudyEditor.getSelectedEditor(project);
+      if (selectedEditor != null) {
+        FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+        VirtualFile openedFile = fileDocumentManager.getFile(selectedEditor.getDocument());
+        if (openedFile != null) {
+          StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+          TaskFile selectedTaskFile = taskManager.getTaskFile(openedFile);
+          if (selectedTaskFile != null) {
+            TaskWindow selectedTaskWindow = selectedTaskFile.getSelectedTaskWindow();
+            if (selectedTaskWindow == null) {
+              return;
+            }
+            TaskWindow nextTaskWindow = getNextTaskWindow(selectedTaskWindow);
+            if (nextTaskWindow == null) {
+              return;
+            }
+            nextTaskWindow.draw(selectedEditor, true, true);
+            selectedTaskFile.setSelectedTaskWindow(nextTaskWindow);
+            }
+          }
+        }
+      }
+
+  @Nullable
+  protected abstract TaskWindow getNextTaskWindow(@NotNull final TaskWindow window);
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project == null) {
+      return;
+    }
+    navigateWindow(project);
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    StudyUtils.updateAction(e);
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java
new file mode 100644
index 0000000..89613ac
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Course.java
@@ -0,0 +1,104 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Course {
+
+  private static final Logger LOG = Logger.getInstance(Course.class.getName());
+  public static final String PLAYGROUND_DIR = "Playground";
+  public List<Lesson> lessons = new ArrayList<Lesson>();
+  public String description;
+  public String name;
+  public String myResourcePath = "";
+  public String author;
+  public static final String COURSE_DIR = "course";
+  public static final String HINTS_DIR = "hints";
+
+
+  public List<Lesson> getLessons() {
+    return lessons;
+  }
+
+  /**
+   * Initializes state of course
+   */
+  public void init(boolean isRestarted) {
+    for (Lesson lesson : lessons) {
+      lesson.init(this, isRestarted);
+    }
+  }
+
+  public String getAuthor() {
+    return author;
+  }
+
+  /**
+   * Creates course directory in project user created
+   *
+   * @param baseDir      project directory
+   * @param resourceRoot directory where original course is stored
+   */
+  public void create(@NotNull final VirtualFile baseDir, @NotNull final File resourceRoot) {
+    ApplicationManager.getApplication().invokeLater(
+      new Runnable() {
+        @Override
+        public void run() {
+          ApplicationManager.getApplication().runWriteAction(new Runnable() {
+            @Override
+            public void run() {
+              try {
+                for (int i = 0; i < lessons.size(); i++) {
+                  Lesson lesson = lessons.get(i);
+                  lesson.setIndex(i);
+                  lesson.create(baseDir, resourceRoot);
+                }
+                baseDir.createChildDirectory(this, PLAYGROUND_DIR);
+                File[] files = resourceRoot.listFiles(new FilenameFilter() {
+                  @Override
+                  public boolean accept(File dir, String name) {
+                   return !name.contains(Lesson.LESSON_DIR) && !name.equals("course.json") && !name.equals("hints");
+                  }
+                });
+                for (File file: files) {
+                  FileUtil.copy(file, new File(baseDir.getPath(), file.getName()));
+                }
+              }
+              catch (IOException e) {
+                LOG.error(e);
+              }
+            }
+          });
+        }
+      });
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setResourcePath(@NotNull final String resourcePath) {
+    myResourcePath = resourcePath;
+  }
+
+  public String getResourcePath() {
+    return myResourcePath;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java
new file mode 100644
index 0000000..9f820c1
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/CourseInfo.java
@@ -0,0 +1,52 @@
+package com.jetbrains.python.edu.course;
+
+/**
+ * Implementation of class which contains information to be shawn in course description in tool window
+ * and when project is being created
+ */
+public class CourseInfo {
+  private String myName;
+  private String myAuthor;
+  private String myDescription;
+  public static CourseInfo INVALID_COURSE = new CourseInfo("", "", "");
+
+  public CourseInfo(String name, String author, String description) {
+    myName = name;
+    myAuthor = author;
+    myDescription = description;
+  }
+
+  public String getName() {
+    return myName;
+  }
+
+  public String getAuthor() {
+    return myAuthor;
+  }
+
+  public String getDescription() {
+    return myDescription;
+  }
+
+  @Override
+  public String toString() {
+    return myName;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    CourseInfo that = (CourseInfo)o;
+    return that.getName().equals(myName) && that.getAuthor().equals(myAuthor)
+           && that.getDescription().equals(myDescription);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = myName != null ? myName.hashCode() : 0;
+    result = 31 * result + (myAuthor != null ? myAuthor.hashCode() : 0);
+    result = 31 * result + (myDescription != null ? myDescription.hashCode() : 0);
+    return result;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java
new file mode 100644
index 0000000..3879d51
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Lesson.java
@@ -0,0 +1,109 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Transient;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Lesson implements Stateful{
+  public String name;
+  public List<Task> taskList = new ArrayList<Task>();
+  private Course myCourse = null;
+  public int myIndex = -1;
+  public static final String LESSON_DIR = "lesson";
+  public LessonInfo myLessonInfo = new LessonInfo();
+
+  public LessonInfo getLessonInfo() {
+    return myLessonInfo;
+  }
+
+  @Transient
+  public StudyStatus getStatus() {
+    for (Task task : taskList) {
+      StudyStatus taskStatus = task.getStatus();
+      if (taskStatus == StudyStatus.Unchecked || taskStatus == StudyStatus.Failed) {
+        return StudyStatus.Unchecked;
+      }
+    }
+    return StudyStatus.Solved;
+  }
+
+  @Override
+  public void setStatus(StudyStatus status, StudyStatus oldStatus) {
+    for (Task task : taskList) {
+      task.setStatus(status, oldStatus);
+    }
+  }
+
+  public List<Task> getTaskList() {
+    return taskList;
+  }
+
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  /**
+   * Creates lesson directory in its course folder in project user created
+   *
+   * @param courseDir    project directory of course
+   * @param resourceRoot directory where original lesson stored
+   * @throws java.io.IOException
+   */
+  public void create(@NotNull final VirtualFile courseDir, @NotNull final File resourceRoot) throws IOException {
+    String lessonDirName = LESSON_DIR + Integer.toString(myIndex + 1);
+    VirtualFile lessonDir = courseDir.createChildDirectory(this, lessonDirName);
+    for (int i = 0; i < taskList.size(); i++) {
+      Task task = taskList.get(i);
+      task.setIndex(i);
+      task.create(lessonDir, new File(resourceRoot, lessonDir.getName()));
+    }
+  }
+
+
+  /**
+   * Initializes state of lesson
+   *
+   * @param course course which lesson belongs to
+   */
+  public void init(final Course course, boolean isRestarted) {
+    myCourse = course;
+    myLessonInfo.setTaskNum(taskList.size());
+    myLessonInfo.setTaskUnchecked(taskList.size());
+    for (Task task : taskList) {
+      task.init(this, isRestarted);
+    }
+  }
+
+  public Lesson next() {
+    List<Lesson> lessons = myCourse.getLessons();
+    if (myIndex + 1 >= lessons.size()) {
+      return null;
+    }
+    return lessons.get(myIndex + 1);
+  }
+
+  public void setIndex(int index) {
+    myIndex = index;
+  }
+
+  public int getIndex() {
+    return myIndex;
+  }
+
+  public Lesson prev() {
+    if (myIndex - 1 < 0) {
+      return null;
+    }
+    return myCourse.getLessons().get(myIndex - 1);
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java
new file mode 100644
index 0000000..85e2eb8
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/LessonInfo.java
@@ -0,0 +1,60 @@
+package com.jetbrains.python.edu.course;
+
+/**
+ * Implementation of class which contains information about student progress in current lesson
+ */
+public class LessonInfo {
+  private int myTaskNum;
+  private int myTaskFailed;
+  private int myTaskSolved;
+  private int myTaskUnchecked;
+
+  public int getTaskNum() {
+    return myTaskNum;
+  }
+
+  public void setTaskNum(int taskNum) {
+    myTaskNum = taskNum;
+  }
+
+  public int getTaskFailed() {
+    return myTaskFailed;
+  }
+
+  public void setTaskFailed(int taskFailed) {
+    myTaskFailed = taskFailed;
+  }
+
+  public int getTaskSolved() {
+    return myTaskSolved;
+  }
+
+  public void setTaskSolved(int taskSolved) {
+    myTaskSolved = taskSolved;
+  }
+
+  public int getTaskUnchecked() {
+    return myTaskUnchecked;
+  }
+
+  public void setTaskUnchecked(int taskUnchecked) {
+    myTaskUnchecked = taskUnchecked;
+  }
+
+  public void update(StudyStatus status, int delta) {
+    switch (status) {
+      case Solved: {
+        myTaskSolved += delta;
+        break;
+      }
+      case Failed: {
+        myTaskFailed += delta;
+        break;
+      }
+      case Unchecked: {
+        myTaskUnchecked += delta;
+        break;
+      }
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java
new file mode 100644
index 0000000..3a16362
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Stateful.java
@@ -0,0 +1,6 @@
+package com.jetbrains.python.edu.course;
+
+public interface Stateful {
+  StudyStatus getStatus();
+  void setStatus(StudyStatus status, StudyStatus oldStatus);
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java
new file mode 100644
index 0000000..d95b42b
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/StudyStatus.java
@@ -0,0 +1,8 @@
+package com.jetbrains.python.edu.course;
+
+/**
+ * @see {@link TaskWindow#myStatus}
+ */
+public enum StudyStatus {
+  Unchecked, Solved, Failed
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java
new file mode 100644
index 0000000..2323412
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/Task.java
@@ -0,0 +1,201 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Transient;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import com.jetbrains.python.edu.StudyUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of task which contains task files, tests, input file for tests
+ */
+public class Task implements Stateful{
+  public static final String TASK_DIR = "task";
+  private static final String ourTestFile = "tests.py";
+  public String name;
+  private static final String ourTextFile = "task.html";
+  public Map<String, TaskFile> taskFiles = new HashMap<String, TaskFile>();
+  private Lesson myLesson;
+  public int myIndex;
+  public List<UserTest> userTests = new ArrayList<UserTest>();
+  public static final String USER_TESTS = "userTests";
+
+  public Map<String, TaskFile> getTaskFiles() {
+    return taskFiles;
+  }
+
+  @Transient
+  public StudyStatus getStatus() {
+    for (TaskFile taskFile : taskFiles.values()) {
+      StudyStatus taskFileStatus = taskFile.getStatus();
+      if (taskFileStatus == StudyStatus.Unchecked) {
+        return StudyStatus.Unchecked;
+      }
+      if (taskFileStatus == StudyStatus.Failed) {
+        return StudyStatus.Failed;
+      }
+    }
+    return StudyStatus.Solved;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public void setStatus(@NotNull final StudyStatus status, @NotNull final StudyStatus oldStatus) {
+    LessonInfo lessonInfo = myLesson.getLessonInfo();
+    if (status != oldStatus) {
+      lessonInfo.update(oldStatus, -1);
+      lessonInfo.update(status, +1);
+    }
+    for (TaskFile taskFile : taskFiles.values()) {
+      taskFile.setStatus(status, oldStatus);
+    }
+  }
+
+  public List<UserTest> getUserTests() {
+    return userTests;
+  }
+
+  public String getTestFile() {
+    return ourTestFile;
+  }
+
+  public String getText() {
+    return ourTextFile;
+  }
+
+  /**
+   * Creates task directory in its lesson folder in project user created
+   *
+   * @param lessonDir    project directory of lesson which task belongs to
+   * @param resourceRoot directory where original task file stored
+   * @throws java.io.IOException
+   */
+  public void create(@NotNull final VirtualFile lessonDir, @NotNull final File resourceRoot) throws IOException {
+    VirtualFile taskDir = lessonDir.createChildDirectory(this, TASK_DIR + Integer.toString(myIndex + 1));
+    File newResourceRoot = new File(resourceRoot, taskDir.getName());
+    int i = 0;
+    for (Map.Entry<String, TaskFile> taskFile : taskFiles.entrySet()) {
+      TaskFile taskFileContent = taskFile.getValue();
+      taskFileContent.setIndex(i);
+      i++;
+      taskFileContent.create(taskDir, newResourceRoot, taskFile.getKey());
+    }
+    File[] filesInTask = newResourceRoot.listFiles();
+    if (filesInTask != null) {
+      for (File file : filesInTask) {
+        String fileName = file.getName();
+        if (!isTaskFile(fileName)) {
+          File resourceFile = new File(newResourceRoot, fileName);
+          File fileInProject = new File(taskDir.getCanonicalPath(), fileName);
+          FileUtil.copy(resourceFile, fileInProject);
+        }
+      }
+    }
+  }
+
+  private boolean isTaskFile(@NotNull final String fileName) {
+    return taskFiles.get(fileName) != null;
+  }
+
+  @Nullable
+  public TaskFile getFile(@NotNull final String fileName) {
+    return taskFiles.get(fileName);
+  }
+
+  /**
+   * Initializes state of task file
+   *
+   * @param lesson lesson which task belongs to
+   */
+  public void init(final Lesson lesson, boolean isRestarted) {
+    myLesson = lesson;
+    for (TaskFile taskFile : taskFiles.values()) {
+      taskFile.init(this, isRestarted);
+    }
+  }
+
+  public Task next() {
+    Lesson currentLesson = this.myLesson;
+    List<Task> taskList = myLesson.getTaskList();
+    if (myIndex + 1 < taskList.size()) {
+      return taskList.get(myIndex + 1);
+    }
+    Lesson nextLesson = currentLesson.next();
+    if (nextLesson == null) {
+      return null;
+    }
+    return StudyUtils.getFirst(nextLesson.getTaskList());
+  }
+
+  public Task prev() {
+    Lesson currentLesson = this.myLesson;
+    if (myIndex - 1 >= 0) {
+      return myLesson.getTaskList().get(myIndex - 1);
+    }
+    Lesson prevLesson = currentLesson.prev();
+    if (prevLesson == null) {
+      return null;
+    }
+    //getting last task in previous lesson
+    return prevLesson.getTaskList().get(prevLesson.getTaskList().size() - 1);
+  }
+
+  public void setIndex(int index) {
+    myIndex = index;
+  }
+
+  public int getIndex() {
+    return myIndex;
+  }
+
+  public Lesson getLesson() {
+    return myLesson;
+  }
+
+
+  @Nullable
+  public VirtualFile getTaskDir(Project project) {
+    String lessonDirName = Lesson.LESSON_DIR + String.valueOf(myLesson.getIndex() + 1);
+    String taskDirName = TASK_DIR + String.valueOf(myIndex + 1);
+    VirtualFile courseDir = project.getBaseDir();
+    if (courseDir != null) {
+      VirtualFile lessonDir = courseDir.findChild(lessonDirName);
+      if (lessonDir != null) {
+        return lessonDir.findChild(taskDirName);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Gets text of resource file such as test input file or task text in needed format
+   *
+   * @param fileName name of resource file which should exist in task directory
+   * @param wrapHTML if it's necessary to wrap text with html tags
+   * @return text of resource file wrapped with html tags if necessary
+   */
+  @Nullable
+  public String getResourceText(@NotNull final Project project, @NotNull final String fileName, boolean wrapHTML) {
+    VirtualFile taskDir = getTaskDir(project);
+    if (taskDir != null) {
+      return StudyUtils.getFileText(taskDir.getCanonicalPath(), fileName, wrapHTML);
+    }
+    return null;
+  }
+
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java
new file mode 100644
index 0000000..4f17fc0
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskFile.java
@@ -0,0 +1,228 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.xmlb.annotations.Transient;
+import com.jetbrains.python.edu.StudyUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of task file which contains task windows for student to type in and
+ * which is visible to student in project view
+ */
+
+public class TaskFile implements Stateful{
+  public List<TaskWindow> taskWindows = new ArrayList<TaskWindow>();
+  private Task myTask;
+  @Transient
+  private TaskWindow mySelectedTaskWindow = null;
+  public int myIndex = -1;
+  private boolean myUserCreated = false;
+
+  /**
+   * @return if all the windows in task file are marked as resolved
+   */
+  @Transient
+  public StudyStatus getStatus() {
+    for (TaskWindow taskWindow : taskWindows) {
+      StudyStatus windowStatus = taskWindow.getStatus();
+      if (windowStatus == StudyStatus.Failed) {
+        return StudyStatus.Failed;
+      }
+      if (windowStatus == StudyStatus.Unchecked) {
+        return StudyStatus.Unchecked;
+      }
+    }
+    return StudyStatus.Solved;
+  }
+
+  public Task getTask() {
+    return myTask;
+  }
+
+  @Nullable
+  @Transient
+  public TaskWindow getSelectedTaskWindow() {
+    return mySelectedTaskWindow;
+  }
+
+  /**
+   * @param selectedTaskWindow window from this task file to be set as selected
+   */
+  public void setSelectedTaskWindow(@NotNull final TaskWindow selectedTaskWindow) {
+    if (selectedTaskWindow.getTaskFile() == this) {
+      mySelectedTaskWindow = selectedTaskWindow;
+    }
+    else {
+      throw new IllegalArgumentException("Window may be set as selected only in task file which it belongs to");
+    }
+  }
+
+  public List<TaskWindow> getTaskWindows() {
+    return taskWindows;
+  }
+
+  /**
+   * Creates task files in its task folder in project user created
+   *
+   * @param taskDir      project directory of task which task file belongs to
+   * @param resourceRoot directory where original task file stored
+   * @throws java.io.IOException
+   */
+  public void create(@NotNull final VirtualFile taskDir, @NotNull final File resourceRoot,
+                     @NotNull final String name) throws IOException {
+    String systemIndependentName = FileUtil.toSystemIndependentName(name);
+    final int index = systemIndependentName.lastIndexOf("/");
+    if (index > 0) {
+      systemIndependentName = systemIndependentName.substring(index + 1);
+    }
+    File resourceFile = new File(resourceRoot, name);
+    File fileInProject = new File(taskDir.getPath(), systemIndependentName);
+    FileUtil.copy(resourceFile, fileInProject);
+  }
+
+  public void drawAllWindows(Editor editor) {
+    for (TaskWindow taskWindow : taskWindows) {
+      taskWindow.draw(editor, false, false);
+    }
+  }
+
+
+  /**
+   * @param pos position in editor
+   * @return task window located in specified position or null if there is no task window in this position
+   */
+  @Nullable
+  public TaskWindow getTaskWindow(@NotNull final Document document, @NotNull final LogicalPosition pos) {
+    int line = pos.line;
+    if (line >= document.getLineCount()) {
+      return null;
+    }
+    int column = pos.column;
+    int offset = document.getLineStartOffset(line) + column;
+    for (TaskWindow tw : taskWindows) {
+      if (tw.getLine() <= line) {
+        int twStartOffset = tw.getRealStartOffset(document);
+        final int length = tw.getLength() > 0 ? tw.getLength() : 0;
+        int twEndOffset = twStartOffset + length;
+        if (twStartOffset <= offset && offset <= twEndOffset) {
+          return tw;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Updates task window lines
+   *
+   * @param startLine lines greater than this line and including this line will be updated
+   * @param change    change to be added to line numbers
+   */
+  public void incrementLines(int startLine, int change) {
+    for (TaskWindow taskTaskWindow : taskWindows) {
+      if (taskTaskWindow.getLine() >= startLine) {
+        taskTaskWindow.setLine(taskTaskWindow.getLine() + change);
+      }
+    }
+  }
+
+  /**
+   * Initializes state of task file
+   *
+   * @param task task which task file belongs to
+   */
+
+  public void init(final Task task, boolean isRestarted) {
+    myTask = task;
+    for (TaskWindow taskWindow : taskWindows) {
+      taskWindow.init(this, isRestarted);
+    }
+    Collections.sort(taskWindows);
+    for (int i = 0; i < taskWindows.size(); i++) {
+      taskWindows.get(i).setIndex(i);
+    }
+  }
+
+  /**
+   * @param index index of task file in list of task files of its task
+   */
+  public void setIndex(int index) {
+    myIndex = index;
+  }
+
+  /**
+   * Updates windows in specific line
+   *
+   * @param lineChange         change in line number
+   * @param line               line to be updated
+   * @param newEndOffsetInLine distance from line start to end of inserted fragment
+   * @param oldEndOffsetInLine distance from line start to end of changed fragment
+   */
+  public void updateLine(int lineChange, int line, int newEndOffsetInLine, int oldEndOffsetInLine) {
+    for (TaskWindow w : taskWindows) {
+      if ((w.getLine() == line) && (w.getStart() >= oldEndOffsetInLine)) {
+        int distance = w.getStart() - oldEndOffsetInLine;
+        if (lineChange != 0 || newEndOffsetInLine <= w.getStart()) {
+          w.setStart(distance + newEndOffsetInLine);
+          w.setLine(line + lineChange);
+        }
+      }
+    }
+  }
+
+  public static void copy(@NotNull final TaskFile source, @NotNull final TaskFile target) {
+    List<TaskWindow> sourceTaskWindows = source.getTaskWindows();
+    List<TaskWindow> windowsCopy = new ArrayList<TaskWindow>(sourceTaskWindows.size());
+    for (TaskWindow taskWindow : sourceTaskWindows) {
+      TaskWindow taskWindowCopy = new TaskWindow();
+      taskWindowCopy.setLine(taskWindow.getLine());
+      taskWindowCopy.setStart(taskWindow.getStart());
+      taskWindowCopy.setLength(taskWindow.getLength());
+      taskWindowCopy.setPossibleAnswer(taskWindow.getPossibleAnswer());
+      taskWindowCopy.setIndex(taskWindow.getIndex());
+      windowsCopy.add(taskWindowCopy);
+    }
+    target.setTaskWindows(windowsCopy);
+  }
+
+  public void setTaskWindows(List<TaskWindow> taskWindows) {
+    this.taskWindows = taskWindows;
+  }
+
+  public void setStatus(@NotNull final StudyStatus status, @NotNull final StudyStatus oldStatus) {
+    for (TaskWindow taskWindow : taskWindows) {
+      taskWindow.setStatus(status, oldStatus);
+    }
+  }
+
+  public void setUserCreated(boolean userCreated) {
+    myUserCreated = userCreated;
+  }
+
+  public boolean isUserCreated() {
+    return myUserCreated;
+  }
+
+  public void navigateToFirstTaskWindow(@NotNull final Editor editor) {
+    if (!taskWindows.isEmpty()) {
+      TaskWindow firstTaskWindow = StudyUtils.getFirst(taskWindows);
+      mySelectedTaskWindow = firstTaskWindow;
+      LogicalPosition taskWindowStart = new LogicalPosition(firstTaskWindow.getLine(), firstTaskWindow.getStart());
+      editor.getCaretModel().moveToLogicalPosition(taskWindowStart);
+      int startOffset = firstTaskWindow.getRealStartOffset(editor.getDocument());
+      int endOffset = startOffset + firstTaskWindow.getLength();
+      editor.getSelectionModel().setSelection(startOffset, endOffset);
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java
new file mode 100644
index 0000000..4fb112c
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/TaskWindow.java
@@ -0,0 +1,177 @@
+package com.jetbrains.python.edu.course;
+
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.colors.EditorColors;
+import com.intellij.openapi.editor.colors.EditorColorsManager;
+import com.intellij.openapi.editor.markup.HighlighterLayer;
+import com.intellij.openapi.editor.markup.HighlighterTargetArea;
+import com.intellij.openapi.editor.markup.RangeHighlighter;
+import com.intellij.openapi.editor.markup.TextAttributes;
+import com.intellij.ui.JBColor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Implementation of windows which user should type in
+ */
+
+
+public class TaskWindow implements Comparable, Stateful {
+
+  public int line = 0;
+  public int start = 0;
+  public String hint = "";
+  public String possibleAnswer = "";
+  public int length = 0;
+  private TaskFile myTaskFile;
+  public int myIndex = -1;
+  public int myInitialLine = -1;
+  public int myInitialStart = -1;
+  public int myInitialLength = -1;
+  public StudyStatus myStatus = StudyStatus.Unchecked;
+
+  public StudyStatus getStatus() {
+    return myStatus;
+  }
+
+  public void setStatus(StudyStatus status, StudyStatus oldStatus) {
+    myStatus = status;
+  }
+
+  public void setIndex(int index) {
+    myIndex = index;
+  }
+
+  public int getLength() {
+    return length;
+  }
+
+  public void setLength(int length) {
+    this.length = length;
+  }
+
+  public int getStart() {
+    return start;
+  }
+
+  public void setStart(int start) {
+    this.start = start;
+  }
+
+  public void setLine(int line) {
+    this.line = line;
+  }
+
+  public int getLine() {
+    return line;
+  }
+
+
+  /**
+   * Draw task window with color according to its status
+   */
+  public void draw(@NotNull final Editor editor, boolean drawSelection, boolean moveCaret) {
+    Document document = editor.getDocument();
+    if (!isValid(document)) {
+      return;
+    }
+    TextAttributes defaultTestAttributes =
+      EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.LIVE_TEMPLATE_ATTRIBUTES);
+    JBColor color = getColor();
+    int startOffset = document.getLineStartOffset(line) + start;
+    RangeHighlighter
+      rh = editor.getMarkupModel().addRangeHighlighter(startOffset, startOffset + length, HighlighterLayer.LAST + 1,
+                                                       new TextAttributes(defaultTestAttributes.getForegroundColor(),
+                                                                          defaultTestAttributes.getBackgroundColor(), color,
+                                                                          defaultTestAttributes.getEffectType(),
+                                                                          defaultTestAttributes.getFontType()),
+                                                       HighlighterTargetArea.EXACT_RANGE);
+    if (drawSelection) {
+      editor.getSelectionModel().setSelection(startOffset, startOffset + length);
+    }
+    if (moveCaret) {
+      editor.getCaretModel().moveToOffset(startOffset);
+    }
+    rh.setGreedyToLeft(true);
+    rh.setGreedyToRight(true);
+  }
+
+  public boolean isValid(@NotNull final Document document) {
+    boolean isLineValid = line < document.getLineCount() && line >= 0;
+    if (!isLineValid) return false;
+    boolean isStartValid = start >= 0 && start < document.getLineEndOffset(line);
+    boolean isLengthValid = (getRealStartOffset(document) + length) <= document.getTextLength();
+    return isLengthValid && isStartValid;
+  }
+
+  private JBColor getColor() {
+    if (myStatus == StudyStatus.Solved) {
+      return JBColor.GREEN;
+    }
+    if (myStatus == StudyStatus.Failed) {
+      return JBColor.RED;
+    }
+    return JBColor.BLUE;
+  }
+
+  public int getRealStartOffset(@NotNull final Document document) {
+    return document.getLineStartOffset(line) + start;
+  }
+
+  /**
+   * Initializes window
+   *
+   * @param file task file which window belongs to
+   */
+  public void init(final TaskFile file, boolean isRestarted) {
+    if (!isRestarted) {
+      myInitialLine = line;
+      myInitialLength = length;
+      myInitialStart = start;
+    }
+    myTaskFile = file;
+  }
+
+  public TaskFile getTaskFile() {
+    return myTaskFile;
+  }
+
+  @Override
+  public int compareTo(@NotNull Object o) {
+    TaskWindow taskWindow = (TaskWindow)o;
+    if (taskWindow.getTaskFile() != myTaskFile) {
+      throw new ClassCastException();
+    }
+    int lineDiff = line - taskWindow.line;
+    if (lineDiff == 0) {
+      return start - taskWindow.start;
+    }
+    return lineDiff;
+  }
+
+  /**
+   * Returns window to its initial state
+   */
+  public void reset() {
+    myStatus = StudyStatus.Unchecked;
+    line = myInitialLine;
+    start = myInitialStart;
+    length = myInitialLength;
+  }
+
+  public String getHint() {
+    return hint;
+  }
+
+  public String getPossibleAnswer() {
+    return possibleAnswer;
+  }
+
+  public void setPossibleAnswer(String possibleAnswer) {
+    this.possibleAnswer = possibleAnswer;
+  }
+
+  public int getIndex() {
+    return myIndex;
+  }
+}
\ No newline at end of file
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java b/python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java
new file mode 100644
index 0000000..8133e91
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/course/UserTest.java
@@ -0,0 +1,41 @@
+package com.jetbrains.python.edu.course;
+
+public class UserTest {
+  private String input;
+  private String output;
+  private StringBuilder myInputBuffer = new StringBuilder();
+  private StringBuilder myOutputBuffer =  new StringBuilder();
+  private boolean myEditable = false;
+
+  public String getInput() {
+    return input;
+  }
+
+  public void setInput(String input) {
+    this.input = input;
+  }
+
+  public String getOutput() {
+    return output;
+  }
+
+  public void setOutput(String output) {
+    this.output = output;
+  }
+
+  public StringBuilder getInputBuffer() {
+    return myInputBuffer;
+  }
+
+  public StringBuilder getOutputBuffer() {
+    return myOutputBuffer;
+  }
+
+  public boolean isEditable() {
+    return myEditable;
+  }
+
+  public void setEditable(boolean editable) {
+    myEditable = editable;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java
new file mode 100644
index 0000000..69c5acc
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyEditor.java
@@ -0,0 +1,347 @@
+package com.jetbrains.python.edu.editor;
+
+import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.editor.colors.EditorColorsManager;
+import com.intellij.openapi.editor.colors.EditorColorsScheme;
+import com.intellij.openapi.editor.impl.DocumentImpl;
+import com.intellij.openapi.fileEditor.*;
+import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
+import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorImpl;
+import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.Navigatable;
+import com.intellij.ui.HideableTitledPanel;
+import com.intellij.ui.JBColor;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.StudyDocumentListener;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.actions.*;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.text.MutableAttributeSet;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyledDocument;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeListener;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of StudyEditor which has panel with special buttons and task text
+ * also @see {@link com.jetbrains.python.edu.editor.StudyFileEditorProvider}
+ */
+public class StudyEditor implements TextEditor {
+  private static final String TASK_TEXT_HEADER = "Task Text";
+  private final FileEditor myDefaultEditor;
+  private final JComponent myComponent;
+  private JButton myCheckButton;
+  private JButton myNextTaskButton;
+  private JButton myPrevTaskButton;
+  private JButton myRefreshButton;
+  private static final Map<Document, StudyDocumentListener> myDocumentListeners = new HashMap<Document, StudyDocumentListener>();
+  private Project myProject;
+
+  public JButton getCheckButton() {
+    return myCheckButton;
+  }
+
+  public JButton getPrevTaskButton() {
+    return myPrevTaskButton;
+  }
+
+  private static JButton addButton(@NotNull final JComponent parentComponent, String toolTipText, Icon icon) {
+    JButton newButton = new JButton();
+    newButton.setToolTipText(toolTipText);
+    newButton.setIcon(icon);
+    newButton.setSize(new Dimension(icon.getIconWidth(), icon.getIconHeight()));
+    parentComponent.add(newButton);
+    return newButton;
+  }
+
+  public static void addDocumentListener(@NotNull final Document document, @NotNull final StudyDocumentListener listener) {
+    myDocumentListeners.put(document, listener);
+  }
+
+  @Nullable
+  public static StudyDocumentListener getListener(@NotNull final Document document) {
+    return myDocumentListeners.get(document);
+  }
+
+  public StudyEditor(@NotNull final Project project, @NotNull final VirtualFile file) {
+    myProject = project;
+    myDefaultEditor = TextEditorProvider.getInstance().createEditor(myProject, file);
+    myComponent = myDefaultEditor.getComponent();
+    JPanel studyPanel = new JPanel();
+    studyPanel.setLayout(new BoxLayout(studyPanel, BoxLayout.Y_AXIS));
+    TaskFile taskFile = StudyTaskManager.getInstance(myProject).getTaskFile(file);
+    if (taskFile != null) {
+      Task currentTask = taskFile.getTask();
+      String taskText = currentTask.getResourceText(project, currentTask.getText(), false);
+      initializeTaskText(studyPanel, taskText);
+      JPanel studyButtonPanel = new JPanel(new GridLayout(1, 2));
+      JPanel taskActionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+      studyButtonPanel.add(taskActionsPanel);
+      studyButtonPanel.add(new JPanel());
+      initializeButtons(taskActionsPanel, taskFile);
+      studyPanel.add(studyButtonPanel);
+      myComponent.add(studyPanel, BorderLayout.NORTH);
+    }
+  }
+
+  private static void initializeTaskText(JPanel studyPanel, @Nullable String taskText) {
+    JTextPane taskTextPane = new JTextPane();
+    taskTextPane.setContentType("text/html");
+    taskTextPane.setEditable(false);
+    taskTextPane.setText(taskText);
+    EditorColorsScheme editorColorsScheme = EditorColorsManager.getInstance().getGlobalScheme();
+    int fontSize = editorColorsScheme.getEditorFontSize();
+    String fontName = editorColorsScheme.getEditorFontName();
+    setJTextPaneFont(taskTextPane, new Font(fontName, Font.PLAIN, fontSize), JBColor.BLACK);
+    taskTextPane.setBackground(UIUtil.getPanelBackground());
+    taskTextPane.setBorder(new EmptyBorder(15, 20, 0, 100));
+    HideableTitledPanel taskTextPanel = new HideableTitledPanel(TASK_TEXT_HEADER, taskTextPane, true);
+    taskTextPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
+    studyPanel.add(taskTextPanel);
+  }
+
+  private static void setJTextPaneFont(JTextPane jtp, Font font, Color c) {
+    MutableAttributeSet attrs = jtp.getInputAttributes();
+    StyleConstants.setFontFamily(attrs, font.getFamily());
+    StyleConstants.setFontSize(attrs, font.getSize());
+    StyleConstants.setItalic(attrs, (font.getStyle() & Font.ITALIC) != 0);
+    StyleConstants.setBold(attrs, (font.getStyle() & Font.BOLD) != 0);
+    StyleConstants.setForeground(attrs, c);
+    StyledDocument doc = jtp.getStyledDocument();
+    doc.setCharacterAttributes(0, doc.getLength() + 1, attrs, false);
+  }
+
+  private void initializeButtons(@NotNull final JPanel taskActionsPanel, @NotNull final TaskFile taskFile) {
+    myCheckButton = addButton(taskActionsPanel, "Check task", StudyIcons.Resolve);
+    myPrevTaskButton = addButton(taskActionsPanel, "Prev Task", StudyIcons.Prev);
+    myNextTaskButton = addButton(taskActionsPanel, "Next Task", StudyIcons.Next);
+    myRefreshButton = addButton(taskActionsPanel, "Start task again", StudyIcons.Refresh24);
+    if (!taskFile.getTask().getUserTests().isEmpty()) {
+      JButton runButton = addButton(taskActionsPanel, "Run", StudyIcons.Run);
+      runButton.addActionListener(new ActionListener() {
+        @Override
+        public void actionPerformed(ActionEvent e) {
+          StudyRunAction studyRunAction = (StudyRunAction)ActionManager.getInstance().getAction("StudyRunAction");
+          studyRunAction.run(myProject);
+        }
+      });
+      JButton watchInputButton = addButton(taskActionsPanel, "Watch test input", StudyIcons.WatchInput);
+      watchInputButton.addActionListener(new ActionListener() {
+        @Override
+        public void actionPerformed(ActionEvent e) {
+          StudyEditInputAction studyEditInputAction = (StudyEditInputAction)ActionManager.getInstance().getAction("WatchInputAction");
+          studyEditInputAction.showInput(myProject);
+        }
+      });
+    }
+    myCheckButton.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        StudyCheckAction studyCheckAction = (StudyCheckAction)ActionManager.getInstance().getAction("CheckAction");
+        studyCheckAction.check(myProject);
+      }
+    });
+
+    myNextTaskButton.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        StudyNextStudyTaskAction studyNextTaskAction = (StudyNextStudyTaskAction)ActionManager.getInstance().getAction("NextTaskAction");
+        studyNextTaskAction.navigateTask(myProject);
+      }
+    });
+    myPrevTaskButton.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        StudyPreviousStudyTaskAction
+          prevTaskAction = (StudyPreviousStudyTaskAction)ActionManager.getInstance().getAction("PreviousTaskAction");
+        prevTaskAction.navigateTask(myProject);
+      }
+    });
+    myRefreshButton.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        StudyRefreshTaskAction studyRefreshTaskAction = (StudyRefreshTaskAction)ActionManager.getInstance().getAction("RefreshTaskAction");
+        studyRefreshTaskAction.refresh(myProject);
+      }
+    });
+  }
+
+  public JButton getNextTaskButton() {
+    return myNextTaskButton;
+  }
+
+  public JButton getRefreshButton() {
+    return myRefreshButton;
+  }
+
+  FileEditor getDefaultEditor() {
+    return myDefaultEditor;
+  }
+
+  @NotNull
+  @Override
+  public JComponent getComponent() {
+    return myComponent;
+  }
+
+  @Nullable
+  @Override
+  public JComponent getPreferredFocusedComponent() {
+    return myDefaultEditor.getPreferredFocusedComponent();
+  }
+
+  @NotNull
+  @Override
+  public String getName() {
+    return "Study Editor";
+  }
+
+  @NotNull
+  @Override
+  public FileEditorState getState(@NotNull FileEditorStateLevel level) {
+    return myDefaultEditor.getState(level);
+  }
+
+  @Override
+  public void setState(@NotNull FileEditorState state) {
+    myDefaultEditor.setState(state);
+  }
+
+  @Override
+  public boolean isModified() {
+    return myDefaultEditor.isModified();
+  }
+
+  @Override
+  public boolean isValid() {
+    return myDefaultEditor.isValid();
+  }
+
+  @Override
+  public void selectNotify() {
+    myDefaultEditor.selectNotify();
+  }
+
+  @Override
+  public void deselectNotify() {
+    myDefaultEditor.deselectNotify();
+  }
+
+  @Override
+  public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
+    myDefaultEditor.addPropertyChangeListener(listener);
+  }
+
+  @Override
+  public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
+    myDefaultEditor.removePropertyChangeListener(listener);
+  }
+
+  @Nullable
+  @Override
+  public BackgroundEditorHighlighter getBackgroundHighlighter() {
+    return myDefaultEditor.getBackgroundHighlighter();
+  }
+
+  @Nullable
+  @Override
+  public FileEditorLocation getCurrentLocation() {
+    return myDefaultEditor.getCurrentLocation();
+  }
+
+  @Nullable
+  @Override
+  public StructureViewBuilder getStructureViewBuilder() {
+    return myDefaultEditor.getStructureViewBuilder();
+  }
+
+  @Override
+  public void dispose() {
+    Disposer.dispose(myDefaultEditor);
+  }
+
+  @Nullable
+  @Override
+  public <T> T getUserData(@NotNull Key<T> key) {
+    return myDefaultEditor.getUserData(key);
+  }
+
+  @Override
+  public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
+    myDefaultEditor.putUserData(key, value);
+  }
+
+
+  @Nullable
+  public static StudyEditor getSelectedStudyEditor(@NotNull final Project project) {
+    try {
+      FileEditor fileEditor = FileEditorManagerEx.getInstanceEx(project).getSplitters().getCurrentWindow().
+        getSelectedEditor().getSelectedEditorWithProvider().getFirst();
+      if (fileEditor instanceof StudyEditor) {
+        return (StudyEditor)fileEditor;
+      }
+    } catch (Exception e) {
+      return null;
+    }
+    return null;
+  }
+
+  @Nullable
+  public static Editor getSelectedEditor(@NotNull final Project project) {
+    StudyEditor studyEditor = getSelectedStudyEditor(project);
+    if (studyEditor != null) {
+      FileEditor defaultEditor = studyEditor.getDefaultEditor();
+      if (defaultEditor instanceof PsiAwareTextEditorImpl) {
+        return ((PsiAwareTextEditorImpl)defaultEditor).getEditor();
+      }
+    }
+    return null;
+  }
+
+  public static void removeListener(Document document) {
+    myDocumentListeners.remove(document);
+  }
+
+  @NotNull
+  @Override
+  public Editor getEditor() {
+    if (myDefaultEditor instanceof TextEditor)
+      return ((TextEditor)myDefaultEditor).getEditor();
+    return EditorFactory.getInstance().createViewer(new DocumentImpl(""), myProject);
+  }
+
+  @Override
+  public boolean canNavigateTo(@NotNull Navigatable navigatable) {
+    if (myDefaultEditor instanceof TextEditor) {
+      ((TextEditor)myDefaultEditor).canNavigateTo(navigatable);
+    }
+    return false;
+  }
+
+  @Override
+  public void navigateTo(@NotNull Navigatable navigatable) {
+    if (myDefaultEditor instanceof TextEditor) {
+      ((TextEditor)myDefaultEditor).navigateTo(navigatable);
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java
new file mode 100644
index 0000000..631b5a9
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/editor/StudyFileEditorProvider.java
@@ -0,0 +1,64 @@
+package com.jetbrains.python.edu.editor;
+
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorPolicy;
+import com.intellij.openapi.fileEditor.FileEditorProvider;
+import com.intellij.openapi.fileEditor.FileEditorState;
+import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.TaskFile;
+
+/**
+ * User: lia
+ * Date: 10.05.14
+ * Time: 12:45
+ */
+class StudyFileEditorProvider implements FileEditorProvider, DumbAware {
+  static final private String EDITOR_TYPE_ID = "StudyEditor";
+  final private FileEditorProvider defaultTextEditorProvider = TextEditorProvider.getInstance();
+
+  @Override
+  public boolean accept(@NotNull Project project, @NotNull VirtualFile file) {
+    TaskFile taskFile = StudyTaskManager.getInstance(project).getTaskFile(file);
+    return taskFile != null && !taskFile.isUserCreated();
+  }
+
+  @NotNull
+  @Override
+  public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) {
+    return new StudyEditor(project, file);
+  }
+
+  @Override
+  public void disposeEditor(@NotNull FileEditor editor) {
+    defaultTextEditorProvider.disposeEditor(editor);
+  }
+
+  @NotNull
+  @Override
+  public FileEditorState readState(@NotNull Element sourceElement, @NotNull Project project, @NotNull VirtualFile file) {
+    return defaultTextEditorProvider.readState(sourceElement, project, file);
+  }
+
+  @Override
+  public void writeState(@NotNull FileEditorState state, @NotNull Project project, @NotNull Element targetElement) {
+    defaultTextEditorProvider.writeState(state, project, targetElement);
+  }
+
+  @NotNull
+  @Override
+  public String getEditorTypeId() {
+    return EDITOR_TYPE_ID;
+  }
+
+  @NotNull
+  @Override
+  public FileEditorPolicy getPolicy() {
+    return FileEditorPolicy.HIDE_DEFAULT_EDITOR;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java
new file mode 100644
index 0000000..abf648c
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyDirectoryNode.java
@@ -0,0 +1,112 @@
+package com.jetbrains.python.edu.projectView;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.SimpleTextAttributes;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.*;
+import icons.StudyIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class StudyDirectoryNode extends PsiDirectoryNode {
+  private final PsiDirectory myValue;
+  private final Project myProject;
+
+  public StudyDirectoryNode(@NotNull final Project project,
+                            PsiDirectory value,
+                            ViewSettings viewSettings) {
+    super(project, value, viewSettings);
+    myValue = value;
+    myProject = project;
+  }
+
+  @Override
+  protected void updateImpl(PresentationData data) {
+    data.setIcon(StudyIcons.Unchecked);
+    String valueName = myValue.getName();
+    StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(myProject);
+    Course course = studyTaskManager.getCourse();
+    if (course == null) {
+      return;
+    }
+    if (valueName.equals(myProject.getName())) {
+      data.clearText();
+      data.addText(course.getName(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, JBColor.BLUE));
+      data.addText(" (" + valueName + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES);
+      return;
+    }
+    if (valueName.contains(Task.TASK_DIR)) {
+      TaskFile file = null;
+      for (PsiElement child : myValue.getChildren()) {
+        VirtualFile virtualFile = child.getContainingFile().getVirtualFile();
+        file = studyTaskManager.getTaskFile(virtualFile);
+        if (file != null) {
+          break;
+        }
+      }
+      if (file != null) {
+        Task task = file.getTask();
+        setStudyAttributes(task, data, task.getName());
+      }
+    }
+    if (valueName.contains(Lesson.LESSON_DIR)) {
+      int lessonIndex = Integer.parseInt(valueName.substring(Lesson.LESSON_DIR.length())) - 1;
+      Lesson lesson = course.getLessons().get(lessonIndex);
+      setStudyAttributes(lesson, data, lesson.getName());
+    }
+
+    if (valueName.contains(Course.PLAYGROUND_DIR)) {
+      if (myValue.getParent() != null) {
+        if (!myValue.getParent().getName().contains(Course.PLAYGROUND_DIR)) {
+          data.setPresentableText(Course.PLAYGROUND_DIR);
+          data.setIcon(StudyIcons.Playground);
+          return;
+        }
+      }
+    }
+    data.setPresentableText(valueName);
+  }
+
+  @Override
+  public int getTypeSortWeight(boolean sortByType) {
+    String name = myValue.getName();
+    if (name.contains(Lesson.LESSON_DIR) || name.contains(Task.TASK_DIR)) {
+      String logicalName = name.contains(Lesson.LESSON_DIR) ? Lesson.LESSON_DIR : Task.TASK_DIR;
+      return StudyUtils.getIndex(name, logicalName) + 1;
+    }
+    return name.contains(Course.PLAYGROUND_DIR) ? 0 : 3;
+  }
+
+  private static void setStudyAttributes(Stateful stateful, PresentationData data, String additionalName) {
+    StudyStatus taskStatus = stateful.getStatus();
+    switch (taskStatus) {
+      case Unchecked: {
+        updatePresentation(data, additionalName, JBColor.blue, StudyIcons.Unchecked);
+        break;
+      }
+      case Solved: {
+        updatePresentation(data, additionalName, new JBColor(new Color(0, 134, 0), new Color(98, 150, 85)), StudyIcons.Checked);
+        break;
+      }
+      case Failed: {
+        updatePresentation(data, additionalName, JBColor.RED, StudyIcons.Failed);
+      }
+    }
+  }
+
+  private static void updatePresentation(PresentationData data, String additionalName, JBColor color, Icon icon) {
+    data.clearText();
+    data.addText(additionalName, new SimpleTextAttributes(Font.PLAIN, color));
+    data.setIcon(icon);
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java
new file mode 100644
index 0000000..e301bc3
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/projectView/StudyTreeStructureProvider.java
@@ -0,0 +1,83 @@
+package com.jetbrains.python.edu.projectView;
+
+import com.intellij.ide.projectView.TreeStructureProvider;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
+import com.intellij.ide.util.treeView.AbstractTreeNode;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.Task;
+import com.jetbrains.python.edu.course.TaskFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class StudyTreeStructureProvider implements TreeStructureProvider, DumbAware {
+  @NotNull
+  @Override
+  public Collection<AbstractTreeNode> modify(@NotNull AbstractTreeNode parent,
+                                             @NotNull Collection<AbstractTreeNode> children,
+                                             ViewSettings settings) {
+    if (!needModify(parent)) {
+      return children;
+    }
+    Collection<AbstractTreeNode> nodes = new ArrayList<AbstractTreeNode>();
+    for (AbstractTreeNode node : children) {
+      Project project = node.getProject();
+      if (project != null) {
+        if (node.getValue() instanceof PsiDirectory) {
+          PsiDirectory nodeValue = (PsiDirectory)node.getValue();
+          if (!nodeValue.getName().contains(Task.USER_TESTS)) {
+            StudyDirectoryNode newNode = new StudyDirectoryNode(project, nodeValue, settings);
+            nodes.add(newNode);
+          }
+        }
+        else {
+          if (parent instanceof StudyDirectoryNode) {
+            if (node instanceof PsiFileNode) {
+              PsiFileNode psiFileNode = (PsiFileNode)node;
+              VirtualFile virtualFile = psiFileNode.getVirtualFile();
+              if (virtualFile == null) {
+                return nodes;
+              }
+              TaskFile taskFile = StudyTaskManager.getInstance(project).getTaskFile(virtualFile);
+              if (taskFile != null) {
+                nodes.add(node);
+              }
+              String parentName = parent.getName();
+              if (parentName != null) {
+                if (parentName.equals(Course.PLAYGROUND_DIR)) {
+                  nodes.add(node);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    return nodes;
+  }
+
+  private static boolean needModify(AbstractTreeNode parent) {
+    Project project = parent.getProject();
+    if (project != null) {
+      StudyTaskManager studyTaskManager = StudyTaskManager.getInstance(project);
+      if (studyTaskManager.getCourse() == null) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Nullable
+  @Override
+  public Object getData(Collection<AbstractTreeNode> selected, String dataName) {
+    return null;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java
new file mode 100644
index 0000000..5add6c9
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyCondition.java
@@ -0,0 +1,25 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+import com.jetbrains.python.edu.StudyTaskManager;
+
+/**
+ * author: liana
+ * data: 7/29/14.
+ */
+public class StudyCondition implements Condition, DumbAware {
+  public static boolean VALUE = false;
+  @Override
+  public boolean value(Object o) {
+    if (o instanceof Project) {
+      Project project = (Project) o;
+      StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+      if (taskManager.getCourse() != null) {
+        VALUE = true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form
new file mode 100644
index 0000000..133c38d
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.form
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.edu.ui.StudyNewProjectPanel">
+  <grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="2" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+    <margin top="0" left="0" bottom="0" right="0"/>
+    <constraints>
+      <xy x="20" y="20" width="500" height="400"/>
+    </constraints>
+    <properties/>
+    <border type="none"/>
+    <children>
+      <grid id="54488" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+        <margin top="0" left="0" bottom="0" right="0"/>
+        <constraints>
+          <grid row="1" column="1" row-span="1" col-span="3" vsize-policy="2" hsize-policy="4" anchor="0" fill="3" indent="0" use-parent-layout="false">
+            <minimum-size width="-1" height="60"/>
+            <preferred-size width="-1" height="60"/>
+          </grid>
+        </constraints>
+        <properties/>
+        <border type="line">
+          <color color="-6709600"/>
+        </border>
+        <children>
+          <component id="213f6" class="javax.swing.JLabel" binding="myAuthorLabel">
+            <constraints>
+              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <text value=""/>
+            </properties>
+          </component>
+          <component id="d754d" class="javax.swing.JLabel" binding="myDescriptionLabel">
+            <constraints>
+              <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <text value=""/>
+            </properties>
+          </component>
+        </children>
+      </grid>
+      <component id="6c40c" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="81" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <font/>
+          <horizontalTextPosition value="0"/>
+          <text value="Courses:"/>
+        </properties>
+      </component>
+      <component id="21ac6" class="javax.swing.JComboBox" binding="myCoursesComboBox">
+        <constraints>
+          <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="7" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+      <component id="5c614" class="com.intellij.openapi.ui.FixedSizeButton" binding="myBrowseButton">
+        <constraints>
+          <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+            <minimum-size width="30" height="25"/>
+          </grid>
+        </constraints>
+        <properties/>
+      </component>
+      <component id="f1e10" class="javax.swing.JButton" binding="myRefreshButton">
+        <constraints>
+          <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false">
+            <minimum-size width="30" height="23"/>
+          </grid>
+        </constraints>
+        <properties>
+          <hideActionText value="false"/>
+          <text value=""/>
+          <toolTipText value="Refresh course list"/>
+          <verticalAlignment value="1"/>
+          <verticalTextPosition value="1"/>
+        </properties>
+      </component>
+    </children>
+  </grid>
+</form>
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java
new file mode 100644
index 0000000..0f1ec08
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyNewProjectPanel.java
@@ -0,0 +1,196 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.facet.ui.FacetValidatorsManager;
+import com.intellij.facet.ui.ValidationResult;
+import com.intellij.openapi.fileChooser.FileChooser;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.Consumer;
+import com.jetbrains.python.edu.StudyDirectoryProjectGenerator;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.CourseInfo;
+import icons.StudyIcons;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * author: liana
+ * data: 7/31/14.
+ */
+public class StudyNewProjectPanel{
+  private Set<CourseInfo> myAvailableCourses = new HashSet<CourseInfo>();
+  private JComboBox myCoursesComboBox;
+  private JButton myBrowseButton;
+  private JButton myRefreshButton;
+  private JPanel myContentPanel;
+  private JLabel myAuthorLabel;
+  private JLabel myDescriptionLabel;
+  private final StudyDirectoryProjectGenerator myGenerator;
+  private static final String CONNECTION_ERROR = "<html>Failed to download courses.<br>Check your Internet connection.</html>";
+  private static final String INVALID_COURSE = "Selected course is invalid";
+  private FacetValidatorsManager myValidationManager;
+
+  public StudyNewProjectPanel(StudyDirectoryProjectGenerator generator) {
+    myGenerator = generator;
+    Map<CourseInfo, File> courses = myGenerator.getCourses();
+    if (courses.isEmpty()) {
+      setError(CONNECTION_ERROR);
+    }
+    else {
+      myAvailableCourses = courses.keySet();
+      for (CourseInfo courseInfo : myAvailableCourses) {
+        myCoursesComboBox.addItem(courseInfo);
+      }
+      myAuthorLabel.setText("Author: " + StudyUtils.getFirst(myAvailableCourses).getAuthor());
+      myDescriptionLabel.setText(StudyUtils.getFirst(myAvailableCourses).getDescription());
+      //setting the first course in list as selected
+      myGenerator.setSelectedCourse(StudyUtils.getFirst(myAvailableCourses));
+      setOK();
+    }
+    initListeners();
+    myRefreshButton.setVisible(true);
+    myRefreshButton.setIcon(StudyIcons.Refresh);
+  }
+
+  private void initListeners() {
+
+    final FileChooserDescriptor fileChooser = new FileChooserDescriptor(true, false, false, true, false, false) {
+      @Override
+      public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
+        return file.isDirectory() || StudyUtils.isZip(file.getName());
+      }
+
+      @Override
+      public boolean isFileSelectable(VirtualFile file) {
+        return StudyUtils.isZip(file.getName());
+      }
+    };
+    myBrowseButton.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        FileChooser.chooseFile(fileChooser, null, null,
+                               new Consumer<VirtualFile>() {
+                                 @Override
+                                 public void consume(VirtualFile file) {
+                                   String fileName = file.getPath();
+                                   int oldSize = myAvailableCourses.size();
+                                   CourseInfo courseInfo = myGenerator.addLocalCourse(fileName);
+                                   if (courseInfo != null)  {
+                                     if (oldSize != myAvailableCourses.size()) {
+                                       myCoursesComboBox.addItem(courseInfo);
+                                     }
+                                     myCoursesComboBox.setSelectedItem(courseInfo);
+                                     setOK();
+                                   }
+                                   else {
+                                     setError(INVALID_COURSE);
+                                     myCoursesComboBox.removeAllItems();
+                                     myCoursesComboBox.addItem(CourseInfo.INVALID_COURSE);
+                                     for (CourseInfo course : myAvailableCourses) {
+                                       myCoursesComboBox.addItem(course);
+                                     }
+                                     myCoursesComboBox.setSelectedItem(CourseInfo.INVALID_COURSE);
+                                   }
+                                 }
+                               });
+      }
+    });
+    myRefreshButton.addActionListener(new RefreshActionListener());
+    myCoursesComboBox.addActionListener(new CourseSelectedListener());
+  }
+
+  private void setError(String errorMessage) {
+    myGenerator.setValidationResult(new ValidationResult(errorMessage));
+    if (myValidationManager != null) {
+      myValidationManager.validate();
+    }
+  }
+
+  private void setOK() {
+    myGenerator.setValidationResult(ValidationResult.OK);
+    if (myValidationManager != null) {
+      myValidationManager.validate();
+    }
+  }
+
+  public JPanel getContentPanel() {
+    return myContentPanel;
+  }
+
+  public void registerValidators(final FacetValidatorsManager manager) {
+    myValidationManager = manager;
+  }
+
+
+  /**
+   * Handles refreshing courses
+   * Old courses added to new courses only if their
+   * meta file still exists in local file system
+   */
+  private class RefreshActionListener implements ActionListener {
+    @Override
+    public void actionPerformed(ActionEvent e) {
+      myGenerator.downloadAndUnzip(true);
+      Map<CourseInfo, File> downloadedCourses = myGenerator.loadCourses();
+      if (downloadedCourses.isEmpty()) {
+        setError(CONNECTION_ERROR);
+        return;
+      }
+      Map<CourseInfo, File> oldCourses = myGenerator.getLoadedCourses();
+      Map<CourseInfo, File> newCourses = new HashMap<CourseInfo, File>();
+      for (Map.Entry<CourseInfo, File> course : oldCourses.entrySet()) {
+        File courseFile = course.getValue();
+        if (courseFile.exists()) {
+          newCourses.put(course.getKey(), courseFile);
+        }
+      }
+      for (Map.Entry<CourseInfo, File> course : downloadedCourses.entrySet()) {
+        CourseInfo courseName = course.getKey();
+        if (newCourses.get(courseName) == null) {
+          newCourses.put(courseName, course.getValue());
+        }
+      }
+      myCoursesComboBox.removeAllItems();
+
+      for (CourseInfo courseInfo : newCourses.keySet()) {
+        myCoursesComboBox.addItem(courseInfo);
+      }
+      myGenerator.setSelectedCourse(StudyUtils.getFirst(newCourses.keySet()));
+
+      myGenerator.setCourses(newCourses);
+      myAvailableCourses = newCourses.keySet();
+      myGenerator.flushCache();
+    }
+  }
+
+
+  /**
+   * Handles selecting course in combo box
+   * Sets selected course in combo box as selected in
+   * {@link StudyNewProjectPanel#myGenerator}
+   */
+  private class CourseSelectedListener implements ActionListener {
+    @Override
+    public void actionPerformed(ActionEvent e) {
+      JComboBox cb = (JComboBox)e.getSource();
+      CourseInfo selectedCourse = (CourseInfo)cb.getSelectedItem();
+      if (selectedCourse == null || selectedCourse.equals(CourseInfo.INVALID_COURSE)) {
+        myAuthorLabel.setText("");
+        myDescriptionLabel.setText("");
+        return;
+      }
+      myAuthorLabel.setText("Author: " + selectedCourse.getAuthor());
+      myCoursesComboBox.removeItem(CourseInfo.INVALID_COURSE);
+      myDescriptionLabel.setText(selectedCourse.getDescription());
+      myGenerator.setSelectedCourse(selectedCourse);
+      setOK();
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java
new file mode 100644
index 0000000..97fa00d
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyProgressBar.java
@@ -0,0 +1,92 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.ui.GraphicsConfig;
+import com.intellij.ui.ColorUtil;
+import com.intellij.ui.Gray;
+import com.intellij.ui.JBColor;
+import com.intellij.util.ui.GraphicsUtil;
+import com.intellij.util.ui.UIUtil;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+
+public class StudyProgressBar extends JComponent implements DumbAware {
+  public static final Color BLUE = JBColor.BLUE;
+  private static final Color SHADOW1 = new JBColor(Gray._190, JBColor.border());
+  private static final Color SHADOW2 = Gray._105;
+  private static final int BRICK_WIDTH = 10;
+  private static final int BRICK_SPACE = 2;
+  private final int myHeight;
+  private final int myIndent;
+  private double myFraction = 0.0;
+  private Color myColor = BLUE;
+
+  public StudyProgressBar(double fraction, Color color, int height, int indent) {
+    myFraction = fraction;
+    myColor = color;
+    myHeight = height;
+    myIndent = indent;
+  }
+
+  private int getBricksToDraw(double fraction) {
+    int bricksTotal = (getWidth() - 8) / (BRICK_WIDTH + BRICK_SPACE);
+    return (int)(bricksTotal * fraction) + 1;
+  }
+
+  protected void paintComponent(Graphics g) {
+    final GraphicsConfig config = GraphicsUtil.setupAAPainting(g);
+    Graphics2D g2 = (Graphics2D)g;
+    if (myFraction > 1) {
+      myFraction = 1;
+    }
+
+    Dimension size = getSize();
+    double width = size.getWidth() - 2*myIndent;
+    g2.setPaint(UIUtil.getListBackground());
+    Rectangle2D rect = new Rectangle2D.Double(myIndent, 0, width, myHeight);
+    g2.fill(rect);
+
+    g2.setPaint(new JBColor(SHADOW1, JBColor.border()));
+    rect.setRect(myIndent, 0, width, myHeight);
+    int arcWidth = 5;
+    int arcHeight = 5;
+    g2.drawRoundRect(myIndent, 0, (int)width, myHeight, arcWidth, arcHeight);
+    g2.setPaint(SHADOW2);
+    g2.drawRoundRect(myIndent, 0, (int)width, myHeight, arcWidth, arcHeight);
+
+    int y_center = myHeight / 2;
+    int y_steps = myHeight / 2 - 3;
+    int alpha_step = y_steps > 0 ? (255 - 70) / y_steps : 255 - 70;
+    int x_offset = 4;
+
+    g.setClip(4 + myIndent, 3, (int)width - 6, myHeight - 4);
+
+    int bricksToDraw = myFraction == 0 ? 0 : getBricksToDraw(myFraction);
+    for (int i = 0; i < bricksToDraw; i++) {
+      g2.setPaint(myColor);
+      UIUtil.drawLine(g2, x_offset, y_center, x_offset + BRICK_WIDTH - 1, y_center);
+      for (int j = 0; j < y_steps; j++) {
+        Color color = ColorUtil.toAlpha(myColor, 255 - alpha_step * (j + 1));
+        g2.setPaint(color);
+        UIUtil.drawLine(g2, x_offset, y_center - 1 - j, x_offset + BRICK_WIDTH - 1, y_center - 1 - j);
+        if (!(y_center % 2 != 0 && j == y_steps - 1)) {
+          UIUtil.drawLine(g2, x_offset, y_center + 1 + j, x_offset + BRICK_WIDTH - 1, y_center + 1 + j);
+        }
+      }
+      g2.setColor(
+        ColorUtil.toAlpha(myColor, 255 - alpha_step * (y_steps / 2 + 1)));
+      g2.drawRect(x_offset, y_center - y_steps, BRICK_WIDTH - 1, myHeight - 7);
+      x_offset += BRICK_WIDTH + BRICK_SPACE;
+    }
+    config.restore();
+  }
+
+  @Override
+  public Dimension getMaximumSize() {
+    Dimension dimension = super.getMaximumSize();
+    dimension.height = myHeight + 10;
+    return dimension;
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java
new file mode 100644
index 0000000..dee4fba
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyTestContentPanel.java
@@ -0,0 +1,67 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.components.JBScrollPane;
+import org.jetbrains.annotations.NotNull;
+import com.jetbrains.python.edu.course.UserTest;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.text.BadLocationException;
+import java.awt.*;
+
+public class StudyTestContentPanel extends JPanel {
+  public static final Dimension PREFERRED_SIZE = new Dimension(300, 200);
+  private static final Font HEADER_FONT = new Font("Arial", Font.BOLD, 16);
+  private final JTextArea myInputArea = new JTextArea();
+  private final JTextArea myOutputArea = new JTextArea();
+  public StudyTestContentPanel(UserTest userTest) {
+    this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+    initContentLabel("input", myInputArea);
+    myInputArea.getDocument().addDocumentListener(new BufferUpdater(userTest.getInputBuffer()));
+    myOutputArea.getDocument().addDocumentListener(new BufferUpdater(userTest.getOutputBuffer()));
+    initContentLabel("output", myOutputArea);
+    setEditable(userTest.isEditable());
+  }
+
+  private void initContentLabel(final String headerText, @NotNull final JTextArea contentArea) {
+    JLabel headerLabel = new JLabel(headerText);
+    headerLabel.setFont(HEADER_FONT);
+    this.add(headerLabel);
+    this.add(new JSeparator(SwingConstants.HORIZONTAL));
+    JScrollPane scroll = new JBScrollPane(contentArea);
+    scroll.setPreferredSize(PREFERRED_SIZE);
+    this.add(scroll);
+  }
+
+  private void setEditable(boolean isEditable) {
+    myInputArea.setEditable(isEditable);
+    myOutputArea.setEditable(isEditable);
+  }
+  public void addInputContent(final String content) {
+    myInputArea.setText(content);
+  }
+
+  public  void addOutputContent(final String content) {
+    myOutputArea.setText(content);
+  }
+
+  private class BufferUpdater extends DocumentAdapter {
+    private final StringBuilder myBuffer;
+
+    private BufferUpdater(StringBuilder buffer) {
+      myBuffer = buffer;
+    }
+
+    @Override
+    protected void textChanged(DocumentEvent e) {
+      myBuffer.delete(0, myBuffer.length());
+      try {
+        myBuffer.append(e.getDocument().getText(0, e.getDocument().getLength()));
+      }
+      catch (BadLocationException e1) {
+        e1.printStackTrace();
+      }
+    }
+  }
+}
diff --git a/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java
new file mode 100644
index 0000000..a553978
--- /dev/null
+++ b/python/edu/learn-python/src/com/jetbrains/python/edu/ui/StudyToolWindowFactory.java
@@ -0,0 +1,81 @@
+package com.jetbrains.python.edu.ui;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowFactory;
+import com.intellij.ui.JBColor;
+import com.intellij.ui.content.Content;
+import com.intellij.ui.content.ContentFactory;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.python.edu.StudyTaskManager;
+import com.jetbrains.python.edu.course.Course;
+import com.jetbrains.python.edu.course.Lesson;
+import com.jetbrains.python.edu.course.LessonInfo;
+import com.jetbrains.python.edu.course.StudyStatus;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+
+public class StudyToolWindowFactory implements ToolWindowFactory, DumbAware {
+  public static final String STUDY_TOOL_WINDOW = "Course Description";
+  JPanel contentPanel = new JPanel();
+
+  @Override
+  public void createToolWindowContent(@NotNull final Project project, @NotNull final ToolWindow toolWindow) {
+    if (StudyTaskManager.getInstance(project).getCourse() != null) {
+      contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS));
+      contentPanel.add(Box.createRigidArea(new Dimension(10, 0)));
+      StudyTaskManager taskManager = StudyTaskManager.getInstance(project);
+      Course course = taskManager.getCourse();
+      if (course == null) {
+        return;
+      }
+      String courseName = UIUtil.toHtml("<h1>" + course.getName() + "</h1>", 10);
+      String description = UIUtil.toHtml(course.getDescription(), 5);
+      String author = taskManager.getCourse().getAuthor();
+      String authorLabel = UIUtil.toHtml("<b>Author: </b>" + author, 5);
+      contentPanel.add(new JLabel(courseName));
+      contentPanel.add(new JLabel(authorLabel));
+      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
+      contentPanel.add(new JLabel(description));
+
+      int taskNum = 0;
+      int taskSolved = 0;
+      int lessonsCompleted = 0;
+      List<Lesson> lessons = course.getLessons();
+      for (Lesson lesson : lessons) {
+        if (lesson.getStatus() == StudyStatus.Solved) {
+          lessonsCompleted++;
+        }
+        LessonInfo lessonInfo = lesson.getLessonInfo();
+        taskNum += lessonInfo.getTaskNum();
+        taskSolved += lessonInfo.getTaskSolved();
+      }
+      String completedLessons = String.format("%d of %d lessons completed", lessonsCompleted, course.getLessons().size());
+      String completedTasks = String.format("%d of %d tasks completed", taskSolved, taskNum);
+      String tasksLeft = String.format("%d of %d tasks left", taskNum - taskSolved, taskNum);
+      contentPanel.add(Box.createVerticalStrut(10));
+      addStatistics(completedLessons);
+      addStatistics(completedTasks);
+
+      double percent = (taskSolved * 100.0) / taskNum;
+      contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
+      StudyProgressBar studyProgressBar = new StudyProgressBar(percent / 100, JBColor.GREEN, 40, 10);
+      contentPanel.add(studyProgressBar);
+      addStatistics(tasksLeft);
+      ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
+      Content content = contentFactory.createContent(contentPanel, "", true);
+      toolWindow.getContentManager().addContent(content);
+    }
+  }
+
+  private void addStatistics(String statistics) {
+    String labelText = UIUtil.toHtml(statistics, 5);
+    contentPanel.add(Box.createRigidArea(new Dimension(0, 10)));
+    JLabel statisticLabel = new JLabel(labelText);
+    contentPanel.add(statisticLabel);
+  }
+}
diff --git a/python/edu/learn-python/testData/course.json b/python/edu/learn-python/testData/course.json
new file mode 100644
index 0000000..bff570a
--- /dev/null
+++ b/python/edu/learn-python/testData/course.json
@@ -0,0 +1,130 @@
+{
+  "name": "Python для начинающих",
+  "description": "Начальный курс по языку Python",
+  "lessons": [
+    {
+      "name": "Первые программа",
+      "task_list": [
+        {
+          "name": "Задание 1",
+          "text": "hello-text.html",
+          "test_file": "hello-tests.py",
+          "test_num": 1,
+          "task_files": {
+            "helloworld.py": {
+              "task_windows": [
+                {
+                  "line": 0,
+                  "start": 0,
+                  "text": "type operator",
+                  "hint": "hello-text.html",
+                  "possible_answer": "print"
+                },
+                {
+                  "line": 0,
+                  "start": 33,
+                  "text": "type your name",
+                  "hint": "empty_study.docs",
+                  "possible_answer": "Liana"
+                }
+              ]
+            }
+          }
+        },
+        {
+          "name": "Задание 2",
+          "text": "matchends-text.html",
+          "test_file": "matchends-test.py",
+          "test_num": 3,
+          "task_files": {
+            "match_ends.py": {
+              "task_windows": [
+                {
+                  "line": 1,
+                  "start": 43,
+                  "text": "condition",
+                  "hint": "empty_study.docs",
+                  "possible_answer": ">="
+                },
+                {
+                  "line": 1,
+                  "start": 61,
+                  "text": "index",
+                  "hint": "empty_study.docs",
+                  "possible_answer": "0"
+                },
+                {
+                  "line": 1,
+                  "start": 73,
+                  "text": "index",
+                  "hint": "empty_study.docs",
+                  "possible_answer": "-1"
+                },
+                {
+                  "line": 2,
+                  "start": 11,
+                  "text": "function",
+                  "hint": "list.docs",
+                  "possible_answer": "len"
+                }
+              ]
+            }
+          }
+        }
+      ]
+    },
+    {
+      "name": "Простые задачи",
+      "task_list": [
+        {
+          "name": "Задание 1",
+          "text": "sum-text.html",
+          "test_file": "sum_tests.py",
+          "test_num": 3,
+          "user_tests": [
+            {
+              "input": "sum-input.txt",
+              "output": "sum-output"
+            }
+          ],
+          "task_files": {
+            "sum.py": {
+              "task_windows": [
+                {
+                  "line": 4,
+                  "start": 15,
+                  "text": "получите из консоли имя файла",
+                  "hint": "argv.docs",
+                  "possible_answer": "sys.argv[1]"
+                },
+                {
+                  "line": 5,
+                  "start": 8,
+                  "text": "откройте файл на запись",
+                  "hint": "empty_study.docs",
+                  "possible_answer": "open(filename, 'r')"
+                },
+                {
+                  "line": 10,
+                  "start": 4,
+                  "text": "закройте файл",
+                  "hint": "empty_study.docs",
+                  "possible_answer": "f.close()"
+                },
+                {
+                  "line": 11,
+                  "start": 14,
+                  "text": "правильно проинициализируйте значение",
+                  "hint": "empty_study.docs",
+                  "possible_answer": "-sys.maxint"
+                }
+              ]
+            }
+          }
+        }
+
+      ]
+
+    }
+  ]
+}
\ No newline at end of file
diff --git a/python/edu/learn-python/tests/JsonParserTest.java b/python/edu/learn-python/tests/JsonParserTest.java
new file mode 100644
index 0000000..903f0a5
--- /dev/null
+++ b/python/edu/learn-python/tests/JsonParserTest.java
@@ -0,0 +1,37 @@
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import com.jetbrains.python.edu.StudyUtils;
+import com.jetbrains.python.edu.course.Course;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * author: liana
+ * data: 7/4/14.
+ */
+public class JsonParserTest {
+  private Course myCourse = null;
+  @Before
+  public void setUp() throws FileNotFoundException {
+    Reader reader = new InputStreamReader(new FileInputStream("EDIDE/testData/course.json"));
+    Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+    myCourse = gson.fromJson(reader, Course.class);
+  }
+
+  @Test
+  public void testCourseLevel() {
+    assertEquals(myCourse.getName(), "Python для начинающих");
+    assertEquals(StudyUtils.getFirst(myCourse.getLessons().get(1).getTaskList().get(0).getUserTests()).getInput(), "sum-input.txt");
+    assertEquals(myCourse.getLessons().size(), 2);
+    assertEquals(myCourse.getLessons().get(0).getTaskList().size(), 2);
+    assertEquals(myCourse.getLessons().get(1).getTaskList().size(), 1);
+  }
+}