[debian-edu-commits] debian-edu/ 01/02: New upstream version 2.1.16

Aivar Annamaa aivarannamaa-guest at moszumanska.debian.org
Sun Nov 12 15:21:42 UTC 2017


This is an automated email from the git hooks/post-receive script.

aivarannamaa-guest pushed a commit to branch master
in repository thonny.

commit 2e130184357591b8cea0dadfda8235b9bbfa10be
Author: Aivar Annamaa <aivar.annamaa at gmail.com>
Date:   Sun Nov 12 16:30:06 2017 +0200

    New upstream version 2.1.16
---
 CHANGELOG.rst                                      |  268 ++++
 CREDITS.rst                                        |   32 +
 LICENSE.txt                                        |   21 +
 MANIFEST.in                                        |   11 +
 PKG-INFO                                           |   38 +
 README.rst                                         |    3 +
 licenses/ECLIPSE-ICONS-LICENSE.txt                 |   88 ++
 packaging/icons/thonny-128x128.png                 |  Bin 0 -> 1591 bytes
 packaging/icons/thonny-16x16.png                   |  Bin 0 -> 214 bytes
 packaging/icons/thonny-2.png                       |  Bin 0 -> 324 bytes
 packaging/icons/thonny-22x22.png                   |  Bin 0 -> 542 bytes
 packaging/icons/thonny-256x256.png                 |  Bin 0 -> 3823 bytes
 packaging/icons/thonny-32x32.png                   |  Bin 0 -> 624 bytes
 packaging/icons/thonny-48x48.png                   |  Bin 0 -> 914 bytes
 packaging/icons/thonny-64x64.png                   |  Bin 0 -> 933 bytes
 packaging/linux/org.thonny.Thonny.appdata.xml      |   44 +
 packaging/linux/org.thonny.Thonny.desktop          |   17 +
 packaging/linux/thonny.1                           |   29 +
 requirements.txt                                   |    1 +
 setup.cfg                                          |    5 +
 setup.py                                           |   65 +
 thonny.egg-info/PKG-INFO                           |   38 +
 thonny.egg-info/SOURCES.txt                        |  125 ++
 thonny.egg-info/dependency_links.txt               |    1 +
 thonny.egg-info/entry_points.txt                   |    3 +
 thonny.egg-info/requires.txt                       |    1 +
 thonny.egg-info/top_level.txt                      |    1 +
 thonny/VERSION                                     |    1 +
 thonny/__init__.py                                 |  171 +++
 thonny/__main__.py                                 |    3 +
 thonny/ast_utils.py                                |    4 +
 thonny/base_file_browser.py                        |  233 ++++
 thonny/code.py                                     |  664 ++++++++++
 thonny/codeview.py                                 |  204 +++
 thonny/common.py                                   |    4 +
 thonny/config.py                                   |  133 ++
 thonny/config_ui.py                                |   78 ++
 thonny/globals.py                                  |   20 +
 thonny/jedi_utils.py                               |   81 ++
 thonny/memory.py                                   |  101 ++
 thonny/misc_utils.py                               |  121 ++
 thonny/plugins/__init__.py                         |    1 +
 thonny/plugins/about.py                            |  135 ++
 thonny/plugins/ast_view.py                         |  154 +++
 thonny/plugins/autocomplete.py                     |  312 +++++
 thonny/plugins/coloring.py                         |  256 ++++
 thonny/plugins/commenting.py                       |  119 ++
 thonny/plugins/common_editing_commands.py          |   72 ++
 thonny/plugins/debugger.py                         |  684 +++++++++++
 thonny/plugins/editor_config_page.py               |   53 +
 thonny/plugins/event_logging.py                    |  209 ++++
 thonny/plugins/event_view.py                       |   35 +
 thonny/plugins/find_replace.py                     |  355 ++++++
 thonny/plugins/font_config_page.py                 |   86 ++
 thonny/plugins/general_config_page.py              |   38 +
 thonny/plugins/goto_definition.py                  |   34 +
 thonny/plugins/heap.py                             |   55 +
 thonny/plugins/help/__init__.py                    |  102 ++
 thonny/plugins/help/help.rst                       |   59 +
 thonny/plugins/highlight_names.py                  |  290 +++++
 thonny/plugins/interpreter_config_page.py          |   85 ++
 thonny/plugins/locals_marker.py                    |  149 +++
 thonny/plugins/main_file_browser.py                |   96 ++
 thonny/plugins/object_inspector.py                 |  468 +++++++
 thonny/plugins/outline.py                          |  125 ++
 thonny/plugins/paren_matcher.py                    |  155 +++
 thonny/plugins/pip_gui.py                          |  825 +++++++++++++
 thonny/plugins/refactor.py                         |  272 ++++
 thonny/plugins/replayer.py                         |  342 ++++++
 thonny/plugins/styler.py                           |  102 ++
 thonny/plugins/system_shell/__init__.py            |  196 +++
 thonny/plugins/system_shell/explain_environment.py |  177 +++
 thonny/plugins/thonny_folders.py                   |   36 +
 thonny/plugins/variables.py                        |   30 +
 thonny/res/16x16_blank.gif                         |  Bin 0 -> 832 bytes
 thonny/res/1x1_white.gif                           |  Bin 0 -> 807 bytes
 thonny/res/arrow_down2.gif                         |  Bin 0 -> 837 bytes
 thonny/res/class.gif                               |  Bin 0 -> 164 bytes
 thonny/res/closed_folder.gif                       |  Bin 0 -> 1111 bytes
 thonny/res/file.new_file.gif                       |  Bin 0 -> 920 bytes
 thonny/res/file.open_file.gif                      |  Bin 0 -> 1001 bytes
 thonny/res/file.save_file.gif                      |  Bin 0 -> 1008 bytes
 thonny/res/folder.gif                              |  Bin 0 -> 386 bytes
 thonny/res/generic_file.gif                        |  Bin 0 -> 238 bytes
 thonny/res/gray_line.gif                           |  Bin 0 -> 833 bytes
 thonny/res/hard_drive.gif                          |  Bin 0 -> 558 bytes
 thonny/res/hard_drive2.gif                         |  Bin 0 -> 1019 bytes
 thonny/res/method.gif                              |  Bin 0 -> 578 bytes
 thonny/res/open_folder.gif                         |  Bin 0 -> 1108 bytes
 thonny/res/python_file.gif                         |  Bin 0 -> 626 bytes
 thonny/res/python_icon.gif                         |  Bin 0 -> 1049 bytes
 thonny/res/run.debug_current_script.gif            |  Bin 0 -> 950 bytes
 thonny/res/run.reset.gif                           |  Bin 0 -> 900 bytes
 thonny/res/run.run_current_script.gif              |  Bin 0 -> 983 bytes
 thonny/res/run.run_to_cursor.gif                   |  Bin 0 -> 143 bytes
 thonny/res/run.step.gif                            |  Bin 0 -> 363 bytes
 thonny/res/run.step_into.gif                       |  Bin 0 -> 197 bytes
 thonny/res/run.step_out.gif                        |  Bin 0 -> 906 bytes
 thonny/res/run.step_over.gif                       |  Bin 0 -> 211 bytes
 thonny/res/run.stop.gif                            |  Bin 0 -> 1028 bytes
 thonny/res/tab_close.gif                           |  Bin 0 -> 838 bytes
 thonny/res/tab_close_active.gif                    |  Bin 0 -> 834 bytes
 thonny/res/text_file.gif                           |  Bin 0 -> 627 bytes
 thonny/res/thonny.ico                              |  Bin 0 -> 16652 bytes
 thonny/res/thonny.png                              |  Bin 0 -> 5134 bytes
 thonny/res/thonny_small.ico                        |  Bin 0 -> 894 bytes
 thonny/roughparse.py                               |  940 ++++++++++++++
 thonny/running.py                                  | 1106 +++++++++++++++++
 thonny/shared/__init__.py                          |    2 +
 thonny/shared/backend_launcher.py                  |   42 +
 thonny/shared/thonny/__init__.py                   |    2 +
 thonny/shared/thonny/ast_utils.py                  |  513 ++++++++
 thonny/shared/thonny/backend.py                    | 1298 ++++++++++++++++++++
 thonny/shared/thonny/common.py                     |  184 +++
 thonny/shell.py                                    |  640 ++++++++++
 thonny/test/__init__.py                            |    0
 thonny/test/plugins/__init__.py                    |    0
 thonny/test/plugins/test_coloring.py               |   45 +
 thonny/test/plugins/test_locals_marker.py          |   41 +
 thonny/test/plugins/test_name_highlighter.py       |  104 ++
 thonny/test/plugins/test_paren_matcher.py          |   43 +
 thonny/test/test_ast_utils.py                      |   79 ++
 thonny/test/test_ast_utils_mark_text_ranges.py     |  450 +++++++
 thonny/tktextext.py                                |  832 +++++++++++++
 thonny/token_utils.py                              |   35 +
 thonny/ui_utils.py                                 |  989 +++++++++++++++
 thonny/workbench.py                                | 1210 ++++++++++++++++++
 127 files changed, 17271 insertions(+)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..e4226be
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,268 @@
+===============
+Version history
+===============
+
+2.1.16 (2017-11-10)
+===================
+* Tests moved under thonny package
+* Tests included in the source distribution
+* More icons included in the source distribution
+
+2.1.15 (2017-11-07)
+===================
+* Removed StartupNotify from Linux .desktop file (StartupNotify=true leaves cursor spinning in Debian)
+
+2.1.14 (2017-11-02)
+===================
+* Added some Linux-specific files to source distribution. No new features or fixes.
+
+2.1.13 (2017-10-29)
+===================
+* Temporary workaround for #351: Locals and name highlighter occasionally make Thonny freeze
+* Include only required licenses in source dist
+
+2.1.12 (2017-10-13)
+===================
+* FIXED #303: Allow specifying same interpreter for backend as frontend uses
+* FIXED #304: Allow specifying backend interpreter by relative path
+* FIXED #312: Closing unsaved tab causes error    
+* FIXED #319: Linux install script needs quoting around the path(s) 
+* FIXED #320: Install gets recursive if trying to install within extracted tarball 
+* FIXED #321: Linux installer fails if invoked with relative, local user path 
+* FIXED #334: init.tcl not found (Better control over back-end environment variables)
+* FIXED #343: Thonny now also works with jedi 0.11
+
+2.1.11 (2017-07-22)
+===================
+* FIXED #31: Infinite print loop freezes Thonny  
+* FIXED #285: Previous used interpreters are not shown in options dialog
+* FIXED #296: Make it more explicit that pip GUI search box needs exact package name
+* FIXED #298: Python crashes keep backend hanging 
+* FIXED #305: Variables table doesn't get updated, if it's blocked by another view
+
+2.1.10 (2017-06-09)
+===================
+* NEW: More flexibility for classroom setups (see https://bitbucket.org/plas/thonny/wiki/ClassroomSetup) 
+* FIXED #276: Copy with Ctrl+C causes bell
+* FIXED #277: Triple-quoted strings keep keyword coloring
+* FIXED #278: Paste in shell causes bell 
+* FIXED #281: Wrong unindentation with SHIFT+TAB when last line does not end with linebreak
+* FIXED #283: backend.log path doesn't take THONNY_USER_DIR into account
+* FIXED #284: Internal error when saving to a read-only folder/file (now proposes to choose another name)
+
+2.1.9 (2017-06-01)
+==================
+* FIXED #273: Memory leak in editor margin because of undo log
+* FIXED #275: Updating line numbers is very inefficient
+* FIXED: Pasted text occasionally was hidden below bottom edge of the editor
+* FIXED: sys.exit() didn't really close the backend 
+
+2.1.8 (2017-05-28)
+==================
+* ENHANCEMENT: Code completion with Tab-key is now optional (see Tools => Options => Editor)
+* ENHANCEMENT: Clicking on the editor now closes code completion box
+* CHANGED: Code completion box doesn't offer names starting with double underscore anymore.
+* FIXED: Error caused by too fast typing with open code completions box 
+* ENHANCEMENT: Find/Replace dialog can now be operated with F3
+* ENHANCEMENT: Find/Replace pre-selects previously used search string
+* ENHANCEMENT: Find/Replace dialog doesn't block main window anymore
+* FIXED: Find/Replace doesn't ignore spaces in search string anymore 
+* FIXED: Closed views reappeared after restart if they were only views in that notebook  
+* FIXED #264: Debugger fails with with conditional list comprehension 
+* FIXED #265: Error when using two word search string in pip GUI
+* FIXED #266: Occasional incorrect line numbering
+* FIXED #267: Kivy application main window didn't show in Windows
+* TECHNICAL: Better diagnostic logging
+ 
+
+2.1.7 (2017-05-13)
+==================
+* CHANGED: pip GUI now works in read-only mode unless backend is a virtual environment
+* FIXED: Error when non-default backend was used without previously generated Thonny-private virtual environment
+
+2.1.6 (2017-05-12)
+==================
+* FIXED #260: Strange behaviour when indenting with TAB 
+* FIXED #261: Editing a triple-quoted string breaks coloring in following lines 
+* FIXED: Made outdated pip detection more general 
+
+2.1.5 (2017-05-09)
+==================
+* FIXED: Jedi version checking problem 
+
+2.1.4 (2017-05-09)
+==================
+(This release is meant for making Thonny work better with system Python 3.4 in Debian Jessie)
+
+* FIXED #254: "Manage plug-ins" now gives instructions for installing pip if system is missing it or it's too old 
+* FIXED #255: Name highlighter and locals marker are now quietly disabled when system has too old jedi
+* FIXED: Virtual env dialog now closes properly
+* TECHNICAL: SubprocessDialog now has more robust returncode checking in Linux
+
+
+2.1.3 (2017-05-09)
+==================
+* FIXED #250: Debugger focus was off by one line in function frames
+* FIXED #251: Debugger timing issue (wrong command type in the backend)
+* FIXED #252: Debugger timing issue (get_globals and debugger commands interfere)
+* FIXED #253: Creating default virtual env does not work when using Debian python3 without ensurepip
+
+2.1.2 (2017-05-08)
+==================
+* FIXED #220 and #237: Icon problems in Linux tasbar.
+* FIXED #245: Tooltips not working in Mac
+* FIXED #246: Current script did not get executed if cursor was not in the end of the shell 
+* FIXED #249: Reset, Run and Debug caused double prompt
+
+2.1.1 (2017-05-03)
+==================
+* FIXED #241: Some menu items gave errors with micro:bit backend.
+* FIXED #242: Focus got stuck on first run (no entry was possible neither in shell nor editor when initialization dialog closed)
+
+2.1.0 (2017-05-02)
+==================
+* TECHNICAL: Changes in diagnostic logging
+
+2.1.0b11 (2017-04-29)
+=====================
+* TECHNICAL: Implemented more robust approach for installing Thonny plugins
+
+2.1.0b10 (2017-04-29)
+=====================
+* CHANGED: Installed plugins now end up under ~/.thonny/plugins
+* TECHNICAL: Backend preparation now occurs when main window has been opened
+
+2.1.0b9 (2017-04-28)
+====================
+* FIXED: Backend related regression introduced in b8
+
+2.1.0b8 (2017-04-27)
+====================
+* CHANGED: (FIXED #231) Stop/Reset button is now Interrupt/Reset button (tries to interrupt a running command instead of reseting. Resets if pressed in idle state)
+* FIXED #232: Ubuntu showed pip GUI captions with too big font
+* FIXED #233: Thonny now remembers which view was on top in a panel.
+* FIXED #234: Multiline support problems in shell (trailing whitespace was causing trouble)
+* FIXED: pip GUI shows latest version number when there is no stable version.
+* FIXED: pip GUI now can handle also packages without PyPI presence
+* TECHNICAL: Backends are not sent Reset command for initialization anymore.  
+
+2.1.0b7 (2017-04-25)
+==================
+* FIXED: Removed some circular import to support Python 3.4
+* FIXED: pip GUI now also lists installed pre-releases
+* EXPERIMENTAL: GUI for installing Thonny plug-ins (Tools => Manage plug-ins...)
+* TECHNICAL: Thonny+Python bundles again include pip (needed for installing plug-ins)
+* TECHNICAL: Refactored creation of several widgets to support theming
+* TECHNICAL: THONNY_USER_DIR environment variable can now specify where Thonny stores user data (conf files, default virtual env, ...)
+ 
+
+2.1.0b6 (2017-04-19)
+==================
+* ENHANCEMENT: Shell now shows location of external interpreter as welcome text
+* FIXED #224: Tab-indentation didn't work if tail of the text was selected and text didn't end with empty line
+* FIXED: Tab with selected text occasionally invoked code-completion
+* TECHNICAL: Tweaks in Windows console allocation
+* TECHNICAL: Thonny+Python bundles don't include pip anymore (venv gets pip via ensurepip)
+
+2.1.0b5 (2017-04-18)
+==================
+* FIXED: Typo in pipGUI (regression introduced in b4)
+
+2.1.0b4 (2017-04-18)
+====================
+* CHANGED: If you want to use Thonny with external Python interpreter, then now you should select python.exe instead of pythonw.exe.
+* FIXED #223: Can't interrupt subprocess when Thonny is run via thonny.exe
+* FIXED: Private venv didn't find Tcl/Tk in ubuntu (commit 33eabff)
+* FIXED: Right-click on editor tabs now also works on macOS.
+
+2.1.0b3 (2017-04-17)
+====================
+* NEW: Dialog for managing 3rd party packages / a simple pip GUI. Check it out: "Tools => Manage packages"
+* NEW: Shell now supports multiline commands
+* ENHANCEMENT: Window title now shows full path and cursor location of current file. 
+* ENHANCEMENT: Editor lines can be selected by clicking and/or dragging on line-number margin (thanks to Sven).
+* ENHANCEMENT: Most programs can now be interrupted by Ctrl+C without restarting the process.
+* ENHANCEMENT: You can start editing the code that is still running (the process gets interrupted automatically). This is handy when developing tkinter applications.
+* ENHANCEMENT: Tab can be used as alternative code-completion shortcut.
+* ENHANCEMENT: Recommended pip-command now appears faster in System Shell.
+* ENHANCEMENT: Alternative interpreter doesn't need to have jedi installed in order to provide code-completions (see #171: Code auto-complete error)
+* ENHANCEMENT: Double-click on autocomplete list inserts the completion
+* EXPERIMENTAL: Ctrl-click on a name in code tries to locate its definition. NB! Not finished yet!
+* CHANGED: Bundled Python version has been upgraded to 3.6.1
+* CHANGED: Bundled Python in Mac and Linux now uses SSL certs from certifi project (https://pypi.python.org/pypi/certifi).
+* REMOVED: Moved incomplete Exercise system to a separate plugin (https://bitbucket.org/plas/thonny-exersys). With this got rid of tkinterhtml, requests and beautifulsoup4 dependencies.
+* FIXED #16: Run doesn't clear variables (again?)
+* FIXED #98: Nested functions crashed the debugger.
+* FIXED #114: Crash when trying to change interpreter in macOS.
+* FIXED #142: "Open system shell" failed when Thonny path had spaces in it. Paths are now properly quoted.
+* FIXED #154: Problems with Notebook tabs' context menus
+* FIXED #159: Debugging list or set comprehension caused crash
+* FIXED #166: Can't delete one of two spaces with backspace
+* FIXED #180: Right-click doesn't focus editor
+* FIXED #187: Main modules launched by Thonny were missing ``__spec__`` attribute.
+* FIXED #195: Debugger crashes when using generators.
+* FIXED #201: "Tools => Open Thonny data folder" now works also in macOS.
+* FIXED #211: Linux installer was failing when using ``xdg-user-dir`` (thanks to Ryan McQuen)
+* FIXED #213: In single instance mode new Window doesn't get focus
+* FIXED #217: Debugger on Python 3.5 and later can't handle splat operator 
+* FIXED #221: Context menus in Linux can now be closed by clicking elsewhere
+* FIXED: Event logger did not save filenames (eb34c5d).
+* FIXED: Problem in replayer (db78855).
+* TECHNICAL: Bundled Jedi version has been upgraded to 0.10.2.
+* TECHNICAL: 3rd party Thonny plugins must now be under ``thonnycontrib`` namespace package.
+* TECHNICAL: Introduced the concept of "eary plugins" (plugins, which get loaded before initializing the runner).
+* TECHNICAL: Refactored the interface between GUI and backend to allow different backend implementations
+* TECHNICAL: Previously, with bundled Python, Thonny was using nasty tricks to force pip install packages install under ~/.thonny. Now it creates a proper virtual environment under ~/.thonny and uses this as the backend by default (instead of using interpreter running the GUI directly).
+* TECHNICAL: Automatic tkinter updates on the backend are now less invasive
+
+2.0.7 (2017-01-06)
+==================
+* FIXED: Making font size too small would crash Thonny.
+* FIXED: Another take on configuration file corruption. 
+* FIXED: Shift-Tab wasn’t working in some cases.
+* FIXED #165: "Open system shell" did not add Scripts dir to PATH in Windows. 
+* FIXED #183: ``from __future__ import`` crashed the debugger.
+
+2.0.6 (2017-01-06)
+==================
+* FIXED: a bug in Linux installer (configuration file wasn’t created in new installations)
+
+2.0.5 (2016-11-30)
+==================
+* FIXED: Corrected shift key detection (a82bd4d)
+
+2.0.4 (2016-10-26)
+==================
+* FIXED: Configuration file was occasionally getting corrupted (for mysterious reasons, maybe a bug in Python’s configparser)
+* FIXED #104: Negative font size crashed Thonny
+* FIXED #143: Linux installer fails if desktop isn't named "Desktop". (Later turned out this wasn't fixed for all cases).
+* FIXED #134: "Open system shell" doesn't work in Centos 7 KDE 
+
+2.0.3 (2016-09-30)
+==================
+* FIXED: Quoting in "Open system shell" in Mac. Again. 
+
+2.0.2 (2016-09-30)
+==================
+* FIXED: Quoting in "Open system shell" in Mac. 
+
+2.0.1 (2016-09-30)
+==================
+* FIXED #106: Don't let user logs grow too big
+
+2.0.0 (2016-09-29)
+==================
+* NEW: Added code completion (powered by Jedi: https://github.com/davidhalter/jedi)
+* NEW: Added new command "Tools => Open system shell" which opens terminal where current Python is in PATH.
+* CHANGED: Single instance mode is now optional (Tools => Options => General)
+* FIXED: Many bugs
+
+1.2.0b2 (2016-02-10)
+====================
+* NEW: Thonny now runs in single instance mode. Previously, when you opened a py file with Thonny, a new Thonny instance (window) was created even if an instance existed already. This became nuisance if you opened several files. Now Thonny works as single instance program, meaning only one instance of Thonny runs at the time. When you open another file, it is opened in existing window.
+* NEW: Editor enhancements. Added option to show line numbers and right margin in the editor. In order to keep first impression cleaner, they are disabled by default. See Tools => Options => Editor. Don't forget that you don't need line numbers for locating lines mentioned in error messages -- you can click them and Thonny shows you the line.
+* FIXED: Some bugs where Thonny couldn't prepare some programs for debugging.
+
+Older versions
+==============
+See https://bitbucket.org/plas/thonny/issues/ and https://bitbucket.org/plas/thonny/commits/ for details 
diff --git a/CREDITS.rst b/CREDITS.rst
new file mode 100644
index 0000000..c3ac2db
--- /dev/null
+++ b/CREDITS.rst
@@ -0,0 +1,32 @@
+=======
+Credits
+=======
+
+Thonny is thankful to:
+
+Its home
+--------
+Thonny was born in University of Tartu (https://www.ut.ee), Institute of Computer Science (https://www.cs.ut.ee) and the main development is being done here.
+
+Python
+------
+It's a really nice language for teaching programming. It also has some nice technical properties, that made Thonny's program animation features pleasure to implement.
+
+Libraries
+--------------
+* jedi (http://jedi.readthedocs.io) is used for code completion, go to definition, etc.
+* certifi (https://pypi.python.org/pypi/certifi) provides SSL certs for bundled Python in Linux and Mac.
+* distro (https://pypi.python.org/pypi/distro) is optionally used for detecting Linux version in About dialog. 
+
+Source contributors and frequent bug-reporters
+----------------------------------------------
+* Aivar Annamaa
+* Rene Lehtma
+* Filip Schouwenaars
+* Fizban
+* Sven (s_v_e_n)
+* Taavi Ilp
+* Toomas Mölder
+* Xin Rong
+
+Please let us know if we have forgotten to add your name to this list! Also, let us know if you want to remove your name.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..a40186f
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Aivar Annamaa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..2a40095
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,11 @@
+include CHANGELOG.rst
+include CREDITS.rst
+include LICENSE.txt
+include README.rst
+include requirements.txt
+include licenses/ECLIPSE-ICONS-LICENSE.txt
+include packaging/linux/thonny.1
+include packaging/icons/*.png
+include packaging/linux/org.thonny.Thonny.appdata.xml
+include packaging/linux/org.thonny.Thonny.desktop
+
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..64aa9c9
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,38 @@
+Metadata-Version: 1.2
+Name: thonny
+Version: 2.1.16
+Summary: Python IDE for beginners
+Home-page: http://thonny.org
+Author: Aivar Annamaa and others
+Author-email: thonny at googlegroups.com
+License: MIT
+Description: Thonny is a simple Python IDE with features useful for learning programming. See http://thonny.org for more info.
+Keywords: IDE education debugger
+Platform: Windows
+Platform: macOS
+Platform: Linux
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: MacOS X
+Classifier: Environment :: Win32 (MS Windows)
+Classifier: Environment :: X11 Applications
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Education
+Classifier: Intended Audience :: End Users/Desktop
+Classifier: License :: Freeware
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Natural Language :: English
+Classifier: Operating System :: MacOS
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Education
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Debuggers
+Classifier: Topic :: Text Editors
+Requires-Python: >=3.4
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..a845527
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,3 @@
+Thonny is a Python IDE meant for learning programming.
+
+See http://thonny.org for more info.
\ No newline at end of file
diff --git a/licenses/ECLIPSE-ICONS-LICENSE.txt b/licenses/ECLIPSE-ICONS-LICENSE.txt
new file mode 100644
index 0000000..d462c7b
--- /dev/null
+++ b/licenses/ECLIPSE-ICONS-LICENSE.txt
@@ -0,0 +1,88 @@
+
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and
+
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contributio [...]
+
+c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted here [...]
+
+d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within the Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Con [...]
+
+For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims  [...]
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Ag [...]
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF [...]
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Reci [...]
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the respons [...]
+
+This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.
diff --git a/packaging/icons/thonny-128x128.png b/packaging/icons/thonny-128x128.png
new file mode 100644
index 0000000..cb113d4
Binary files /dev/null and b/packaging/icons/thonny-128x128.png differ
diff --git a/packaging/icons/thonny-16x16.png b/packaging/icons/thonny-16x16.png
new file mode 100644
index 0000000..3dca790
Binary files /dev/null and b/packaging/icons/thonny-16x16.png differ
diff --git a/packaging/icons/thonny-2.png b/packaging/icons/thonny-2.png
new file mode 100644
index 0000000..26d6967
Binary files /dev/null and b/packaging/icons/thonny-2.png differ
diff --git a/packaging/icons/thonny-22x22.png b/packaging/icons/thonny-22x22.png
new file mode 100644
index 0000000..bc88a96
Binary files /dev/null and b/packaging/icons/thonny-22x22.png differ
diff --git a/packaging/icons/thonny-256x256.png b/packaging/icons/thonny-256x256.png
new file mode 100644
index 0000000..491117d
Binary files /dev/null and b/packaging/icons/thonny-256x256.png differ
diff --git a/packaging/icons/thonny-32x32.png b/packaging/icons/thonny-32x32.png
new file mode 100644
index 0000000..4b8a211
Binary files /dev/null and b/packaging/icons/thonny-32x32.png differ
diff --git a/packaging/icons/thonny-48x48.png b/packaging/icons/thonny-48x48.png
new file mode 100644
index 0000000..9fe26c5
Binary files /dev/null and b/packaging/icons/thonny-48x48.png differ
diff --git a/packaging/icons/thonny-64x64.png b/packaging/icons/thonny-64x64.png
new file mode 100644
index 0000000..414afb1
Binary files /dev/null and b/packaging/icons/thonny-64x64.png differ
diff --git a/packaging/linux/org.thonny.Thonny.appdata.xml b/packaging/linux/org.thonny.Thonny.appdata.xml
new file mode 100644
index 0000000..e70dd4c
--- /dev/null
+++ b/packaging/linux/org.thonny.Thonny.appdata.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2017 Aivar Annamaa <aivar.annamaa at gmail.com> -->
+<component type="desktop-application">
+  <id>org.thonny.Thonny.desktop</id>
+  <metadata_license>MIT</metadata_license>
+  <project_license>MIT</project_license>
+  <name>Thonny</name>
+  <summary>Python IDE for beginners</summary>
+
+  <description>
+    <p>Thonny is a simple Python IDE with features useful for learning programming.
+    It comes with a debugger which is able to visualize all the conceptual steps
+    taken to run a Python program (executing statements, evaluating expressions,
+    maintaining the call stack). There is a GUI for installing 3rd party packages
+    and special mode for learning about references.</p>
+    <p>See the homepage for more information, screenshots and a walk-through video.</p>
+  </description>
+  
+  <categories>
+    <category>Development</category>
+    <category>Education</category>
+    <category>Debugger</category>
+    <category>IDE</category>
+    <category>ComputerScience</category>
+  </categories>
+  
+  <launchable type="desktop-id">org.thonny.Thonny.desktop</launchable>
+
+  <url type="homepage">http://thonny.org</url>
+  <url type="bugtracker">https://bitbucket.org/plas/thonny/issues/</url>
+  <url type="help">https://bitbucket.org/plas/thonny/wiki/Home</url>
+
+  <screenshots>
+    <screenshot type="default">
+      <image>http://thonny.org/img/screenshot.png</image>
+      <caption>Thonny stepping through a recursive function</caption>
+    </screenshot>
+  </screenshots>
+
+  <provides>
+    <binary>thonny</binary>
+  </provides>
+
+</component>
diff --git a/packaging/linux/org.thonny.Thonny.desktop b/packaging/linux/org.thonny.Thonny.desktop
new file mode 100644
index 0000000..9cbffaf
--- /dev/null
+++ b/packaging/linux/org.thonny.Thonny.desktop
@@ -0,0 +1,17 @@
+[Desktop Entry]
+Type=Application
+Name=Thonny
+GenericName=Python IDE
+Exec=/usr/bin/thonny %F
+Comment=Python IDE for beginners
+Icon=thonny
+StartupWMClass=Thonny
+Terminal=false
+Categories=Education;Development
+Keywords=programming;education
+MimeType=text/x-python;
+Actions=Edit;
+
+[Desktop Action Edit]
+Exec=/usr/bin/thonny %F
+Name=Edit with Thonny
diff --git a/packaging/linux/thonny.1 b/packaging/linux/thonny.1
new file mode 100644
index 0000000..aa58051
--- /dev/null
+++ b/packaging/linux/thonny.1
@@ -0,0 +1,29 @@
+.TH THONNY 1
+.SH NAME
+thonny \- Python IDE for beginners
+.SH SYNOPSIS
+.B thonny
+[\fIFILE...\fR]
+.SH DESCRIPTION
+Thonny is a Python IDE for learning and teaching programming.
+.SH BASIC USAGE
+On the first run you see a code editor and the Python shell. 
+.PP
+Enter some Python code (eg.
+.B print("Hello world")
+) into the editor and save the file with Ctrl+S.
+.PP
+Now run the code by pressing F5. You should see the output of the program in the
+Python shell.
+.PP
+You can also enter Python code directly into the shell.
+.SH USING THE DEBUGGER
+You can see the steps Python takes to run your code.
+For this you need to press Ctrl+F5 to run the program in debug mode.
+In this mode you can advance the program either with big 
+steps (F6) or small steps (F7).
+If you want to see how the steps affect program variables, then open global
+variables pane (View => Variables).
+.SH MORE INFORMATION
+You can find more information, screenshots and a walk-through video at 
+http://thonny.org.
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4822369
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+jedi>=0.9
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..b14b0bc
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c8413b1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,65 @@
+from setuptools import setup, find_packages
+import os.path
+import sys
+
+if sys.version_info < (3,4):
+    raise RuntimeError("Thonny requires Python 3.4 or later")
+
+setupdir = os.path.dirname(__file__)
+
+with open(os.path.join(setupdir, 'thonny', 'VERSION'), encoding="ASCII") as f:
+    version = f.read().strip()
+
+requirements = []
+for line in open(os.path.join(setupdir, 'requirements.txt'), encoding="ASCII"):
+    if line.strip() and not line.startswith('#'):
+        requirements.append(line)
+
+setup(
+      name="thonny",
+      version=version,
+      description="Python IDE for beginners",
+      long_description="Thonny is a simple Python IDE with features useful for learning programming. See http://thonny.org for more info.",
+      url="http://thonny.org",
+      author="Aivar Annamaa and others",
+      author_email="thonny at googlegroups.com",
+      license="MIT",
+      classifiers=[
+        "Development Status :: 5 - Production/Stable",
+        "Environment :: MacOS X",
+        "Environment :: Win32 (MS Windows)",
+        "Environment :: X11 Applications",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Education",
+        "Intended Audience :: End Users/Desktop",
+        "License :: Freeware",
+        "License :: OSI Approved :: MIT License",
+        "Natural Language :: English",
+        "Operating System :: MacOS",
+        "Operating System :: Microsoft :: Windows",
+        "Operating System :: POSIX",
+        "Operating System :: POSIX :: Linux",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3 :: Only",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Topic :: Education",
+        "Topic :: Software Development",
+        "Topic :: Software Development :: Debuggers",
+        "Topic :: Text Editors",
+      ],
+      keywords="IDE education debugger",
+      platforms=["Windows", "macOS", "Linux"],
+      install_requires=requirements,
+      python_requires=">=3.4",
+      packages=find_packages(),
+      package_data={'': ['VERSION',  'res/*'],
+                    'thonny.plugins.help' : ['*.rst']},
+      entry_points={
+        'gui_scripts': [
+            'thonny = thonny:launch',
+        ]
+      },      
+)
\ No newline at end of file
diff --git a/thonny.egg-info/PKG-INFO b/thonny.egg-info/PKG-INFO
new file mode 100644
index 0000000..64aa9c9
--- /dev/null
+++ b/thonny.egg-info/PKG-INFO
@@ -0,0 +1,38 @@
+Metadata-Version: 1.2
+Name: thonny
+Version: 2.1.16
+Summary: Python IDE for beginners
+Home-page: http://thonny.org
+Author: Aivar Annamaa and others
+Author-email: thonny at googlegroups.com
+License: MIT
+Description: Thonny is a simple Python IDE with features useful for learning programming. See http://thonny.org for more info.
+Keywords: IDE education debugger
+Platform: Windows
+Platform: macOS
+Platform: Linux
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: MacOS X
+Classifier: Environment :: Win32 (MS Windows)
+Classifier: Environment :: X11 Applications
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Education
+Classifier: Intended Audience :: End Users/Desktop
+Classifier: License :: Freeware
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Natural Language :: English
+Classifier: Operating System :: MacOS
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Education
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Debuggers
+Classifier: Topic :: Text Editors
+Requires-Python: >=3.4
diff --git a/thonny.egg-info/SOURCES.txt b/thonny.egg-info/SOURCES.txt
new file mode 100644
index 0000000..fe792e6
--- /dev/null
+++ b/thonny.egg-info/SOURCES.txt
@@ -0,0 +1,125 @@
+CHANGELOG.rst
+CREDITS.rst
+LICENSE.txt
+MANIFEST.in
+README.rst
+requirements.txt
+setup.py
+licenses/ECLIPSE-ICONS-LICENSE.txt
+packaging/icons/thonny-128x128.png
+packaging/icons/thonny-16x16.png
+packaging/icons/thonny-2.png
+packaging/icons/thonny-22x22.png
+packaging/icons/thonny-256x256.png
+packaging/icons/thonny-32x32.png
+packaging/icons/thonny-48x48.png
+packaging/icons/thonny-64x64.png
+packaging/linux/org.thonny.Thonny.appdata.xml
+packaging/linux/org.thonny.Thonny.desktop
+packaging/linux/thonny.1
+thonny/VERSION
+thonny/__init__.py
+thonny/__main__.py
+thonny/ast_utils.py
+thonny/base_file_browser.py
+thonny/code.py
+thonny/codeview.py
+thonny/common.py
+thonny/config.py
+thonny/config_ui.py
+thonny/globals.py
+thonny/jedi_utils.py
+thonny/memory.py
+thonny/misc_utils.py
+thonny/roughparse.py
+thonny/running.py
+thonny/shell.py
+thonny/tktextext.py
+thonny/token_utils.py
+thonny/ui_utils.py
+thonny/workbench.py
+thonny.egg-info/PKG-INFO
+thonny.egg-info/SOURCES.txt
+thonny.egg-info/dependency_links.txt
+thonny.egg-info/entry_points.txt
+thonny.egg-info/requires.txt
+thonny.egg-info/top_level.txt
+thonny/plugins/__init__.py
+thonny/plugins/about.py
+thonny/plugins/ast_view.py
+thonny/plugins/autocomplete.py
+thonny/plugins/coloring.py
+thonny/plugins/commenting.py
+thonny/plugins/common_editing_commands.py
+thonny/plugins/debugger.py
+thonny/plugins/editor_config_page.py
+thonny/plugins/event_logging.py
+thonny/plugins/event_view.py
+thonny/plugins/find_replace.py
+thonny/plugins/font_config_page.py
+thonny/plugins/general_config_page.py
+thonny/plugins/goto_definition.py
+thonny/plugins/heap.py
+thonny/plugins/highlight_names.py
+thonny/plugins/interpreter_config_page.py
+thonny/plugins/locals_marker.py
+thonny/plugins/main_file_browser.py
+thonny/plugins/object_inspector.py
+thonny/plugins/outline.py
+thonny/plugins/paren_matcher.py
+thonny/plugins/pip_gui.py
+thonny/plugins/refactor.py
+thonny/plugins/replayer.py
+thonny/plugins/styler.py
+thonny/plugins/thonny_folders.py
+thonny/plugins/variables.py
+thonny/plugins/help/__init__.py
+thonny/plugins/help/help.rst
+thonny/plugins/system_shell/__init__.py
+thonny/plugins/system_shell/explain_environment.py
+thonny/res/16x16_blank.gif
+thonny/res/1x1_white.gif
+thonny/res/arrow_down2.gif
+thonny/res/class.gif
+thonny/res/closed_folder.gif
+thonny/res/file.new_file.gif
+thonny/res/file.open_file.gif
+thonny/res/file.save_file.gif
+thonny/res/folder.gif
+thonny/res/generic_file.gif
+thonny/res/gray_line.gif
+thonny/res/hard_drive.gif
+thonny/res/hard_drive2.gif
+thonny/res/method.gif
+thonny/res/open_folder.gif
+thonny/res/python_file.gif
+thonny/res/python_icon.gif
+thonny/res/run.debug_current_script.gif
+thonny/res/run.reset.gif
+thonny/res/run.run_current_script.gif
+thonny/res/run.run_to_cursor.gif
+thonny/res/run.step.gif
+thonny/res/run.step_into.gif
+thonny/res/run.step_out.gif
+thonny/res/run.step_over.gif
+thonny/res/run.stop.gif
+thonny/res/tab_close.gif
+thonny/res/tab_close_active.gif
+thonny/res/text_file.gif
+thonny/res/thonny.ico
+thonny/res/thonny.png
+thonny/res/thonny_small.ico
+thonny/shared/__init__.py
+thonny/shared/backend_launcher.py
+thonny/shared/thonny/__init__.py
+thonny/shared/thonny/ast_utils.py
+thonny/shared/thonny/backend.py
+thonny/shared/thonny/common.py
+thonny/test/__init__.py
+thonny/test/test_ast_utils.py
+thonny/test/test_ast_utils_mark_text_ranges.py
+thonny/test/plugins/__init__.py
+thonny/test/plugins/test_coloring.py
+thonny/test/plugins/test_locals_marker.py
+thonny/test/plugins/test_name_highlighter.py
+thonny/test/plugins/test_paren_matcher.py
\ No newline at end of file
diff --git a/thonny.egg-info/dependency_links.txt b/thonny.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/thonny.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/thonny.egg-info/entry_points.txt b/thonny.egg-info/entry_points.txt
new file mode 100644
index 0000000..619f42a
--- /dev/null
+++ b/thonny.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+[gui_scripts]
+thonny = thonny:launch
+
diff --git a/thonny.egg-info/requires.txt b/thonny.egg-info/requires.txt
new file mode 100644
index 0000000..4822369
--- /dev/null
+++ b/thonny.egg-info/requires.txt
@@ -0,0 +1 @@
+jedi>=0.9
diff --git a/thonny.egg-info/top_level.txt b/thonny.egg-info/top_level.txt
new file mode 100644
index 0000000..0ee6c45
--- /dev/null
+++ b/thonny.egg-info/top_level.txt
@@ -0,0 +1 @@
+thonny
diff --git a/thonny/VERSION b/thonny/VERSION
new file mode 100644
index 0000000..91dbb17
--- /dev/null
+++ b/thonny/VERSION
@@ -0,0 +1 @@
+2.1.16
\ No newline at end of file
diff --git a/thonny/__init__.py b/thonny/__init__.py
new file mode 100644
index 0000000..faa5678
--- /dev/null
+++ b/thonny/__init__.py
@@ -0,0 +1,171 @@
+import os.path
+import sys
+import runpy
+
+try:
+    runpy.run_module("thonny.customize", run_name="__main__")
+except ImportError:
+    pass
+
+
+THONNY_USER_DIR = os.environ.get("THONNY_USER_DIR", 
+                                 os.path.expanduser(os.path.join("~", ".thonny")))
+
+THONNY_USER_BASE = os.path.join(THONNY_USER_DIR, "plugins")
+
+def launch():
+    _prepare_thonny_user_dir()
+    
+    try:
+        _update_sys_path()
+        
+        from thonny import workbench
+        
+        if _should_delegate():
+            # First check if there is existing Thonny instance to handle the request
+            delegation_result = _try_delegate_to_existing_instance(sys.argv[1:])
+            if delegation_result == True:
+                # we're done
+                print("Delegated to an existing Thonny instance. Exiting now.")
+                return
+            
+            if hasattr(delegation_result, "accept"):
+                # we have server socket to put in use
+                server_socket = delegation_result
+            else:
+                server_socket = None
+                 
+            bench = workbench.Workbench(server_socket)
+        else:
+            bench = workbench.Workbench()
+            
+        try:
+            bench.mainloop()
+        except SystemExit:
+            bench.destroy()
+        return 0
+    except SystemExit as e:
+        from tkinter import messagebox
+        messagebox.showerror("System exit", str(e))
+    except:
+        from logging import exception
+        exception("Internal error")
+        import tkinter.messagebox
+        import traceback
+        tkinter.messagebox.showerror("Internal error", traceback.format_exc())
+        return -1
+    finally:
+        from thonny.globals import get_runner
+        runner = get_runner()
+        if runner != None:
+            runner.kill_backend()
+
+
+def _prepare_thonny_user_dir():
+    if not os.path.exists(THONNY_USER_DIR):
+        os.makedirs(THONNY_USER_DIR, mode=0o700, exist_ok=True)
+        
+        # user_dir_template is a post-installation means for providing
+        # alternative default user environment in multi-user setups
+        template_dir = os.path.join(os.path.dirname(__file__), "user_dir_template")
+                    
+        if os.path.isdir(template_dir):
+            import shutil
+            
+            def copy_contents(src_dir, dest_dir):
+                # I want the copy to have current user permissions
+                for name in os.listdir(src_dir):
+                    src_item = os.path.join(src_dir, name)
+                    dest_item = os.path.join(dest_dir, name)
+                    if os.path.isdir(src_item):
+                        os.makedirs(dest_item, mode=0o700)
+                        copy_contents(src_item, dest_item)
+                    else:
+                        shutil.copyfile(src_item, dest_item)
+                        os.chmod(dest_item, 0o600)
+                        
+            copy_contents(template_dir, THONNY_USER_DIR)
+            
+
+def _update_sys_path():
+    import site
+    
+    # remove old dir from path
+    if site.getusersitepackages() in sys.path:
+        sys.path.remove(site.getusersitepackages())
+        
+    # compute usersitepackages that plugins installation subprocess would see
+    import subprocess
+    env = os.environ.copy()
+    env["PYTHONUSERBASE"] = THONNY_USER_BASE
+    proc = subprocess.Popen(
+        [sys.executable.replace("thonny.exe", "pythonw.exe"),
+         "-c", "import site; print(site.getusersitepackages())"],
+        universal_newlines=True, env=env, stdout=subprocess.PIPE)
+    plugins_sitepackages = proc.stdout.readline().strip()
+    
+    sys.path.append(plugins_sitepackages)
+
+def _should_delegate():
+    from thonny import workbench
+    from thonny.config import try_load_configuration
+    configuration_manager = try_load_configuration(workbench.CONFIGURATION_FILE_NAME)
+    # Setting the default
+    configuration_manager.set_default("general.single_instance", workbench.SINGLE_INSTANCE_DEFAULT)
+    # getting the value (may use the default or return saved value)
+    return configuration_manager.get_option("general.single_instance")
+
+def _try_delegate_to_existing_instance(args):
+    import socket
+    from thonny import workbench
+    try:
+        # Try to create server socket.
+        # This is fastest way to find out if Thonny is already running
+        serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        serversocket.bind(("localhost", workbench.THONNY_PORT))
+        serversocket.listen(10)
+        # we were able to create server socket (ie. Thonny was not running)
+        # Let's use the socket in Thonny so that requests coming while 
+        # UI gets constructed don't get lost.
+        # (Opening several files with Thonny in Windows results in many
+        # Thonny processes opened quickly) 
+        return serversocket
+    except OSError:
+        # port was already taken, most likely by previous Thonny instance.
+        # Try to connect and send arguments
+        try:
+            return _delegate_to_existing_instance(args)
+        except:
+            import traceback
+            traceback.print_exc()
+            return False
+        
+        
+def _delegate_to_existing_instance(args):
+    import socket
+    from thonny import workbench
+    data = repr(args).encode(encoding='utf_8')
+    sock = socket.create_connection(("localhost", workbench.THONNY_PORT))
+    sock.sendall(data)
+    sock.shutdown(socket.SHUT_WR)
+    response = bytes([])
+    while len(response) < len(workbench.SERVER_SUCCESS):
+        new_data = sock.recv(2)
+        if len(new_data) == 0:
+            break
+        else:
+            response += new_data
+    
+    return response.decode("UTF-8") == workbench.SERVER_SUCCESS
+
+
+    
+def get_version():
+    try:
+        package_dir = os.path.dirname(sys.modules["thonny"].__file__)
+        with open(os.path.join(package_dir, "VERSION"), encoding="ASCII") as fp:
+            return fp.read().strip()
+    except:
+        return "0.0.0"
+      
+    
diff --git a/thonny/__main__.py b/thonny/__main__.py
new file mode 100644
index 0000000..2a291e2
--- /dev/null
+++ b/thonny/__main__.py
@@ -0,0 +1,3 @@
+from thonny import launch
+
+launch()
\ No newline at end of file
diff --git a/thonny/ast_utils.py b/thonny/ast_utils.py
new file mode 100644
index 0000000..4941c10
--- /dev/null
+++ b/thonny/ast_utils.py
@@ -0,0 +1,4 @@
+# This is a proxy module which gives frontend the illusion 
+# that ast_utils lives directly in thonny package
+# (this is the case for backend)
+from thonny.shared.thonny.ast_utils import *  # @UnusedWildImport
\ No newline at end of file
diff --git a/thonny/base_file_browser.py b/thonny/base_file_browser.py
new file mode 100644
index 0000000..c2994e2
--- /dev/null
+++ b/thonny/base_file_browser.py
@@ -0,0 +1,233 @@
+import os.path
+import tkinter as tk
+from tkinter import ttk
+
+from thonny.ui_utils import TreeFrame
+from thonny import misc_utils
+from thonny.globals import get_workbench
+
+
+_dummy_node_text = "..."
+    
+
+
+class BaseFileBrowser(TreeFrame):
+    def __init__(self, master, show_hidden_files=False, last_folder_setting_name=None): 
+        TreeFrame.__init__(self, master, 
+                           ["#0", "kind", "path"], 
+                           displaycolumns=(0,))
+        #print(self.get_toplevel_items())
+        
+        self.show_hidden_files = show_hidden_files
+        self.tree['show'] = ('tree',)
+        
+        self.hor_scrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
+        self.tree.config(xscrollcommand=self.hor_scrollbar.set)
+        self.hor_scrollbar['command'] = self.tree.xview
+        self.hor_scrollbar.grid(row=1, column=0, sticky="nsew")
+        
+        wb = get_workbench()
+        self.folder_icon = wb.get_image("folder.gif")
+        self.python_file_icon = wb.get_image("python_file.gif")
+        self.text_file_icon = wb.get_image("text_file.gif")
+        self.generic_file_icon = wb.get_image("generic_file.gif")
+        self.hard_drive_icon = wb.get_image("hard_drive2.gif")
+        
+        self.tree.column('#0', width=500, anchor=tk.W)
+        
+        # set-up root node
+        self.tree.set("", "kind", "root")
+        self.tree.set("", "path", "")
+        self.refresh_tree()
+        
+        
+        self.tree.bind("<<TreeviewOpen>>", self.on_open_node)
+        
+        self._last_folder_setting_name = last_folder_setting_name
+        self.open_initial_folder()
+    
+    def open_initial_folder(self):
+        if self._last_folder_setting_name:
+            path = get_workbench().get_option(self._last_folder_setting_name)
+            if path:
+                self.open_path_in_browser(path, True)
+    
+    def save_current_folder(self):
+        if not self._last_folder_setting_name:
+            return
+        
+        path = self.get_selected_path()
+        
+        if not path:
+            return
+        
+        if os.path.isfile(path):
+            path = os.path.dirname(path)
+        get_workbench().set_option(self._last_folder_setting_name, path)
+    
+    def on_open_node(self, event):
+        node_id = self.get_selected_node()
+        if node_id:
+            self.refresh_tree(node_id, True)
+    
+
+    
+    def get_selected_node(self):
+        nodes = self.tree.selection()
+        assert len(nodes) <= 1
+        if len(nodes) == 1:
+            return nodes[0]
+        else:
+            return None
+    
+    def get_selected_path(self):
+        node_id = self.get_selected_node()
+        
+        if node_id:
+            return self.tree.set(node_id, "path")
+        else:
+            return None
+    
+    def open_path_in_browser(self, path, see=True):
+        
+        # unfortunately os.path.split splits from the wrong end (for this case)
+        def split(path):
+            head, tail = os.path.split(path)
+            if head == "" and tail == "":
+                return []
+            elif head == path or tail == path:
+                return [path]
+            elif head == "":
+                return [tail]
+            elif tail == "":
+                return split(head)
+            else:
+                return split(head) + [tail]
+        
+        parts = split(path)
+        current_node_id = ""
+        current_path = ""
+        
+        while parts != []:
+            current_path = os.path.join(current_path, parts.pop(0))
+            
+            for child_id in self.tree.get_children(current_node_id):
+                child_path = self.tree.set(child_id, "path")
+                if child_path == current_path:
+                    self.tree.item(child_id, open=True)
+                    self.refresh_tree(child_id)
+                    current_node_id = child_id
+                    break
+        
+        if see:
+            self.tree.selection_set(current_node_id)
+            self.tree.focus(current_node_id)
+            
+            if self.tree.set(current_node_id, "kind") == "file":
+                self.tree.see(self.tree.parent(current_node_id))
+            else:
+                self.tree.see(current_node_id)    
+                
+    
+    def populate_tree(self):
+        for path in self.get_toplevel_items():
+            self.show_item(path, path, "", 2)
+    
+    def refresh_tree(self, node_id="", opening=None):
+        path = self.tree.set(node_id, "path")
+        #print("REFRESH", path)
+        
+        if os.path.isfile(path):
+            self.tree.set_children(node_id)
+            self.tree.item(node_id, open=False)
+        else:
+            # either root or directory
+            if node_id == "" or self.tree.item(node_id, "open") or opening == True:
+                fs_children_names = self.listdir(path, self.show_hidden_files)
+                tree_children_ids = self.tree.get_children(node_id) 
+                
+                # recollect children
+                children = {}
+                
+                # first the ones, which are present already in tree
+                for child_id in tree_children_ids:
+                    name = self.tree.item(child_id, "text")
+                    if name in fs_children_names:
+                        children[name] = child_id
+                
+                # add missing children
+                for name in fs_children_names:
+                    if name not in children:
+                        children[name] = self.tree.insert(node_id, "end")
+                        self.tree.set(children[name], "path", os.path.join(path, name))
+                        
+                
+                def file_order(name):
+                    # items in a folder should be ordered so that 
+                    # folders come first and names are ordered case insensitively
+                    return (os.path.isfile(os.path.join(path, name)),
+                            name.upper())
+                
+                # update tree
+                ids_sorted_by_name = list(map(lambda key: children[key],
+                                              sorted(children.keys(), key=file_order)))
+                self.tree.set_children(node_id, *ids_sorted_by_name)
+                
+                for child_id in ids_sorted_by_name:
+                    self.update_node_format(child_id)
+                    self.refresh_tree(child_id)
+                
+            else:
+                # closed dir
+                # Don't fetch children yet, but ensure that expand button is visible
+                children_ids = self.tree.get_children(node_id)
+                if len(children_ids) == 0:
+                    self.tree.insert(node_id, "end", text=_dummy_node_text) 
+    
+    def update_node_format(self, node_id):
+        assert node_id != ""
+        
+        path = self.tree.set(node_id, "path")
+        
+        
+        if os.path.isdir(path) or path.endswith(":") or path.endswith(":\\"):
+            self.tree.set(node_id, "kind", "dir")
+            if path.endswith(":") or path.endswith(":\\"):
+                img = self.hard_drive_icon
+            else:
+                img = self.folder_icon
+        else:
+            self.tree.set(node_id, "kind", "file")
+            if path.lower().endswith(".py"):
+                img = self.python_file_icon
+            elif path.lower().endswith(".txt") or path.lower().endswith(".csv"):
+                img = self.text_file_icon
+            else:
+                img = self.generic_file_icon
+            
+        # compute caption 
+        text = os.path.basename(path)
+        if text == "": # in case of drive roots 
+            text = path
+            
+        self.tree.item(node_id, text=" " + text, image=img)
+        self.tree.set(node_id, "path", path)
+    
+    def listdir(self, path="", include_hidden_files=False):
+        
+        if path == "" and misc_utils.running_on_windows():
+            result = misc_utils.get_win_drives()
+        else:
+            if path == "":
+                first_level = True
+                path = "/"
+            else:
+                first_level = False
+            result = [x for x in os.listdir(path) 
+                        if include_hidden_files 
+                            or not misc_utils.is_hidden_or_system_file(os.path.join(path, x))]
+            
+            if first_level:
+                result = ["/" + x for x in result]
+            
+        return sorted(result, key=str.upper)
diff --git a/thonny/code.py b/thonny/code.py
new file mode 100644
index 0000000..71f7c60
--- /dev/null
+++ b/thonny/code.py
@@ -0,0 +1,664 @@
+# -*- coding: utf-8 -*-
+import sys
+import os.path
+import tkinter as tk
+from tkinter import ttk, messagebox
+from tkinter.filedialog import asksaveasfilename
+from tkinter.filedialog import askopenfilename
+
+from thonny.misc_utils import eqfn, running_on_mac_os
+from thonny.codeview import CodeView
+from thonny.globals import get_workbench, get_runner
+from logging import exception
+from thonny.ui_utils import get_current_notebook_tab_widget, select_sequence
+from thonny.common import parse_shell_command
+from thonny.tktextext import rebind_control_a
+import tokenize
+from thonny.shared.thonny.common import ToplevelCommand, DebuggerCommand
+from tkinter.messagebox import askyesno
+import traceback
+
+_dialog_filetypes = [('Python files', '.py .pyw'), ('text files', '.txt'), ('all files', '.*')]
+
+
+                
+class Editor(ttk.Frame):
+    def __init__(self, master, filename=None):
+        
+        ttk.Frame.__init__(self, master)
+        assert isinstance(master, EditorNotebook)
+        
+        # parent of codeview will be workbench so that it can be maximized
+        self._code_view = CodeView(get_workbench(),
+                                   propose_remove_line_numbers=True,
+                                   font=get_workbench().get_font("EditorFont"))
+        get_workbench().event_generate("EditorTextCreated", editor=self, text_widget=self.get_text_widget())
+        
+        self._code_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self)
+        self._code_view.home_widget = self # don't forget home
+        self.maximizable_widget = self._code_view
+        
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        
+        self._filename = None
+        
+        if filename is not None:
+            self._load_file(filename)
+            self._code_view.text.edit_modified(False)
+        
+        self._code_view.text.bind("<<Modified>>", self._on_text_modified, True)
+        self._code_view.text.bind("<<TextChange>>", self._on_text_change, True)
+        self._code_view.text.bind("<Control-Tab>", self._control_tab, True)
+        
+        get_workbench().bind("AfterKnownMagicCommand", self._listen_for_execute, True)
+        get_workbench().bind("ToplevelResult", self._listen_for_toplevel_result, True)
+        
+        self.update_appearance()
+
+
+    def get_text_widget(self):
+        return self._code_view.text
+    
+    def get_code_view(self):
+        # TODO: try to get rid of this
+        return self._code_view
+    
+    def get_filename(self, try_hard=False):
+        if self._filename is None and try_hard:
+            self.save_file()
+            
+        return self._filename
+    
+    def get_long_description(self):
+        
+        if self._filename is None:
+            result = "<untitled>"
+        else:
+            result = self._filename
+        
+        try:
+            index = self._code_view.text.index("insert")
+            if index and "." in index:
+                line, col = index.split(".")
+                result += "  @  {} : {}".format(line, int(col)+1)
+        except:
+            exception("Finding cursor location")
+        
+        return result
+    
+    def _load_file(self, filename):
+        with tokenize.open(filename) as fp: # TODO: support also text files
+            source = fp.read() 
+            
+        self._filename = filename
+        get_workbench().event_generate("Open", editor=self, filename=filename)
+        self._code_view.set_content(source)
+        self._code_view.focus_set()
+        self.master.remember_recent_file(filename)
+        
+    def is_modified(self):
+        return self._code_view.text.edit_modified()
+    
+    
+    def save_file_enabled(self):
+        return self.is_modified() or not self.get_filename()
+    
+    def save_file(self, ask_filename=False):
+        if self._filename is not None and not ask_filename:
+            filename = self._filename
+            get_workbench().event_generate("Save", editor=self, filename=filename)
+        else:
+            # http://tkinter.unpythonic.net/wiki/tkFileDialog
+            filename = asksaveasfilename (
+                filetypes = _dialog_filetypes, 
+                defaultextension = ".py",
+                initialdir = get_workbench().get_option("run.working_directory")
+            )
+            if filename in ["", (), None]: # Different tkinter versions may return different values
+                return None
+            
+            # Seems that in some Python versions defaultextension 
+            # acts funny
+            if filename.lower().endswith(".py.py"):
+                filename = filename[:-3]
+            
+            get_workbench().event_generate("SaveAs", editor=self, filename=filename)
+                
+        
+        content = self._code_view.get_content()
+        encoding = "UTF-8" # TODO: check for marker in the head of the code
+        try: 
+            f = open(filename, mode="wb", )
+            f.write(content.encode(encoding))
+            f.close()
+        except PermissionError:
+            if askyesno("Permission Error",
+                     "Looks like this file or folder is not writable.\n\n"
+                     + "Do you want to save under another folder and/or filename?"):
+                return self.save_file(True)
+            else:
+                return None
+            
+
+        self._filename = filename
+        self.master.remember_recent_file(filename)
+        
+        self._code_view.text.edit_modified(False)
+
+        return self._filename
+    
+    def show(self):
+        self.master.select(self)
+    
+    def update_appearance(self):
+        self._code_view.set_line_numbers(get_workbench().get_option("view.show_line_numbers"))
+        self._code_view.set_line_length_margin(get_workbench().get_option("view.recommended_line_length"))
+        self._code_view.text.event_generate("<<UpdateAppearance>>")
+    
+    def _listen_for_execute(self, event):
+        command, args = parse_shell_command(event.cmd_line)
+        # Go read-only
+        if command.lower() == "debug":
+            if len(args) == 0:
+                return
+            filename = args[0]
+            self_filename = self.get_filename()
+            if self_filename is not None and os.path.basename(self_filename) == filename:
+                # Not that command has only basename
+                # so this solution may make more editors read-only than necessary
+                self._code_view.text.set_read_only(True)
+    
+    def _listen_for_toplevel_result(self, event):
+        self._code_view.text.set_read_only(False)
+    
+    def _control_tab(self, event):
+        if event.state & 1: # shift was pressed
+            direction = -1
+        else:
+            direction = 1
+        self.master.select_next_prev_editor(direction)
+        return "break"
+    
+    def _shift_control_tab(self, event):
+        self.master.select_next_prev_editor(-1)
+        return "break"
+    
+    def select_range(self, text_range):
+        self._code_view.select_range(text_range)
+    
+    def focus_set(self):
+        self._code_view.focus_set()
+    
+    def is_focused(self):
+        return self.focus_displayof() == self._code_view.text
+
+    def _on_text_modified(self, event):
+        try:
+            self.master.update_editor_title(self)
+        except:
+            traceback.print_exc()
+
+    def _on_text_change(self, event):
+        self.master.update_editor_title(self)
+        runner = get_runner()
+        if (runner.get_state() in ["running", "waiting_input", "waiting_debugger_command"]
+            and isinstance(runner.get_current_command(), (ToplevelCommand, DebuggerCommand))): # exclude running InlineCommands
+            runner.interrupt_backend()
+        
+    def destroy(self):
+        get_workbench().unbind("AfterKnownMagicCommand", self._listen_for_execute)
+        get_workbench().unbind("ToplevelResult", self._listen_for_toplevel_result)
+        ttk.Frame.destroy(self)
+    
+class EditorNotebook(ttk.Notebook):
+    """
+    Manages opened files / modules
+    """
+    def __init__(self, master):
+        _check_create_ButtonNotebook_style()
+        ttk.Notebook.__init__(self, master, padding=0, style="ButtonNotebook")
+        
+        get_workbench().set_default("file.reopen_all_files", False)
+        get_workbench().set_default("file.open_files", [])
+        get_workbench().set_default("file.current_file", None)
+        get_workbench().set_default("file.recent_files", [])
+        get_workbench().set_default("view.show_line_numbers", False)
+        get_workbench().set_default("view.recommended_line_length", 0)
+        
+        self._init_commands()
+        self.enable_traversal()
+        
+        # open files from last session
+        """ TODO: they should go only to recent files
+        for filename in prefs["open_files"].split(";"):
+            if os.path.exists(filename):
+                self._open_file(filename)
+        """
+        
+        self.update_appearance()
+    
+    def _list_recent_files(self):
+        pass
+        # TODO:
+        
+    
+    def _init_commands(self):    
+        # TODO: do these commands have to be in EditorNotebook ??
+        # Create a module level function install_editor_notebook ??
+        # Maybe add them separately, when notebook has been installed ??
+        
+        
+        get_workbench().add_command("new_file", "file", "New", 
+            self._cmd_new_file,
+            default_sequence=select_sequence("<Control-n>", "<Command-n>"),
+            group=10,
+            image_filename="file.new_file.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command("open_file", "file", "Open...", 
+            self._cmd_open_file,
+            default_sequence=select_sequence("<Control-o>", "<Command-o>"),
+            group=10,
+            image_filename="file.open_file.gif",
+            include_in_toolbar=True)
+        
+        # http://stackoverflow.com/questions/22907200/remap-default-keybinding-in-tkinter
+        get_workbench().bind_class("Text", "<Control-o>", self._control_o)
+        rebind_control_a(get_workbench())
+        
+        get_workbench().add_command("close_file", "file", "Close", 
+            self._cmd_close_file,
+            default_sequence=select_sequence("<Control-w>", "<Command-w>"),
+            tester=lambda: self.get_current_editor() is not None,
+            group=10)
+        
+        get_workbench().add_command("close_files", "file", "Close all", 
+            self._cmd_close_files,
+            tester=lambda: self.get_current_editor() is not None,
+            group=10)
+        
+        get_workbench().add_command("save_file", "file", "Save", 
+            self._cmd_save_file,
+            default_sequence=select_sequence("<Control-s>", "<Command-s>"),
+            tester=self._cmd_save_file_enabled,
+            group=10,
+            image_filename="file.save_file.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command("save_file_as", "file", "Save as...",
+            self._cmd_save_file_as,
+            default_sequence=select_sequence("<Control-Shift-S>", "<Command-Shift-S>"),
+            tester=lambda: self.get_current_editor() is not None,
+            group=10)
+        
+
+        get_workbench().createcommand("::tk::mac::OpenDocument", self._mac_open_document)
+    
+    
+    def load_startup_files(self):
+        """If no filename was sent from command line
+        then load previous files (if setting allows)"""  
+        
+        cmd_line_filenames = [name for name in sys.argv[1:] if os.path.exists(name)]
+        
+        
+        if len(cmd_line_filenames) > 0:
+            filenames = cmd_line_filenames
+        elif get_workbench().get_option("file.reopen_all_files"):
+            filenames = get_workbench().get_option("file.open_files")
+        elif get_workbench().get_option("file.current_file"):
+            filenames = [get_workbench().get_option("file.current_file")]
+        else:
+            filenames = []
+            
+        if len(filenames) > 0:
+            for filename in filenames:
+                if os.path.exists(filename):
+                    self.show_file(filename)
+            
+            cur_file = get_workbench().get_option("file.current_file")
+            # choose correct active file
+            if len(cmd_line_filenames) > 0:
+                self.show_file(cmd_line_filenames[0])
+            elif cur_file and os.path.exists(cur_file):
+                self.show_file(cur_file)
+            else:
+                self._cmd_new_file()
+        else:
+            self._cmd_new_file()
+            
+        
+        self._remember_open_files()
+    
+    def save_all_named_editors(self):
+        all_saved = True
+        for editor in self.winfo_children():
+            if editor.get_filename() and editor.is_modified():
+                success = editor.save_file()
+                all_saved = all_saved and success
+        
+        return all_saved
+    
+    def remember_recent_file(self, filename):
+        recents = get_workbench().get_option("file.recent_files")
+        if filename in recents:
+            recents.remove(filename)
+        recents.insert(0, filename)
+        existing_recents = [name for name in recents if os.path.exists(name)]
+        get_workbench().set_option("file.recent_files", existing_recents[:10])
+            
+    def _remember_open_files(self):
+        if (self.get_current_editor() is not None 
+            and self.get_current_editor().get_filename() is not None):
+            get_workbench().set_option("file.current_file", 
+                                       self.get_current_editor().get_filename())
+            
+        open_files = [editor.get_filename() 
+                          for editor in self.winfo_children() 
+                          if editor.get_filename()]
+        
+        get_workbench().set_option("file.open_files", open_files)
+        
+        if len(open_files) == 0:
+            get_workbench().set_option("file.current_file", None)
+    
+    def _cmd_new_file(self):
+        new_editor = Editor(self)
+        get_workbench().event_generate("NewFile", editor=new_editor)
+        self.add(new_editor, text=self._generate_editor_title(None))
+        self.select(new_editor)
+        new_editor.focus_set()
+    
+    def _cmd_open_file(self):
+        filename = askopenfilename (
+            filetypes = _dialog_filetypes, 
+            initialdir = get_workbench().get_option("run.working_directory")
+        )
+        if filename: # Note that missing filename may be "" or () depending on tkinter version
+            #self.close_single_untitled_unmodified_editor()
+            self.show_file(filename)
+            self._remember_open_files()
+    
+    def _control_o(self, event):
+        # http://stackoverflow.com/questions/22907200/remap-default-keybinding-in-tkinter
+        self._cmd_open_file()
+        return "break"
+    
+    def _cmd_close_files(self):
+        self._close_files(None)
+        
+    def _close_files(self, except_index=None):
+        
+        for tab_index in reversed(range(len(self.winfo_children()))):
+            if except_index is not None and tab_index == except_index:
+                continue
+            else:
+                editor = self._get_editor_by_index(tab_index)
+                if self.check_allow_closing(editor):
+                    self.forget(editor)
+                    editor.destroy()
+                
+        self._remember_open_files()
+        
+    
+    def _cmd_close_file(self):
+        self._close_file(None)
+    
+    def _close_file(self, index=None):
+        if index is None:
+            editor = self.get_current_editor()
+        else:
+            editor = self._get_editor_by_index(index)
+            
+        if editor:
+            if not self.check_allow_closing(editor):
+                return
+            self.forget(editor)
+            editor.destroy()
+            self._remember_open_files()
+    
+    def _cmd_save_file(self):
+        if self.get_current_editor():
+            self.get_current_editor().save_file()
+            self.update_editor_title(self.get_current_editor())
+        
+        self._remember_open_files()
+    
+    def _cmd_save_file_enabled(self):
+        return (self.get_current_editor() 
+            and self.get_current_editor().save_file_enabled())
+    
+    def _cmd_save_file_as(self):
+        if self.get_current_editor():
+            self.get_current_editor().save_file(ask_filename=True)
+            self.update_editor_title(self.get_current_editor())
+            
+        self._remember_open_files()
+    
+    def _cmd_save_file_as_enabled(self):
+        return self.get_current_editor() is not None
+    
+    def close_single_untitled_unmodified_editor(self):
+        editors = self.winfo_children()
+        if (len(editors) == 1 
+            and not editors[0].is_modified()
+            and not editors[0].get_filename()):
+            self._cmd_close_file()
+    
+    def _mac_open_document(self, *args):
+        for arg in args:
+            if isinstance(arg, str) and os.path.exists(arg):
+                self.show_file(arg)
+        get_workbench().become_topmost_window()
+        
+    def get_current_editor(self):
+        return get_current_notebook_tab_widget(self)
+    
+    def select_next_prev_editor(self, direction):
+        cur_index = self.index(self.select())
+        next_index = (cur_index + direction) % len(self.tabs())
+        self.select(self._get_editor_by_index(next_index))
+        
+    
+    def _get_editor_by_index(self, index):
+        tab_id = self.tabs()[index]
+        for child in self.winfo_children():
+            if str(child) == tab_id:
+                return child
+        return None
+    
+    def show_file(self, filename, text_range=None):
+        #self.close_single_untitled_unmodified_editor()
+        editor = self.get_editor(filename, True)
+        assert editor is not None
+        
+        self.select(editor)
+        editor.focus_set()
+        
+        if text_range is not None:
+            editor.select_range(text_range)
+            
+        return editor
+    
+    def update_appearance(self):
+        for editor in self.winfo_children():
+            editor.update_appearance()
+    
+    def update_editor_title(self, editor):
+        self.tab(editor,
+            text=self._generate_editor_title(editor.get_filename(), editor.is_modified()))
+    
+     
+    def _generate_editor_title(self, filename, is_modified=False):
+        if filename is None:
+            result = "<untitled>"
+        else:
+            result = os.path.basename(filename)
+        
+        if is_modified:
+            result += " *"
+        
+        return result
+    
+    def _open_file(self, filename):
+        editor = Editor(self, filename)
+        self.add(editor, text=self._generate_editor_title(filename))
+              
+        return editor
+        
+    def get_editor(self, filename, open_when_necessary=False):
+        for child in self.winfo_children():
+            child_filename = child.get_filename(False)
+            if child_filename and eqfn(child.get_filename(), filename):
+                return child
+        
+        if open_when_necessary:
+            return self._open_file(filename)
+        else:
+            return None
+    
+    
+    def focus_set(self):
+        editor = self.get_current_editor()
+        if editor: 
+            editor.focus_set()
+        else:
+            super().focus_set()
+
+    def current_editor_is_focused(self):
+        editor = self.get_current_editor()
+        return editor.is_focused()
+
+    
+    def check_allow_closing(self, editor=None):
+        if not editor:
+            modified_editors = [e for e in self.winfo_children() if e.is_modified()]
+        else:
+            if not editor.is_modified():
+                return True
+            else:
+                modified_editors = [editor]
+        if len(modified_editors) == 0:
+            return True
+        
+        message = "Do you want to save files before closing?"
+        if editor:
+            message = "Do you want to save file before closing?"
+            
+        confirm = messagebox.askyesnocancel(
+                  title="Save On Close",
+                  message=message,
+                  default=messagebox.YES,
+                  master=self)
+        
+        if confirm:
+            for editor in modified_editors:
+                if editor.get_filename(True):
+                    editor.save_file()
+                else:
+                    return False
+            return True
+        
+        elif confirm is None:
+            return False
+        else:
+            return True
+        
+
+def _check_create_ButtonNotebook_style():
+    """Taken from http://svn.python.org/projects/python/trunk/Demo/tkinter/ttk/notebook_closebtn.py"""
+    style = ttk.Style()
+    if "closebutton" in style.element_names():
+        # It's done already
+        return
+
+    get_workbench().get_image('tab_close.gif', "img_close")
+    get_workbench().get_image('tab_close_active.gif', "img_close_active")
+    
+    style.element_create("closebutton", "image", "img_close",
+        ("active", "pressed", "!disabled", "img_close_active"),
+        ("active", "!disabled", "img_close_active"), border=8, sticky='')
+
+    style.layout("ButtonNotebook", [("Notebook.client", {"sticky": "nswe"})])
+
+    style.layout("ButtonNotebook.Tab", [
+        ("ButtonNotebook.tab", {"sticky": "nswe", "children":
+            [("ButtonNotebook.padding", {"side": "top", "sticky": "nswe",
+                                         "children":
+                [("ButtonNotebook.focus", {"side": "top", "sticky": "nswe",
+                                           "children":
+                    [("ButtonNotebook.label", {"side": "left", "sticky": ''}),
+                     ("ButtonNotebook.closebutton", {"side": "left", "sticky": ''})
+                     ]
+                })]
+            })]
+        })]
+    )
+    
+    menu = tk.Menu(get_workbench(), tearoff=False)
+    menu.popup_index = None
+    
+    menu.add_command(label="Close",
+                     command=lambda:get_workbench().get_editor_notebook()._close_file(menu.popup_index))
+    menu.add_command(label="Close others",
+                     command=lambda:get_workbench().get_editor_notebook()._close_files(menu.popup_index))
+    menu.add_command(label="Close all",
+                     command=lambda:get_workbench().get_editor_notebook()._close_files())
+
+    def letf_btn_press(event):
+        try:
+            x, y, widget = event.x, event.y, event.widget
+            elem = widget.identify(x, y)
+            index = widget.index("@%d,%d" % (x, y))
+        
+            if "closebutton" in elem:
+                widget.state(['pressed'])
+                widget.pressed_index = index
+        except:
+            # may fail, if clicked outside of tab
+            pass
+    
+    def left_btn_release(event):
+        x, y, widget = event.x, event.y, event.widget
+    
+        if not widget.instate(['pressed']):
+            return
+    
+        try:
+            elem =  widget.identify(x, y)
+            index = widget.index("@%d,%d" % (x, y))
+        
+            if "closebutton" in elem and widget.pressed_index == index:
+                if isinstance(widget, EditorNotebook):
+                    widget._cmd_close_file()
+                else:
+                    widget.forget(index)
+                    widget.event_generate("<<NotebookClosedTab>>")
+        
+            widget.state(["!pressed"])
+            widget.pressed_index = None
+        except:
+            # may fail, when mouse is dragged
+            exception("Closing tab")
+    
+    def right_btn_press(event):
+        x, y, widget = event.x, event.y, event.widget
+        try:
+            if "ButtonNotebook" in widget["style"]:
+                index = widget.index("@%d,%d" % (x, y))
+                menu.popup_index = index
+                menu.tk_popup(*get_workbench().winfo_pointerxy())
+        except:
+            pass
+    
+    get_workbench().bind_class("TNotebook", "<ButtonPress-1>", letf_btn_press, True)
+    get_workbench().bind_class("TNotebook", "<ButtonRelease-1>", left_btn_release, True)
+    if running_on_mac_os():
+        get_workbench().bind_class("TNotebook", "<ButtonPress-2>", right_btn_press, True)
+        get_workbench().bind_class("TNotebook", "<Control-Button-1>", right_btn_press, True)
+    else:  
+        get_workbench().bind_class("TNotebook", "<ButtonPress-3>", right_btn_press, True)
+    
+    
+
diff --git a/thonny/codeview.py b/thonny/codeview.py
new file mode 100644
index 0000000..ddc6a1b
--- /dev/null
+++ b/thonny/codeview.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+
+import tkinter as tk
+from thonny.common import TextRange
+from thonny.globals import get_workbench
+from thonny import tktextext, roughparse
+from thonny.ui_utils import EnhancedTextWithLogging
+from thonny.tktextext import EnhancedText
+
+EDIT_BACKGROUND="white"
+READ_ONLY_BACKGROUND="LightYellow"
+
+class PythonText(EnhancedText):
+    
+    def perform_return(self, event):
+        # copied from idlelib.EditorWindow (Python 3.4.2)
+        # slightly modified
+        
+        text = event.widget
+        assert text is self
+        
+        try:
+            # delete selection
+            first, last = text.get_selection_indices()
+            if first and last:
+                text.delete(first, last)
+                text.mark_set("insert", first)
+            
+            # Strip whitespace after insert point
+            # (ie. don't carry whitespace from the right of the cursor over to the new line)
+            while text.get("insert") in [" ", "\t"]:
+                text.delete("insert")
+            
+            left_part = text.get("insert linestart", "insert")
+            # locate first non-white character
+            i = 0
+            n = len(left_part)
+            while i < n and left_part[i] in " \t":
+                i = i+1
+            
+            # is it only whitespace?
+            if i == n:
+                # start the new line with the same whitespace
+                text.insert("insert", '\n' + left_part)
+                return "break"
+            
+            # Turned out the left part contains visible chars
+            # Remember the indent
+            indent = left_part[:i]
+            
+            # Strip whitespace before insert point
+            # (ie. after inserting the linebreak this line doesn't have trailing whitespace)
+            while text.get("insert-1c", "insert") in [" ", "\t"]:
+                text.delete("insert-1c", "insert")
+                
+            # start new line
+            text.insert("insert", '\n')
+    
+            # adjust indentation for continuations and block
+            # open/close first need to find the last stmt
+            lno = tktextext.index2line(text.index('insert'))
+            y = roughparse.RoughParser(text.indentwidth, text.tabwidth)
+            
+            for context in roughparse.NUM_CONTEXT_LINES:
+                startat = max(lno - context, 1)
+                startatindex = repr(startat) + ".0"
+                rawtext = text.get(startatindex, "insert")
+                y.set_str(rawtext)
+                bod = y.find_good_parse_start(
+                          False,
+                          roughparse._build_char_in_string_func(startatindex))
+                if bod is not None or startat == 1:
+                    break
+            y.set_lo(bod or 0)
+    
+            c = y.get_continuation_type()
+            if c != roughparse.C_NONE:
+                # The current stmt hasn't ended yet.
+                if c == roughparse.C_STRING_FIRST_LINE:
+                    # after the first line of a string; do not indent at all
+                    pass
+                elif c == roughparse.C_STRING_NEXT_LINES:
+                    # inside a string which started before this line;
+                    # just mimic the current indent
+                    text.insert("insert", indent)
+                elif c == roughparse.C_BRACKET:
+                    # line up with the first (if any) element of the
+                    # last open bracket structure; else indent one
+                    # level beyond the indent of the line with the
+                    # last open bracket
+                    text._reindent_to(y.compute_bracket_indent())
+                elif c == roughparse.C_BACKSLASH:
+                    # if more than one line in this stmt already, just
+                    # mimic the current indent; else if initial line
+                    # has a start on an assignment stmt, indent to
+                    # beyond leftmost =; else to beyond first chunk of
+                    # non-whitespace on initial line
+                    if y.get_num_lines_in_stmt() > 1:
+                        text.insert("insert", indent)
+                    else:
+                        text._reindent_to(y.compute_backslash_indent())
+                else:
+                    assert 0, "bogus continuation type %r" % (c,)
+                return "break"
+    
+            # This line starts a brand new stmt; indent relative to
+            # indentation of initial line of closest preceding
+            # interesting stmt.
+            indent = y.get_base_indent_string()
+            text.insert("insert", indent)
+            if y.is_block_opener():
+                text.perform_smart_tab(event)
+            elif indent and y.is_block_closer():
+                text.perform_smart_backspace(event)
+            return "break"
+        finally:
+            text.see("insert")
+            text.event_generate("<<NewLine>>")
+            return "break" 
+
+
+class CodeViewText(EnhancedTextWithLogging, PythonText):
+    """Provides opportunities for monkey-patching by plugins"""
+    def __init__(self, master=None, cnf={}, **kw):
+        if not "background" in kw:
+            kw["background"] = EDIT_BACKGROUND
+        
+        EnhancedTextWithLogging.__init__(self, master=master, cnf=cnf, **kw)
+        self._original_background = kw["background"]
+        # Allow binding to events of all CodeView texts
+        self.bindtags(self.bindtags() + ('CodeViewText',))
+        tktextext.fixwordbreaks(tk._default_root)
+    
+    def set_read_only(self, value):
+        EnhancedTextWithLogging.set_read_only(self, value)
+        if value:
+            self.configure(background=READ_ONLY_BACKGROUND)
+        else:
+            self.configure(background=self._original_background)
+
+    
+    def on_secondary_click(self, event):
+        super().on_secondary_click(event)
+        get_workbench().get_menu("edit").tk_popup(event.x_root, event.y_root)
+
+class CodeView(tktextext.TextFrame):
+    def __init__(self, master, propose_remove_line_numbers=False, **text_frame_args):
+        tktextext.TextFrame.__init__(self, master, text_class=CodeViewText,
+                                     undo=True, wrap=tk.NONE, background=EDIT_BACKGROUND,
+                                     **text_frame_args)
+        
+        # TODO: propose_remove_line_numbers on paste??
+        
+        self.text.bind("<<TextChange>>", self._on_text_changed, True)
+        
+    def get_content(self):
+        return self.text.get("1.0", "end-1c") # -1c because Text always adds a newline itself
+    
+    def set_content(self, content):
+        self.text.direct_delete("1.0", tk.END)
+        self.text.direct_insert("1.0", content)
+        self.update_line_numbers()
+        self.text.edit_reset();
+
+        self.text.event_generate("<<TextChange>>")
+    
+    def _on_text_changed(self, event):
+        self.update_line_numbers()
+        self.update_margin_line()
+    
+    def select_lines(self, first_line, last_line):
+        self.text.tag_remove("sel", "1.0", tk.END)
+        self.text.tag_add("sel", "%s.0" % first_line, "%s.end" % last_line)
+    
+    def select_range(self, text_range):
+        self.text.tag_remove("sel", "1.0", tk.END)
+        
+        if text_range:
+            if isinstance(text_range, int):
+                # it's line number
+                start = str(text_range - self._first_line_number + 1) + ".0"
+                end = str(text_range - self._first_line_number + 1) + ".end"
+            elif isinstance(text_range, TextRange):
+                start = "%s.%s" % (text_range.lineno - self._first_line_number + 1, text_range.col_offset)
+                end = "%s.%s" % (text_range.end_lineno - self._first_line_number + 1, text_range.end_col_offset)
+            else:
+                assert isinstance(text_range, tuple)
+                start, end  = text_range
+                
+            self.text.tag_add("sel", start, end)
+            if isinstance(text_range, int):
+                self.text.mark_set("insert", end) 
+            self.text.see("%s -1 lines" % start)
+            
+    
+    def get_selected_range(self):
+        if self.text.has_selection():
+            lineno, col_offset = map(int, self.text.index(tk.SEL_FIRST).split("."))
+            end_lineno, end_col_offset = map(int, self.text.index(tk.SEL_LAST).split("."))
+        else:
+            lineno, col_offset = map(int, self.text.index(tk.INSERT).split("."))
+            end_lineno, end_col_offset = lineno, col_offset
+            
+        return TextRange(lineno, col_offset, end_lineno, end_col_offset)
diff --git a/thonny/common.py b/thonny/common.py
new file mode 100644
index 0000000..a7f0eb6
--- /dev/null
+++ b/thonny/common.py
@@ -0,0 +1,4 @@
+# This is a proxy module which gives frontend the illusion 
+# that common lives directly in thonny package
+# (this is the case for backend)
+from thonny.shared.thonny.common import *  # @UnusedWildImport
\ No newline at end of file
diff --git a/thonny/config.py b/thonny/config.py
new file mode 100644
index 0000000..f108158
--- /dev/null
+++ b/thonny/config.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+
+import tkinter as tk
+import os.path
+import ast
+from configparser import ConfigParser
+import configparser
+from logging import exception
+from tkinter import messagebox
+
+def try_load_configuration(filename):
+    try: 
+        return ConfigurationManager(filename)
+    except configparser.Error:
+        if (os.path.exists(filename) 
+            and messagebox.askyesno("Problem", 
+                "Thonny's configuration file can't be read. It may be corrupt.\n\n"
+                + "Do you want to discard the file and open Thonny with default settings?")):
+            os.replace(filename, filename + "_corrupt")
+            # For some reasons Thonny styles are not loaded properly once messagebox has been shown before main window (At least Windows Py 3.5)
+            raise SystemExit("Configuration file has been discarded. Please restart Thonny!")
+        else:
+            raise
+    
+
+class ConfigurationManager:
+    def __init__(self, filename):
+        self._ini = ConfigParser()
+        self._filename = filename
+        self._defaults = {}
+        self._variables = {} # Tk variables
+        
+        if os.path.exists(self._filename):
+            with open(self._filename, 'r', encoding="UTF-8") as fp: 
+                self._ini.read_file(fp)
+
+        #print(prefs_filename, self.sections())
+    
+    def get_option(self, name, secondary_default=None):
+        section, option = self._parse_name(name)
+        name = section + "." + option
+        
+        # variable may have more recent value
+        if name in self._variables:
+            return self._variables[name].get()
+            
+        try:
+            val = self._ini.get(section, option)
+            try:
+                return ast.literal_eval(val)
+            except:
+                return val
+        except:
+            if name in self._defaults:
+                return self._defaults[name]
+            else:
+                return secondary_default
+            
+    
+    def has_option(self, name):
+        return name in self._defaults
+    
+    def set_option(self, name, value):
+        section, option = self._parse_name(name)
+        name = section + "." + option
+        if not self._ini.has_section(section):
+            self._ini.add_section(section)
+        
+        if isinstance(value, str):
+            self._ini.set(section, option, value)
+        else:
+            self._ini.set(section, option, repr(value))
+        
+        # update variable
+        if name in self._variables:
+            self._variables[name].set(value)
+    
+    def set_default(self, name, primary_default_value):
+        section, option = self._parse_name(name)
+        name = section + "." + option
+        self._defaults[name] = primary_default_value
+
+    def get_variable(self, name):
+        section, option = self._parse_name(name)
+        name = section + "." + option
+        
+        if name in self._variables:
+            return self._variables[name]
+        else:
+            value = self.get_option(name)
+            if isinstance(value, bool):
+                var = tk.BooleanVar(value=value)
+            elif isinstance(value, int):
+                var = tk.IntVar(value=value)
+            elif isinstance(value, str):
+                var = tk.StringVar(value=value)
+            else:
+                raise KeyError("Can't create Tk Variable for " + name)
+            self._variables[name] = var
+            return var
+    
+    def save(self):
+        # save all tk variables
+        for name in self._variables:
+            self.set_option(name, self._variables[name].get())
+            
+        # store
+        if not os.path.exists(self._filename):
+            os.makedirs(os.path.dirname(self._filename), mode=0o700, exist_ok=True)
+
+        # Normal saving occasionally creates corrupted file:
+        # https://bitbucket.org/plas/thonny/issues/167/configuration-file-occasionally-gets
+        # Now I'm saving the configuration to a temp file 
+        # and if the save is successful, I replace configuration file with it
+        temp_filename = self._filename + ".temp"             
+        with open(temp_filename, 'w', encoding="UTF-8") as fp:
+            self._ini.write(fp)
+            
+        try:
+            ConfigurationManager(temp_filename)
+            # temp file was created successfully
+            os.chmod(temp_filename, 0o600)
+            os.replace(temp_filename, self._filename)
+            os.chmod(self._filename, 0o600)
+        except:
+            exception("Could not save configuration file. Reverting to previous file.")
+        
+
+    def _parse_name(self, name):
+        if "." in name:
+            return name.split(".", 1)
+        else:
+            return "general", name 
diff --git a/thonny/config_ui.py b/thonny/config_ui.py
new file mode 100644
index 0000000..45641a5
--- /dev/null
+++ b/thonny/config_ui.py
@@ -0,0 +1,78 @@
+import tkinter as tk
+from tkinter import ttk
+from thonny.globals import get_workbench
+
+
+class ConfigurationDialog(tk.Toplevel):
+    def __init__(self, master, page_records):
+        tk.Toplevel.__init__(self, master)
+        width = 400
+        height = 400
+        left = max(int(get_workbench().winfo_x() + get_workbench().winfo_width()/2 - width/2), 0)
+        top = max(int(get_workbench().winfo_y() + get_workbench().winfo_height()/2 - height/2), 0)
+        self.geometry("%dx%d+%d+%d" % (width, height, left, top))
+        self.title("Thonny options")
+        
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        
+        main_frame = ttk.Frame(self) # otherwise there is wrong color background with clam
+        main_frame.grid(row=0, column=0, sticky=tk.NSEW)
+        main_frame.columnconfigure(0, weight=1)
+        main_frame.rowconfigure(0, weight=1)
+        
+        self._notebook = ttk.Notebook(main_frame)
+        self._notebook.grid(row=0, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10)
+        
+        self._ok_button = ttk.Button(main_frame, text="OK", command=self._ok, default="active")
+        self._cancel_button = ttk.Button(main_frame, text="Cancel", command=self._cancel)
+        self._ok_button.grid(row=1, column=1, padx=(0,11), pady=(0,10))
+        self._cancel_button.grid(row=1, column=2, padx=(0,11), pady=(0,10))
+        
+        self._pages = {}
+        for title in sorted(page_records):
+            page_class = page_records[title]
+            spacer = ttk.Frame(self)
+            spacer.rowconfigure(0, weight=1)
+            spacer.columnconfigure(0, weight=1)
+            page = page_class(spacer)
+            self._pages[title] = page
+            page.grid(sticky=tk.NSEW, pady=10, padx=15)
+            self._notebook.add(spacer, text=title)
+        
+        self.bind("<Return>", self._ok, True)
+        self.bind("<Escape>", self._cancel, True)
+    
+    def _ok(self, event=None):
+        for title in sorted(self._pages):
+            try:
+                page = self._pages[title]
+                if page.apply() == False:
+                    return
+            except:
+                get_workbench().report_exception("Error when applying options in " + title)
+             
+        self.destroy()
+    
+    def _cancel(self, event=None):
+        self.destroy()
+
+class ConfigurationPage(ttk.Frame):
+    """This is an example dummy implementation of a configuration page.
+    
+    It's not required that configuration pages inherit from this class
+    (can be any widget), but the class must have constructor with single parameter
+    for getting the master."""
+    def __init__(self, master):
+        ttk.Frame.__init__(self, master)
+    
+    def add_checkbox(self, flag_name, description, row=None, pady=0, columnspan=1):
+        variable = get_workbench().get_variable(flag_name)
+        checkbox = ttk.Checkbutton(self, text=description, variable=variable)
+        checkbox.grid(row=row, column=0, sticky=tk.W, pady=pady, columnspan=columnspan)
+                
+    def apply(self):
+        """Apply method should return False, when page contains invalid
+        input and configuration dialog should not be closed."""
+        pass
+        
\ No newline at end of file
diff --git a/thonny/globals.py b/thonny/globals.py
new file mode 100644
index 0000000..31ee7a1
--- /dev/null
+++ b/thonny/globals.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+
+_worbench = None
+_runner = None
+
+def register_workbench(workbench):
+    global _workbench
+    _workbench = workbench
+
+def register_runner(runner):
+    global _runner
+    _runner = runner
+
+
+def get_workbench():    
+    return _workbench
+
+def get_runner():    
+    return _runner
+    
\ No newline at end of file
diff --git a/thonny/jedi_utils.py b/thonny/jedi_utils.py
new file mode 100644
index 0000000..3374b85
--- /dev/null
+++ b/thonny/jedi_utils.py
@@ -0,0 +1,81 @@
+# Utils to handle different jedi versions
+
+def import_tree():
+    try:
+        # jedi 0.11
+        from parso.python import tree
+    except ImportError:
+        try:
+            # jedi 0.10
+            from jedi.parser.python import tree
+        except ImportError:
+            # jedi 0.9
+            try:
+                from jedi.parser import tree
+            except:
+                # older versions
+                tree = None
+    
+    return tree
+
+def get_params(func_node):
+    if hasattr(func_node, "get_params"):
+        # parso
+        return func_node.get_params()
+    else:
+        # older jedi
+        return func_node.params
+
+def get_parent_scope(node):    
+    try:
+        # jedi 0.11
+        from jedi import parser_utils
+        return parser_utils.get_parent_scope(node)
+    except ImportError:
+        # Older versions
+        return node.get_parent_scope()
+
+def get_statement_of_position(node, pos):    
+    try:
+        # jedi 0.11
+        from jedi.parser_utils import get_statement_of_position
+        return get_statement_of_position(node, pos)
+    except ImportError:
+        # Older versions
+        return node.get_statement_for_position(pos)
+
+def get_module_node(script):
+    if hasattr(script, "_get_module_node"):
+        return script._get_module_node()
+    elif hasattr(script, "_get_module"):
+        return script._get_module()
+    else:
+        return script._parser.module()
+
+def is_scope(node):
+    try:
+        # jedi 0.11
+        from jedi import parser_utils
+        return parser_utils.is_scope(node)
+    except ImportError:
+        # Older versions
+        return node.is_scope()
+
+def get_name_of_position(obj, position):
+    if hasattr(obj, "get_name_of_position"):
+        # parso
+        return obj.get_name_of_position(position)
+    else:
+        # older jedi
+        return obj.name_for_position(position)
+
+def get_version_tuple():
+    import jedi
+    nums = []
+    for item in jedi.__version__.split("."):
+        try:
+            nums.append(int(item))
+        except:
+            nums.append(0)
+            
+    return tuple(nums)
\ No newline at end of file
diff --git a/thonny/memory.py b/thonny/memory.py
new file mode 100644
index 0000000..53f7299
--- /dev/null
+++ b/thonny/memory.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+ 
+import tkinter as tk
+import tkinter.font as tk_font
+
+from thonny.ui_utils import TreeFrame
+from thonny.misc_utils import shorten_repr
+from thonny.globals import get_workbench
+
+MAX_REPR_LENGTH_IN_GRID = 100
+
+def format_object_id(object_id):
+    # this format aligns with how Python shows memory addresses
+    if object_id is None:
+        return None
+    else:
+        return "0x" + hex(object_id)[2:].upper() #.rjust(8,'0')
+
+def parse_object_id(object_id_repr):
+    return int(object_id_repr, base=16)
+
+class MemoryFrame(TreeFrame):
+    def __init__(self, master, columns):
+        TreeFrame.__init__(self, master, columns)
+        
+        font = tk_font.nametofont("TkDefaultFont").copy()
+        font.configure(underline=True)
+        self.tree.tag_configure("hovered", font=font)
+    
+    def stop_debugging(self):
+        self._clear_tree()
+        
+    def show_selected_object_info(self):
+        iid = self.tree.focus()
+        if iid != '':
+            # NB! Assuming id is second column!
+            id_str = self.tree.item(iid)['values'][1]
+            if id_str in ["", None, "None"]:
+                return
+            
+            object_id = parse_object_id(id_str)
+            get_workbench().event_generate("ObjectSelect", object_id=object_id)
+    
+    
+        
+class VariablesFrame(MemoryFrame):
+    def __init__(self, master):
+        MemoryFrame.__init__(self, master, ('name', 'id', 'value'))
+    
+        self.tree.column('name', width=120, anchor=tk.W, stretch=False)
+        self.tree.column('id', width=450, anchor=tk.W, stretch=True)
+        self.tree.column('value', width=450, anchor=tk.W, stretch=True)
+        
+        self.tree.heading('name', text='Name', anchor=tk.W) 
+        self.tree.heading('id', text='Value ID', anchor=tk.W)
+        self.tree.heading('value', text='Value', anchor=tk.W)
+        
+        get_workbench().bind("ShowView", self._update_memory_model, True)
+        get_workbench().bind("HideView", self._update_memory_model, True)
+        self._update_memory_model()
+        #self.tree.tag_configure("item", font=ui_utils.TREE_FONT)
+        
+    def destroy(self):
+        MemoryFrame.destroy(self)
+        get_workbench().unbind("ShowView", self._update_memory_model)
+        get_workbench().unbind("HideView", self._update_memory_model)
+
+    def _update_memory_model(self, event=None):
+        if get_workbench().in_heap_mode():
+            self.tree.configure(displaycolumns=("name", "id"))
+            #self.tree.columnconfigure(1, weight=1, width=400)
+            #self.tree.columnconfigure(2, weight=0)
+        else:
+            self.tree.configure(displaycolumns=("name", "value"))
+            #self.tree.columnconfigure(1, weight=0)
+            #self.tree.columnconfigure(2, weight=1, width=400)
+
+    def update_variables(self, variables):
+        self._clear_tree()
+        
+        if variables:
+            for name in sorted(variables.keys()):
+                
+                if not name.startswith("__"):
+                    node_id = self.tree.insert("", "end", tags="item")
+                    self.tree.set(node_id, "name", name)
+                    if isinstance(variables[name], dict):
+                        repr_str = variables[name]["repr"]
+                        id_str = variables[name]["id"]
+                    else:
+                        repr_str = variables[name]
+                        id_str = None
+                        
+                    self.tree.set(node_id, "id", format_object_id(id_str))
+                    self.tree.set(node_id, "value", shorten_repr(repr_str, MAX_REPR_LENGTH_IN_GRID))
+    
+    
+    def on_select(self, event):
+        self.show_selected_object_info()
+        
+
diff --git a/thonny/misc_utils.py b/thonny/misc_utils.py
new file mode 100644
index 0000000..5ff03d8
--- /dev/null
+++ b/thonny/misc_utils.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+
+import os.path
+import platform
+import sys
+import shutil
+import time
+
+
+def eqfn(name1, name2):
+    return os.path.normcase(name1) == os.path.normcase(name2)
+
+def delete_dir_try_hard(path, hardness=5):
+    # Deleting the folder on Windows is not so easy task
+    # http://bugs.python.org/issue15496
+    for i in range(hardness):
+        if os.path.exists(path):
+            time.sleep(i * 0.5)
+            shutil.rmtree(path, True)
+        else:
+            break
+
+    if os.path.exists(path):
+        # try once more but now without ignoring errors
+        shutil.rmtree(path, False)
+
+def running_on_windows():
+    return platform.system() == "Windows"
+    
+def running_on_mac_os():
+    return platform.system() == "Darwin"
+    
+def running_on_linux():
+    return platform.system() == "Linux"
+
+def is_hidden_or_system_file(path):
+    if os.path.basename(path).startswith("."):
+        return True
+    elif running_on_windows():
+        from ctypes import windll
+        FILE_ATTRIBUTE_HIDDEN = 0x2
+        FILE_ATTRIBUTE_SYSTEM = 0x4
+        return bool(windll.kernel32.GetFileAttributesW(path)  # @UndefinedVariable
+                & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM))
+    else:
+        return False 
+    
+def get_win_drives():
+    # http://stackoverflow.com/a/2288225/261181
+    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364939%28v=vs.85%29.aspx
+    import string
+    from ctypes import windll
+    
+    all_drive_types = ['DRIVE_UNKNOWN', 
+                       'DRIVE_NO_ROOT_DIR',
+                       'DRIVE_REMOVABLE',
+                       'DRIVE_FIXED',
+                       'DRIVE_REMOTE',
+                       'DRIVE_CDROM',
+                       'DRIVE_RAMDISK']
+    
+    required_drive_types = ['DRIVE_REMOVABLE',
+                            'DRIVE_FIXED',
+                            'DRIVE_REMOTE',
+                            'DRIVE_RAMDISK']
+
+    drives = []
+    bitmask = windll.kernel32.GetLogicalDrives()  # @UndefinedVariable
+    for letter in string.ascii_uppercase:
+        drive_type = all_drive_types[windll.kernel32.GetDriveTypeW("%s:\\" % letter)]  # @UndefinedVariable
+        if bitmask & 1 and drive_type in required_drive_types:
+            drives.append(letter + ":\\")
+        bitmask >>= 1
+
+    return drives
+
+
+
+def shorten_repr(original_repr, max_len=1000):
+    if len(original_repr) > max_len:
+        return original_repr[:max_len] + " ... [{} chars truncated]".format(len(original_repr) - max_len)
+    else:
+        return original_repr
+        
+def __maybe_later_get_thonny_data_folder():
+    if running_on_windows():
+        # CSIDL_LOCAL_APPDATA 
+        # http://www.installmate.com/support/im9/using/symbols/functions/csidls.htm
+        return os.path.join(__maybe_later_get_windows_special_folder(28), "Thonny")
+    elif running_on_linux():
+        # https://specifications.freedesktop.org/basedir-spec/latest/ar01s02.html
+        # $XDG_DATA_HOME or $HOME/.local/share
+        data_home = os.environ.get("XDG_DATA_HOME", 
+                                   os.path.expanduser("~/.local/share"))
+        return os.path.join(data_home, "Thonny") 
+    elif running_on_mac_os():
+        return os.path.expanduser("~/Library/Thonny")
+    else:
+        return os.path.expanduser("~/.thonny")
+
+def __maybe_later_get_windows_special_folder(code):
+    # http://stackoverflow.com/a/3859336/261181
+    # http://www.installmate.com/support/im9/using/symbols/functions/csidls.htm
+    import ctypes.wintypes
+    SHGFP_TYPE_CURRENT= 0
+    buf= ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
+    ctypes.windll.shell32.SHGetFolderPathW(0, code, 0, SHGFP_TYPE_CURRENT, buf)
+    return buf.value
+
+def get_python_version_string(version_info=None):
+    if version_info == None:
+        version_info = sys.version_info
+         
+    result = ".".join(map(str, version_info[:3]))
+    if version_info[3] != "final":
+        result += "-" + version_info[3]
+    
+    result += " (" + ("64" if sys.maxsize > 2**32 else "32")+ " bit)\n"
+    
+    return result    
+
diff --git a/thonny/plugins/__init__.py b/thonny/plugins/__init__.py
new file mode 100644
index 0000000..434f652
--- /dev/null
+++ b/thonny/plugins/__init__.py
@@ -0,0 +1 @@
+# Package marker
\ No newline at end of file
diff --git a/thonny/plugins/about.py b/thonny/plugins/about.py
new file mode 100644
index 0000000..f0e409e
--- /dev/null
+++ b/thonny/plugins/about.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+
+import datetime
+import webbrowser
+import platform
+
+import tkinter as tk
+from tkinter import ttk
+import tkinter.font as font
+
+import thonny
+from thonny import misc_utils, ui_utils
+from thonny.misc_utils import get_python_version_string
+from thonny.globals import get_workbench
+
+class AboutDialog(tk.Toplevel):
+    def __init__(self, master):
+        tk.Toplevel.__init__(self, master)
+        
+
+        main_frame = ttk.Frame(self)
+        main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15)
+        main_frame.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(0, weight=1)
+
+        self.title("About Thonny")
+        if misc_utils.running_on_mac_os():
+            self.configure(background="systemSheetBackground")
+        self.resizable(height=tk.FALSE, width=tk.FALSE)
+        self.transient(master)
+        self.grab_set()
+        self.protocol("WM_DELETE_WINDOW", self._ok)
+        
+        
+        #bg_frame = ttk.Frame(self) # gives proper color in aqua
+        #bg_frame.grid()
+        
+        heading_font = font.nametofont("TkHeadingFont").copy()
+        heading_font.configure(size=19, weight="bold")
+        heading_label = ttk.Label(main_frame, 
+                                  text="Thonny " + thonny.get_version(),
+                                  font=heading_font)
+        heading_label.grid()
+        
+        
+        url = "http://thonny.org"
+        url_font = font.nametofont("TkDefaultFont").copy()
+        url_font.configure(underline=1)
+        url_label = ttk.Label(main_frame, text=url,
+                              cursor="hand2",
+                              foreground="blue",
+                              font=url_font,)
+        url_label.grid()
+        url_label.bind("<Button-1>", lambda _:webbrowser.open(url))
+        
+        if platform.system() == "Linux":
+            try:
+                import distro # distro don't need to be installed
+                system_desc = distro.name(True)
+            except ImportError:
+                system_desc = "Linux"
+                
+            if "32" not in system_desc and "64" not in system_desc:
+                system_desc += " " + self.get_os_word_size_guess()
+        else:
+            system_desc = (platform.system() 
+                        + " " + platform.release()  
+                        + " " + self.get_os_word_size_guess())
+        
+        platform_label = ttk.Label(main_frame, justify=tk.CENTER, 
+                                   text= system_desc + "\n"
+                                        + "Python " + get_python_version_string() 
+                                        + "Tk " + ui_utils.get_tk_version_str())
+        platform_label.grid(pady=20)
+        
+        credits_label = ttk.Label(main_frame, text="Made in\nUniversity of Tartu, Estonia\n"
+                                + "with the help from\nopen-source community",
+                              cursor="hand2",
+                              foreground="blue",
+                              font=url_font,
+                              justify=tk.CENTER)
+        credits_label.grid()
+        credits_label.bind("<Button-1>", lambda _:webbrowser.open("https://bitbucket.org/plas/thonny/src/master/CREDITS.rst"))
+        
+        license_font = font.nametofont("TkDefaultFont").copy()
+        license_font.configure(size=7)
+        license_label = ttk.Label(main_frame,
+                                  text="Copyright (©) "
+                                  + str(datetime.datetime.now().year)
+                                  + " Aivar Annamaa\n"
+                                  + "This program comes with\n"
+                                  + "ABSOLUTELY NO WARRANTY!\n"
+                                  + "It is free software, and you are welcome to\n"
+                                  + "redistribute it under certain conditions, see\n"
+                                  + "https://opensource.org/licenses/MIT\n"
+                                  + "for details",
+                                  justify=tk.CENTER, font=license_font)
+        license_label.grid(pady=20)
+        
+        
+        ok_button = ttk.Button(main_frame, text="OK", command=self._ok, default="active")
+        ok_button.grid(pady=(0,15))
+        ok_button.focus_set()
+        
+        self.bind('<Return>', self._ok, True) 
+        self.bind('<Escape>', self._ok, True)
+        
+        ui_utils.center_window(self, master)        
+        self.wait_window()
+        
+    def _ok(self, event=None):
+        self.destroy()
+    
+    def get_os_word_size_guess(self):
+        if "32" in platform.machine() and "64" not in platform.machine():
+            return "(32-bit)"
+        elif "64" in platform.machine() and "32" not in platform.machine():
+            return "(64-bit)"
+        else:
+            return ""
+
+
+def load_plugin():
+    def open_about(*args):
+        AboutDialog(get_workbench())
+        
+    get_workbench().add_command("changelog", "help", "Version history",
+                                lambda: webbrowser.open("https://bitbucket.org/plas/thonny/src/master/CHANGELOG.rst"), group=60)
+    get_workbench().add_command("about", "help", "About Thonny", open_about, group=61)
+    
+    # For Mac
+    get_workbench().createcommand("tkAboutDialog", open_about)
+
+
+    
\ No newline at end of file
diff --git a/thonny/plugins/ast_view.py b/thonny/plugins/ast_view.py
new file mode 100644
index 0000000..eeb99bc
--- /dev/null
+++ b/thonny/plugins/ast_view.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+ 
+import ast
+import traceback
+import tkinter as tk
+
+from thonny import ast_utils
+from thonny import ui_utils
+from thonny.common import TextRange
+from thonny.globals import get_workbench
+
+class AstView(ui_utils.TreeFrame):
+    def __init__(self, master):
+        ui_utils.TreeFrame.__init__(self, master,
+            columns=('range', 'lineno', 'col_offset', 'end_lineno', 'end_col_offset'),
+            displaycolumns=(0,)
+        )
+        
+        self._current_code_view = None
+        self.tree.bind("<<TreeviewSelect>>", self._locate_code)
+        self.tree.bind("<<Copy>>", self._copy_to_clipboard)
+        get_workbench().get_editor_notebook().bind("<<NotebookTabChanged>>", self._update)
+        get_workbench().bind("Save", self._update, True)
+        get_workbench().bind("SaveAs", self._update, True)
+        get_workbench().bind_class("Text", "<Double-Button-1>", self._update, True)
+
+        self.tree.column('#0', width=550, anchor=tk.W)
+        self.tree.column('range', width=100, anchor=tk.W)
+        self.tree.column('lineno', width=30, anchor=tk.W)
+        self.tree.column('col_offset', width=30, anchor=tk.W)
+        self.tree.column('end_lineno', width=30, anchor=tk.W)
+        self.tree.column('end_col_offset', width=30, anchor=tk.W)
+        
+        self.tree.heading('#0', text="Node", anchor=tk.W)
+        self.tree.heading('range', text='Code range', anchor=tk.W)
+        
+        self.tree['show'] = ('headings', 'tree')
+        self._current_source = None
+        
+        self._update(None)
+    
+    def _update(self, event):
+        editor = get_workbench().get_editor_notebook().get_current_editor()
+        
+        if not editor:
+            self._current_code_view = None
+            return
+        
+        self._current_code_view = editor.get_code_view()
+        self._current_source = self._current_code_view.get_content()
+        selection = self._current_code_view.get_selected_range()
+
+        self._clear_tree()
+        
+        try:
+            root = ast_utils.parse_source(self._current_source)
+            selected_ast_node = _find_closest_containing_node(root, selection)
+            
+        except Exception as e:
+            self.tree.insert("", "end", text=str(e), open=True)
+            traceback.print_exc()
+            return
+        
+        def _format(key, node, parent_id):
+            
+            
+            if isinstance(node, ast.AST):
+                fields = [(key, val) for key, val in ast.iter_fields(node)]
+                
+                value_label = node.__class__.__name__
+                    
+            elif isinstance(node, list):
+                fields = list(enumerate(node))
+                if len(node) == 0:
+                    value_label = "[]"
+                else:
+                    value_label = "[...]"
+            else:
+                fields = []
+                value_label = repr(node)
+            
+            item_text = str(key) + "=" + value_label
+            node_id = self.tree.insert(parent_id, "end", text=item_text, open=True)
+            if node == selected_ast_node:
+                self.tree.see(node_id)
+                self.tree.selection_add(node_id)
+            
+            if hasattr(node, "lineno") and hasattr(node, "col_offset"):
+                self.tree.set(node_id, "lineno", node.lineno)
+                self.tree.set(node_id, "col_offset", node.col_offset)
+                
+                range_str = str(node.lineno) + '.' + str(node.col_offset)
+                if hasattr(node, "end_lineno") and hasattr(node, "end_col_offset"):
+                    self.tree.set(node_id, "end_lineno", node.end_lineno)
+                    self.tree.set(node_id, "end_col_offset", node.end_col_offset)
+                    range_str += "  -  " + str(node.end_lineno) + '.' + str(node.end_col_offset)
+                else:
+                    # fallback
+                    self.tree.set(node_id, "end_lineno", node.lineno)
+                    self.tree.set(node_id, "end_col_offset", node.col_offset + 1)
+                    
+                self.tree.set(node_id, "range", range_str)
+                
+            for field_key, field_value in fields:
+                _format(field_key, field_value, node_id)
+                
+                
+        _format("root", root, "")
+    
+    def _locate_code(self, event):
+        if self._current_code_view is None:
+            return
+        
+        iid = self.tree.focus()
+        
+        if iid != '':
+            values = self.tree.item(iid)['values']
+            if isinstance(values, list) and len(values) >= 5:
+                start_line, start_col, end_line, end_col = values[1:5] 
+                self._current_code_view.select_range(TextRange(start_line, start_col, 
+                                                    end_line, end_col))
+    
+    def _clear_tree(self):
+        for child_id in self.tree.get_children():
+            self.tree.delete(child_id)
+    
+    def _copy_to_clipboard(self, event):
+        self.clipboard_clear()
+        if self._current_source is not None:
+            pretty_ast = ast_utils.pretty(ast_utils.parse_source(self._current_source))
+            self.clipboard_append(pretty_ast)
+
+def _find_closest_containing_node(tree, text_range):
+    # first look among children
+    for child in ast.iter_child_nodes(tree):
+        result = _find_closest_containing_node(child, text_range)
+        if result is not None:
+            return result
+
+    # no suitable child was found
+    if (hasattr(tree, "lineno")
+        and TextRange(tree.lineno, tree.col_offset, tree.end_lineno, tree.end_col_offset)
+            .contains_smaller_eq(text_range)):
+        return tree
+    # nope
+    else:
+        return None
+
+            
+def load_plugin(): 
+    get_workbench().add_view(AstView, "AST", "s")
+        
+    
+        
\ No newline at end of file
diff --git a/thonny/plugins/autocomplete.py b/thonny/plugins/autocomplete.py
new file mode 100644
index 0000000..fbfd436
--- /dev/null
+++ b/thonny/plugins/autocomplete.py
@@ -0,0 +1,312 @@
+import tkinter as tk
+from thonny.globals import get_workbench, get_runner
+from thonny.codeview import CodeViewText
+from thonny.shell import ShellText
+from thonny.common import InlineCommand
+from tkinter import messagebox
+
+
+
+# TODO: adjust the window position in cases where it's too close to bottom or right edge - but make sure the current line is shown
+"""Completions get computed on the backend, therefore getting the completions is
+asynchronous.
+"""
+class Completer(tk.Listbox):
+    def __init__(self, text):
+        self.font = get_workbench().get_font("EditorFont").copy()
+        tk.Listbox.__init__(self, master=text,
+                            font=self.font,
+                            activestyle="dotbox",
+                            exportselection=False)
+        
+        self.text = text
+        self.completions = []
+        
+        self.doc_label = tk.Label(master=text, text="Aaappiiiii", bg="#ffffe0",
+                                  justify="left", anchor="nw")
+        
+        # Auto indenter will eat up returns, therefore I need to raise the priority
+        # of this binding
+        self.text_priority_bindtag = "completable" + str(self.text.winfo_id())
+        self.text.bindtags((self.text_priority_bindtag,) + self.text.bindtags())
+        self.text.bind_class(self.text_priority_bindtag, "<Key>", self._on_text_keypress, True)
+        
+        self.text.bind("<<TextChange>>", self._on_text_change, True) # Assuming TweakableText
+        
+        # for cases when Listbox gets focus
+        self.bind("<Escape>", self._close)
+        self.bind("<Return>", self._insert_current_selection)
+        self.bind("<Double-Button-1>", self._insert_current_selection)
+        self._bind_result_event()
+    
+    def _bind_result_event(self):    
+        # TODO: remove binding when editor gets closed
+        get_workbench().bind("EditorCompletions", self._handle_backend_response, True)
+    
+    def handle_autocomplete_request(self):
+        row, column = self._get_position()
+        source = self.text.get("1.0", "end-1c")
+        get_runner().send_command(InlineCommand(command="editor_autocomplete",
+                                                source=source,
+                                                row=row,
+                                                column=column,
+                                                filename=self._get_filename()))
+    
+    def _handle_backend_response(self, msg):
+        row, column = self._get_position()
+        source = self.text.get("1.0", "end-1c")
+        
+        if msg.source != source or msg.row != row or msg.column != column:
+            # situation has changed, information is obsolete
+            self._close()
+        elif msg.error:
+            self._close()
+            messagebox.showerror("Autocomplete error", msg.error)
+        else:
+            self._present_completions(msg.completions)
+            
+            
+    def _present_completions(self, completions):
+        self.completions = completions
+        
+        # broadcast logging info
+        row, column = self._get_position()
+        get_workbench().event_generate("AutocompleteProposal",
+            text_widget=self.text,
+            row=row,
+            column=column,
+            proposal_count=len(completions))
+        
+        # present
+        if len(completions) == 0:
+            self._close()
+        elif len(completions) == 1:
+            self._insert_completion(completions[0]) #insert the only completion
+            self._close()
+        else:
+            self._show_box(completions)
+        
+            
+    def _show_box(self, completions):
+        self.delete(0, self.size())
+        self.insert(0, *[c["name"] for c in completions])
+        self.activate(0)
+        self.selection_set(0)
+        
+        # place box
+        if not self._is_visible():
+            
+            self.font.configure(size=get_workbench().get_font("EditorFont")["size"]-2)
+            
+            
+            #_, _, _, list_box_height = self.bbox(0)
+            height = 100 #min(150, list_box_height * len(completions) * 1.15)
+            typed_name_length = len(completions[0]["name"]) - len(completions[0]["complete"])
+            text_box_x, text_box_y, _, text_box_height = self.text.bbox('insert-%dc' % typed_name_length);
+            
+            # should the box appear below or above cursor?
+            space_below = self.master.winfo_height() - text_box_y - text_box_height
+            space_above = text_box_y
+            
+            if space_below >= height or space_below > space_above:
+                height = min(height, space_below)
+                y = text_box_y + text_box_height
+            else:
+                height = min(height, space_above)
+                y = text_box_y - height
+            
+            width = 400    
+            self.place(x=text_box_x, y=y, width=width, height=height)
+            
+            self._update_doc()
+            
+            
+    def _update_doc(self):
+        c = self._get_selected_completion() 
+        
+        if c is None:
+            self.doc_label["text"] = ""
+            self.doc_label.place_forget()
+        else:
+            docstring = c.get("docstring", None)
+            if docstring:
+                self.doc_label["text"] = docstring
+                self.doc_label.place(x=self.winfo_x() + self.winfo_width(),
+                                     y=self.winfo_y(),
+                                     width=400,
+                                     height=self.winfo_height())
+            else:
+                self.doc_label["text"] = ""
+                self.doc_label.place_forget()
+
+    def _is_visible(self):
+        return self.winfo_ismapped()
+    
+    def _insert_completion(self, completion):
+        typed_len = len(completion["name"]) - len(completion["complete"])
+        typed_prefix = self.text.get("insert-{}c".format(typed_len), "insert")
+        get_workbench().event_generate("AutocompleteInsertion",
+            text_widget=self.text,
+            typed_prefix=typed_prefix,
+            completed_name=completion["name"])
+        
+        if self._is_visible():
+            self._close()
+        
+        if not completion["name"].startswith(typed_prefix):
+            # eg. case of the prefix was not correct
+            self.text.delete("insert-{}c".format(typed_len), "insert")
+            self.text.insert('insert', completion["name"])
+        else:
+            self.text.insert('insert', completion["complete"])
+        
+    
+    def _get_filename(self):
+        # TODO: allow completing in shell
+        if not isinstance(self.text, CodeViewText):
+            return None
+        
+        codeview = self.text.master
+        
+        editor = get_workbench().get_editor_notebook().get_current_editor()
+        if editor.get_code_view() is codeview:
+            return editor.get_filename()
+        else:
+            return None
+    
+    def _move_selection(self, delta):
+        selected = self.curselection()
+        if len(selected) == 0:
+            index = 0
+        else:
+            index = selected[0]
+        
+        index += delta
+        index = max(0, min(self.size()-1, index))
+        
+        self.selection_clear(0, self.size()-1)
+        self.selection_set(index)
+        self.activate(index)
+        self.see(index)
+        self._update_doc()
+    
+    def _get_request_id(self):
+        return "autocomplete_" + str(self.text.winfo_id())
+    
+    def _get_position(self):
+        return map(int, self.text.index("insert").split("."))
+    
+    def _on_text_keypress(self, event=None):
+        if not self._is_visible():
+            return
+        
+        if event.keysym == "Escape":
+            self._close()
+            return "break"
+        elif event.keysym in ["Up", "KP_Up"]:
+            self._move_selection(-1)
+            return "break"
+        elif event.keysym in ["Down", "KP_Down"]:
+            self._move_selection(1)
+            return "break"
+        elif event.keysym in ["Return", "KP_Enter"]:
+            assert self.size() > 0
+            self._insert_current_selection()
+            return "break"
+    
+    def _insert_current_selection(self, event=None):
+        self._insert_completion(self._get_selected_completion())
+    
+    def _get_selected_completion(self):
+        sel = self.curselection()
+        if len(sel) != 1:
+            return None
+        
+        return self.completions[sel[0]]
+    
+    def _on_text_change(self, event=None):
+        if self._is_visible():
+            self.handle_autocomplete_request()
+
+    
+    def _close(self, event=None):
+        self.place_forget()
+        self.doc_label.place_forget()
+        self.text.focus_set()
+    
+    def on_text_click(self, event=None):
+        if self._is_visible():
+            self._close()
+
+class ShellCompleter(Completer):
+    def _bind_result_event(self):    
+        # TODO: remove binding when editor gets closed
+        get_workbench().bind("ShellCompletions", self._handle_backend_response, True)
+        
+    def handle_autocomplete_request(self):
+        source=self._get_prefix()
+        
+        get_runner().send_command(InlineCommand(command="shell_autocomplete",
+                                                source=source))
+    
+    def _handle_backend_response(self, msg):
+        # check if the response is relevant for current state
+        if msg.source != self._get_prefix():
+            self._close()
+        else:
+            self._present_completions(msg.completions)
+
+
+    def _get_prefix(self):
+        return self.text.get("insert linestart", "insert") # TODO: allow multiple line input
+
+        
+def handle_autocomplete_request(event=None):
+    if event is None:
+        text = get_workbench().focus_get()
+    else:
+        text = event.widget
+    
+    _handle_autocomplete_request_for_text(text)
+
+def _handle_autocomplete_request_for_text(text):
+    if not hasattr(text, "autocompleter"):
+        if isinstance(text, (CodeViewText, ShellText)):
+            if isinstance(text, CodeViewText):
+                text.autocompleter = Completer(text)
+            elif isinstance(text, ShellText):
+                text.autocompleter = ShellCompleter(text)
+            text.bind("<1>", text.autocompleter.on_text_click)
+        else:
+            return
+
+    text.autocompleter.handle_autocomplete_request()
+
+
+def patched_perform_midline_tab(text, event):
+    if isinstance(text, ShellText):
+        option_name = "edit.tab_complete_in_shell"
+    else:
+        option_name = "edit.tab_complete_in_editor"
+        
+    if get_workbench().get_option(option_name):
+        if not text.has_selection():
+            _handle_autocomplete_request_for_text(text)
+            return "break"
+    else:
+        return text.perform_smart_tab(event)
+    
+
+def load_plugin():
+    
+    get_workbench().add_command("autocomplete", "edit", "Auto-complete",
+        handle_autocomplete_request,
+        default_sequence="<Control-space>"
+        # TODO: tester
+        )
+    
+    get_workbench().set_default("edit.tab_complete_in_editor", True)
+    get_workbench().set_default("edit.tab_complete_in_shell", True)
+    
+    CodeViewText.perform_midline_tab = patched_perform_midline_tab
+    ShellText.perform_midline_tab = patched_perform_midline_tab
diff --git a/thonny/plugins/coloring.py b/thonny/plugins/coloring.py
new file mode 100644
index 0000000..82edede
--- /dev/null
+++ b/thonny/plugins/coloring.py
@@ -0,0 +1,256 @@
+"""
+Each text will get its on SyntaxColorer.
+
+For performance reasons, coloring is updated in 2 phases:
+    1. recolor single-line tokens on the modified line(s)
+    2. recolor multi-line tokens (triple-quoted strings) in the whole text
+
+First phase may insert wrong tokens inside triple-quoted strings, but the 
+priorities of triple-quoted-string tags are higher and therefore user 
+doesn't see these wrong taggings.
+
+In Shell only current command entry is colored
+    
+Regexes are adapted from idlelib
+"""
+
+import re
+
+from thonny.globals import get_workbench
+from thonny.shell import ShellText
+from thonny.codeview import CodeViewText
+
+
+class SyntaxColorer:
+    def __init__(self, text, main_font, bold_font):
+        self.text = text
+        self._compile_regexes()
+        self._config_colors(main_font, bold_font)
+        self._update_scheduled = False
+        self._dirty_ranges = set()
+        self._use_coloring = True
+    
+    def _compile_regexes(self):
+        from thonny.token_utils import BUILTIN, COMMENT, MAGIC_COMMAND, STRING3,\
+            STRING3_DELIMITER, STRING_OPEN, KW, STRING_CLOSED
+            
+        
+        self.uniline_regex = re.compile(
+            KW 
+            + "|" + BUILTIN 
+            + "|" + COMMENT 
+            + "|" + MAGIC_COMMAND 
+            + "|" + STRING3_DELIMITER # to avoid marking """ and ''' as single line string in uniline mode
+            + "|" + STRING_CLOSED 
+            + "|" + STRING_OPEN
+            , re.S)
+        
+        self.multiline_regex = re.compile(
+            STRING3
+            + "|" + COMMENT 
+            + "|" + MAGIC_COMMAND 
+            #+ "|" + STRING_CLOSED # need to include single line strings otherwise '"""' ... '""""' will give wrong result
+            + "|" + STRING_OPEN # (seems that it works faster and also correctly with only open strings)
+            , re.S)
+        
+        self.id_regex = re.compile(r"\s+(\w+)", re.S)
+
+    def _config_colors(self, main_font, bold_font):
+        string_foreground = "DarkGreen"
+        open_string_background = "#c3f9d3"
+        self.uniline_tagdefs = {
+            "COMMENT"       : {"font":main_font, 'background':None, 'foreground':"DarkGray", },
+            "MAGIC_COMMAND" : {"font":main_font, 'background':None, 'foreground':"DarkGray", },
+            "STRING_CLOSED" : {"font":main_font, 'background':None, 'foreground':string_foreground, },
+            "STRING_OPEN"   : {"font":main_font, 'background': open_string_background, "foreground": string_foreground},
+            "KEYWORD"       : {"font":bold_font, 'background':None, 'foreground':"#7f0055", },
+            "BUILTIN"       : {"font":main_font, 'background':None, 'foreground':None},
+            #"DEFINITION"    : {},
+            }
+        
+        self.multiline_tagdefs = {
+            "STRING_CLOSED3": self.uniline_tagdefs["STRING_CLOSED"],
+            "STRING_OPEN3"  : self.uniline_tagdefs["STRING_OPEN"],
+            }
+        
+        for tagdefs in [self.multiline_tagdefs, self.uniline_tagdefs]:
+            for tag, cnf in tagdefs.items():
+                if cnf:
+                    self.text.tag_configure(tag, **cnf)
+        
+        self.text.tag_raise('sel')
+        self.text.tag_raise('STRING_CLOSED3')
+        self.text.tag_raise('STRING_OPEN3')
+
+    def schedule_update(self, event, use_coloring=True):
+        self._use_coloring = use_coloring
+        
+        # Allow reducing work by remembering only changed lines
+        if hasattr(event, "sequence"):
+            if event.sequence == "TextInsert":
+                index = self.text.index(event.index)
+                start_row = int(index.split(".")[0])
+                end_row = start_row + event.text.count("\n")
+                start_index = "%d.%d" % (start_row, 0)
+                end_index = "%d.%d" % (end_row + 1, 0)
+            elif event.sequence == "TextDelete":
+                index = self.text.index(event.index1)
+                start_row = int(index.split(".")[0])
+                start_index = "%d.%d" % (start_row, 0)
+                end_index = "%d.%d" % (start_row + 1, 0)
+        else:
+            start_index = "1.0"
+            end_index = "end"
+        
+        self._dirty_ranges.add((start_index, end_index))
+        
+        def perform_update():
+            try:
+                self._update_coloring()
+            finally:
+                self._update_scheduled = False
+                self._dirty_ranges = set()
+        
+        if not self._update_scheduled:
+            self._update_scheduled = True
+            self.text.after_idle(perform_update)
+            
+    def _update_coloring(self):
+        self._update_uniline_tokens("1.0", "end")
+        self._update_multiline_tokens("1.0", "end")
+
+    def _update_uniline_tokens(self, start, end):
+        chars = self.text.get(start, end)
+                
+        # clear old tags
+        for tag in self.uniline_tagdefs:
+            self.text.tag_remove(tag, start, end)
+        
+        if not self._use_coloring:
+            return
+        
+        for match in self.uniline_regex.finditer(chars):
+            for token_type, token_text in match.groupdict().items():
+                if token_text and token_type in self.uniline_tagdefs:
+                    token_text = token_text.strip()
+                    match_start, match_end = match.span(token_type)
+                    
+                    self.text.tag_add(token_type,
+                             start + "+%dc" % match_start,
+                             start + "+%dc" % match_end)
+                    
+                    # Mark also the word following def or class
+                    if token_text in ("def", "class"):
+                        id_match = self.id_regex.match(chars, match_end)
+                        if id_match:
+                            id_match_start, id_match_end = id_match.span(1)
+                            self.text.tag_add("DEFINITION",
+                                         start + "+%dc" % id_match_start,
+                                         start + "+%dc" % id_match_end)
+                
+        
+         
+    def _update_multiline_tokens(self, start, end):
+        chars = self.text.get(start, end)
+        # clear old tags
+        for tag in self.multiline_tagdefs:
+            self.text.tag_remove(tag, start, end)
+        
+        if not self._use_coloring:
+            return
+        
+        # Count number of open multiline strings to be able to detect when string gets closed
+        self.text.number_of_open_multiline_strings = 0
+        
+        interesting_token_types = list(self.multiline_tagdefs.keys()) + ["STRING3"]
+        for match in self.multiline_regex.finditer(chars):
+            for token_type, token_text in match.groupdict().items():
+                if token_text and token_type in interesting_token_types:
+                    token_text = token_text.strip()
+                    match_start, match_end = match.span(token_type)
+                    if token_type == "STRING3":
+                        if (token_text.startswith('"""') and not token_text.endswith('"""')
+                            or token_text.startswith("'''") and not token_text.endswith("'''")
+                            or len(token_text) == 3):
+                            str_end = int(float(self.text.index(start + "+%dc" % match_end)))
+                            file_end = int(float(self.text.index("end")))
+
+                            if str_end == file_end:
+                                token_type = "STRING_OPEN3"
+                                self.text.number_of_open_multiline_strings += 1
+                            else:
+                                token_type = None
+                        elif len(token_text) >= 4 and token_text[-4] == "\\":
+                            token_type = "STRING_OPEN3"
+                            self.text.number_of_open_multiline_strings += 1
+                        else:
+                            token_type = "STRING_CLOSED3"
+                    
+                    token_start = start + "+%dc" % match_start
+                    token_end = start + "+%dc" % match_end
+                    # clear uniline tags
+                    for tag in self.uniline_tagdefs:
+                        self.text.tag_remove(tag, token_start, token_end)
+                    # add tag
+                    self.text.tag_add(token_type,
+                             token_start,
+                             token_end)
+        
+
+class CodeViewSyntaxColorer(SyntaxColorer):
+    def _update_coloring(self):
+        for dirty_range in self._dirty_ranges:
+            self._update_uniline_tokens(*dirty_range)
+        
+        # Multiline tokens need to be searched from the whole source
+        open_before = getattr(self.text, "number_of_open_multiline_strings", 0)
+        self._update_multiline_tokens("1.0", "end")
+        open_after = getattr(self.text, "number_of_open_multiline_strings", 0)
+        
+        if open_after == 0 and open_before != 0:
+            # recolor uniline tokens after closing last open multiline string
+            self._update_uniline_tokens("1.0", "end")
+
+class ShellSyntaxColorer(SyntaxColorer):
+    def _update_coloring(self):
+        parts = self.text.tag_prevrange("command", "end")
+        
+        if parts:
+            end_row, end_col = map(int, self.text.index(parts[1]).split("."))
+            
+            if end_col != 0: # if not just after the last linebreak
+                end_row += 1 # then extend the range to the beginning of next line
+                end_col = 0  # (otherwise open strings are not displayed correctly)
+            
+            start_index = parts[0]
+            end_index = "%d.%d" % (end_row, end_col)
+            
+            self._update_uniline_tokens(start_index, end_index)
+            self._update_multiline_tokens(start_index, end_index)
+
+def update_coloring(event):
+    if hasattr(event, "text_widget"):
+        text = event.text_widget
+    else:
+        text = event.widget
+    
+    if not hasattr(text, "syntax_colorer"):
+        if isinstance(text, ShellText):
+            class_ = ShellSyntaxColorer
+        elif isinstance(text, CodeViewText):
+            class_ = CodeViewSyntaxColorer
+        else:
+            return
+        
+        text.syntax_colorer = class_(text, get_workbench().get_font("EditorFont"),
+                            get_workbench().get_font("BoldEditorFont"))
+    
+    text.syntax_colorer.schedule_update(event, get_workbench().get_option("view.syntax_coloring"))
+
+def load_plugin():
+    wb = get_workbench() 
+
+    wb.set_default("view.syntax_coloring", True)
+    wb.bind("TextInsert", update_coloring, True)
+    wb.bind("TextDelete", update_coloring, True)
+    wb.bind("<<UpdateAppearance>>", update_coloring, True)
diff --git a/thonny/plugins/commenting.py b/thonny/plugins/commenting.py
new file mode 100644
index 0000000..d35b677
--- /dev/null
+++ b/thonny/plugins/commenting.py
@@ -0,0 +1,119 @@
+import tkinter as tk
+from thonny.globals import get_workbench
+from thonny.ui_utils import select_sequence
+from thonny.common import TextRange
+
+BLOCK_COMMENT_PREFIX = "##"
+
+def _get_focused_writable_text():
+    widget = get_workbench().focus_get()
+    # In Ubuntu when moving from one menu to another, this may give None when text is actually focused
+    if (isinstance(widget, tk.Text) 
+        and (not hasattr(widget, "is_read_only") or not widget.is_read_only())):
+        return widget 
+    else:
+        return None
+
+def _writable_text_is_focused():
+    return _get_focused_writable_text() is not None 
+
+
+def _selection_is_line_commented(text):
+    sel_range = _get_focused_code_range(text)
+    
+    for lineno in range(sel_range.lineno, sel_range.end_lineno+1):
+        line = text.get(str(lineno) + '.0', str(lineno) + '.end')
+        if not line.startswith(BLOCK_COMMENT_PREFIX):
+            return False
+    
+    return True
+    
+def _select_lines(text, first_line, last_line):
+    text.tag_remove("sel", "1.0", tk.END)
+    text.tag_add("sel", 
+                      str(first_line) + ".0",
+                      str(last_line) + ".end")
+
+def _toggle_selection_comment(text):
+    if _selection_is_line_commented(text):
+        _uncomment_selection(text)
+    else:
+        _comment_selection(text)
+    
+
+def _comment_selection(text):
+    """Adds ## in front of all selected lines if any lines are selected, 
+    or just the current line otherwise"""
+    
+    sel_range = _get_focused_code_range(text)
+
+    for lineno in range(sel_range.lineno, sel_range.end_lineno+1):
+        text.insert(str(lineno) + '.0', BLOCK_COMMENT_PREFIX)
+    
+    if sel_range.end_lineno > sel_range.lineno:
+        _select_lines(text, sel_range.lineno, sel_range.end_lineno)
+    
+    text.edit_separator()
+        
+
+def _uncomment_selection(text):
+    sel_range = _get_focused_code_range(text)
+    
+    for lineno in range(sel_range.lineno, sel_range.end_lineno+1):
+        line = text.get(str(lineno) + '.0', str(lineno) + '.end')
+        if line.startswith(BLOCK_COMMENT_PREFIX):
+            text.delete(str(lineno) + ".0",
+                             str(lineno) + "." + str(len(BLOCK_COMMENT_PREFIX)))
+
+def _get_focused_code_range(text):
+    if len(text.tag_ranges("sel")) > 0:
+        lineno, col_offset = map(int, text.index(tk.SEL_FIRST).split("."))
+        end_lineno, end_col_offset = map(int, text.index(tk.SEL_LAST).split("."))
+        
+        if end_lineno > lineno and end_col_offset == 0:
+            # SelectAll includes nonexisting extra line
+            end_lineno -= 1
+            end_col_offset = int(text.index(str(end_lineno) + ".end").split(".")[1])
+    else:
+        lineno, col_offset = map(int, text.index(tk.INSERT).split("."))
+        end_lineno, end_col_offset = lineno, col_offset
+        
+    return TextRange(lineno, col_offset, end_lineno, end_col_offset)
+
+
+def _cmd_toggle_selection_comment():
+    text = _get_focused_writable_text()
+    if text is not None: 
+        _toggle_selection_comment(text)
+        
+def _cmd_comment_selection():
+    text = _get_focused_writable_text()
+    if text is not None: 
+        _comment_selection(text)
+
+def _cmd_uncomment_selection():
+    text = _get_focused_writable_text()
+    if text is not None: 
+        _uncomment_selection(text)
+
+
+
+def load_plugin():
+    
+    get_workbench().add_command("toggle_comment", "edit", "Toggle comment",
+        _cmd_toggle_selection_comment,
+        default_sequence=select_sequence("<Control-Key-3>", "<Command-Key-3>"),
+        tester=_writable_text_is_focused,
+        group=50)
+    
+    get_workbench().add_command("comment_selection", "edit", "Comment out",
+        _cmd_comment_selection,
+        default_sequence="<Alt-Key-3>",
+        tester=_writable_text_is_focused,
+        group=50)
+    
+    get_workbench().add_command("uncomment_selection", "edit", "Uncomment",
+        _cmd_uncomment_selection,
+        default_sequence="<Alt-Key-4>",
+        tester=_writable_text_is_focused,
+        group=50)
diff --git a/thonny/plugins/common_editing_commands.py b/thonny/plugins/common_editing_commands.py
new file mode 100644
index 0000000..96229ca
--- /dev/null
+++ b/thonny/plugins/common_editing_commands.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+import tkinter as tk
+from tkinter import ttk
+from thonny.globals import get_workbench
+from thonny.ui_utils import select_sequence
+
+def load_plugin():
+    def create_edit_command_handler(virtual_event_sequence):
+        def handler(event=None):
+            widget = get_workbench().focus_get()
+            if widget:
+                return widget.event_generate(virtual_event_sequence)
+        
+        return handler
+    
+    def select_all(event=None):
+        # Tk 8.6 has <<SelectAll>> virtual event, but 8.5 doesn't
+        widget = get_workbench().focus_get()
+        if isinstance(widget, tk.Text):
+            widget.tag_remove("sel","1.0","end")
+            widget.tag_add("sel","1.0","end")
+        elif isinstance(widget, ttk.Entry) or isinstance(widget, tk.Entry):
+            widget.select_range(0, tk.END)
+        
+    
+    get_workbench().add_command("undo", "edit", "Undo",
+        create_edit_command_handler("<<Undo>>"),
+        tester=None, # TODO:
+        default_sequence=select_sequence("<Control-z>", "<Command-z>"),
+        skip_sequence_binding=True,
+        group=10)
+    
+    get_workbench().add_command("redo", "edit", "Redo",
+        create_edit_command_handler("<<Redo>>"),
+        tester=None, # TODO:
+        default_sequence=select_sequence("<Control-y>", "<Command-y>"),
+        skip_sequence_binding=True,
+        group=10)
+    
+    # Ctrl+Shift+Z as alternative shortcut for redo
+    get_workbench().bind_class("Text", select_sequence("<Control-Shift-Z>", "<Command-Shift-Z>"),
+                               create_edit_command_handler("<<Redo>>"), True)
+    
+    
+    get_workbench().add_command("Cut", "edit", "Cut",
+        create_edit_command_handler("<<Cut>>"),
+        tester=None, # TODO:
+        default_sequence=select_sequence("<Control-x>", "<Command-x>"),
+        skip_sequence_binding=True,
+        group=20)
+    
+    get_workbench().add_command("Copy", "edit", "Copy",
+        create_edit_command_handler("<<Copy>>"),
+        tester=None, # TODO:
+        default_sequence=select_sequence("<Control-c>", "<Command-c>"),
+        skip_sequence_binding=True,
+        group=20)
+    
+    get_workbench().add_command("Paste", "edit", "Paste",
+        create_edit_command_handler("<<Paste>>"),
+        tester=None, # TODO:
+        default_sequence=select_sequence("<Control-v>", "<Command-v>"),
+        skip_sequence_binding=True,
+        group=20)
+    
+    get_workbench().add_command("SelectAll", "edit", "Select all",
+        select_all,
+        tester=None, # TODO:
+        default_sequence=select_sequence("<Control-a>", "<Command-a>"),
+        skip_sequence_binding=True,
+        group=20)
+    
diff --git a/thonny/plugins/debugger.py b/thonny/plugins/debugger.py
new file mode 100644
index 0000000..293015c
--- /dev/null
+++ b/thonny/plugins/debugger.py
@@ -0,0 +1,684 @@
+# -*- coding: utf-8 -*-
+
+"""
+Adds debugging commands and features. 
+"""
+
+import tkinter as tk
+from tkinter import ttk
+from thonny.common import DebuggerCommand
+from thonny.memory import VariablesFrame
+from thonny import ast_utils, memory, misc_utils, ui_utils
+from thonny.misc_utils import shorten_repr
+import ast
+from thonny.codeview import CodeView, READ_ONLY_BACKGROUND
+from tkinter.messagebox import showinfo, showerror
+from thonny.globals import get_workbench, get_runner
+from thonny.ui_utils import select_sequence
+import tokenize
+import logging
+
+_SUSPENDED_FOCUS_BACKGROUND = "#DCEDF2"
+_ACTIVE_FOCUS_BACKGROUND = "#F8FC9A"
+
+class Debugger:
+    def __init__(self):
+        
+        self._init_commands()
+        
+        self._main_frame_visualizer = None
+        self._last_progress_message = None
+        
+        get_workbench().bind("DebuggerProgress", self._handle_debugger_progress, True)
+        get_workbench().bind("ToplevelResult", self._handle_toplevel_result, True)
+        
+        get_workbench().get_view("ShellView").add_command("Debug", 
+            get_runner().handle_execute_from_shell)
+        
+    
+    def _init_commands(self):
+        get_workbench().add_command("debug", "run", "Debug current script",
+            self._cmd_debug_current_script,
+            tester=self._cmd_debug_current_script_enabled,
+            default_sequence="<Control-F5>",
+            group=10,
+            image_filename="run.debug_current_script.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command("step_over", "run", "Step over",
+            self._cmd_step_over,
+            tester=self._cmd_stepping_commands_enabled,
+            default_sequence="<F6>",
+            group=30,
+            image_filename="run.step_over.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command("step_into", "run", "Step into",
+            self._cmd_step_into,
+            tester=self._cmd_stepping_commands_enabled,
+            default_sequence="<F7>",
+            group=30,
+            image_filename="run.step_into.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command("step_out", "run", "Step out",
+            self._cmd_step_out,
+            tester=self._cmd_stepping_commands_enabled,
+            default_sequence="<F8>",
+            group=30,
+            image_filename="run.step_out.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command("run_to_cursor", "run", "Run to cursor",
+            self._cmd_run_to_cursor,
+            tester=self._cmd_run_to_cursor_enabled,
+            default_sequence=select_sequence("<Control-F8>", "<Control-F8>"),
+            group=30,
+            image_filename="run.run_to_cursor.gif",
+            include_in_toolbar=False)
+
+    
+    def _cmd_debug_current_script(self):
+        get_runner().execute_current("Debug")
+
+    def _cmd_debug_current_script_enabled(self):
+        return (get_workbench().get_editor_notebook().get_current_editor() is not None
+                and get_runner().get_state() == "waiting_toplevel_command"
+                and "debug" in get_runner().supported_features())
+
+    def _check_issue_debugger_command(self, command, **kwargs):
+        cmd = DebuggerCommand(command=command, **kwargs)
+        self._last_debugger_command = cmd
+        
+        state = get_runner().get_state() 
+        if (state == "waiting_debugger_command"
+            or getattr(cmd, "automatic", False) and state == "running"):
+            logging.debug("_check_issue_debugger_command: %s", cmd)
+            
+            # tell VM the state we are seeing
+            cmd.setdefault (
+                frame_id=self._last_progress_message.stack[-1].id,
+                state=self._last_progress_message.stack[-1].last_event,
+                focus=self._last_progress_message.stack[-1].last_event_focus
+            )
+            
+            get_runner().send_command(cmd)
+        else:
+            logging.debug("Bad state for sending debugger command " + str(command))
+
+
+    def _cmd_stepping_commands_enabled(self):
+        return get_runner().get_state() == "waiting_debugger_command"
+    
+    def _cmd_step_into(self):
+        self._check_issue_debugger_command("step")
+    
+    def _cmd_step_over(self):
+        # Step over should stop when new statement or expression is selected.
+        # At the same time, I need to get value from after_expression event.
+        # Therefore I ask backend to stop first after the focus
+        # and later I ask it to run to the beginning of new statement/expression.
+        
+        self._check_issue_debugger_command("exec")
+        
+    def _cmd_step_out(self):
+        self._check_issue_debugger_command("out")
+
+    def _cmd_run_to_cursor(self):
+        visualizer = self._get_topmost_selected_visualizer()
+        if visualizer:
+            assert isinstance(visualizer._text_frame, CodeView)
+            code_view = visualizer._text_frame
+            selection = code_view.get_selected_range()
+            
+            target_lineno = visualizer._firstlineno-1 + selection.lineno
+            self._check_issue_debugger_command("line",
+                                               target_filename=visualizer._filename, 
+                                               target_lineno=target_lineno,
+                                               )
+
+    def _cmd_run_to_cursor_enabled(self):
+        return (self._cmd_stepping_commands_enabled()
+                and self._get_topmost_selected_visualizer() is not None
+                )
+
+    def _get_topmost_selected_visualizer(self):
+        
+        visualizer = self._main_frame_visualizer
+        if visualizer is None:
+            return None
+        
+        while visualizer._next_frame_visualizer is not None:
+            visualizer = visualizer._next_frame_visualizer
+        
+        topmost_text_widget = visualizer._text
+        focused_widget = get_workbench().focus_get()
+        
+        if focused_widget is None:
+            return None
+        elif focused_widget == topmost_text_widget:
+            return visualizer
+        else:
+            return None
+        
+
+    def _handle_debugger_progress(self, msg):
+        self._last_progress_message = msg
+        
+        if self._should_skip_event(msg):
+            self._check_issue_debugger_command("run_to_before", automatic=True)
+        else:
+            main_frame_id = msg.stack[0].id
+            
+            # clear obsolete main frame visualizer
+            if (self._main_frame_visualizer 
+                and self._main_frame_visualizer.get_frame_id() != main_frame_id):
+                self._main_frame_visualizer.close()
+                self._main_frame_visualizer = None
+                
+            if not self._main_frame_visualizer:
+                self._main_frame_visualizer = MainFrameVisualizer(msg.stack[0])
+                
+            self._main_frame_visualizer.update_this_and_next_frames(msg)
+        
+        # advance automatically in some cases
+        event = msg.stack[-1].last_event
+        args = msg.stack[-1].last_event_args
+
+        if msg.exception:
+            showerror("Exception",
+                      # Following is clever but noisy 
+                      msg.exception_lower_stack_description.lstrip() + 
+                      msg.exception["type_name"] 
+                      + ": " + msg.exception_msg)
+            self._check_issue_debugger_command("step", automatic=True)
+            
+        elif (event == "after_expression" 
+            and "last_child" in args["node_tags"]
+            and "child_of_statement" in args["node_tags"]):
+            # This means we're done with the expression, so let's speed up a bit.
+            self._check_issue_debugger_command("step", automatic=True)
+            # Next event will be before_statement_again
+
+                        
+            
+    
+    def _should_skip_event(self, msg):
+        frame_info = msg.stack[-1]
+        event = frame_info.last_event
+        tags = frame_info.last_event_args["node_tags"]
+        
+        if event == "after_statement":
+            return True
+        
+        # TODO: consult also configuration
+        if "call_function" in tags:
+            return True
+        else:
+            return False
+    
+    def _handle_toplevel_result(self, msg):
+        if self._main_frame_visualizer is not None:
+            self._main_frame_visualizer.close()
+            self._main_frame_visualizer = None    
+
+
+class FrameVisualizer:
+    """
+    Is responsible for stepping through statements and updating corresponding UI
+    in Editor-s, FunctionCallDialog-s, ModuleDialog-s
+    """
+    def __init__(self, text_frame, frame_info):
+        self._text_frame = text_frame
+        self._text = text_frame.text
+        self._frame_id = frame_info.id
+        self._filename = frame_info.filename
+        self._firstlineno = frame_info.firstlineno
+        self._source = frame_info.source
+        self._expression_box = ExpressionBox(text_frame)
+        self._next_frame_visualizer = None
+        
+        self._text.tag_configure('focus', background=_ACTIVE_FOCUS_BACKGROUND, borderwidth=1, relief=tk.SOLID)
+        self._text.tag_configure('exception', background="#FFBFD6")
+        self._text.tag_raise("exception", "focus")
+        self._text.set_read_only(True)
+    
+    def close(self):
+        if self._next_frame_visualizer:
+            self._next_frame_visualizer.close()
+            self._next_frame_visualizer = None
+            
+        self._text.set_read_only(False)
+        self._remove_focus_tags()
+        self._expression_box.clear_debug_view()
+    
+    def get_frame_id(self):
+        return self._frame_id
+    
+    def update_this_and_next_frames(self, msg):
+        """Must not be used on obsolete frame"""
+        
+        #debug("State: %s, focus: %s", msg.state, msg.focus)
+        
+        frame_info, next_frame_info = self._find_this_and_next_frame(msg.stack)
+        self._update_this_frame(msg, frame_info)
+        
+        # clear obsolete next frame visualizer
+        if (self._next_frame_visualizer 
+            and (not next_frame_info or 
+                 self._next_frame_visualizer.get_frame_id() != next_frame_info.id)):
+            self._next_frame_visualizer.close()
+            self._next_frame_visualizer = None
+            
+        if next_frame_info and not self._next_frame_visualizer:
+            self._next_frame_visualizer = self._create_next_frame_visualizer(next_frame_info)
+            
+        if self._next_frame_visualizer:
+            self._next_frame_visualizer.update_this_and_next_frames(msg)
+        
+    
+    def _remove_focus_tags(self):
+        self._text.tag_remove("focus", "0.0", "end")
+        self._text.tag_remove("exception", "0.0", "end")
+     
+    def _update_this_frame(self, msg, frame_info):
+        self._frame_info = frame_info
+        
+        # TODO: if focus is in expression, then find and highlight closest
+        # statement
+        if "statement" in frame_info.last_event:
+            self._remove_focus_tags()
+            self._tag_range(frame_info.last_event_focus, "focus", True)
+            if msg.exception is not None:
+                self._tag_range(frame_info.last_event_focus, "exception", True)
+                
+            self._text.tag_configure('focus', background=_ACTIVE_FOCUS_BACKGROUND, borderwidth=1, relief=tk.SOLID)
+        else:
+            self._text.tag_configure('focus', background=READ_ONLY_BACKGROUND, borderwidth=1, relief=tk.SOLID)
+            
+        self._expression_box.update_expression(msg, frame_info)
+
+    def _find_this_and_next_frame(self, stack):
+        for i in range(len(stack)):
+            if stack[i].id == self._frame_id:
+                if i == len(stack)-1: # last frame
+                    return stack[i], None
+                else:
+                    return stack[i], stack[i+1]
+                    
+        else:
+            raise AssertionError("Frame doesn't exist anymore")
+        
+    
+    def _tag_range(self, text_range, tag, see=False):
+        first_line, first_col, last_line = self._get_text_range_block(text_range)
+        
+        for lineno in range(first_line, last_line+1):
+            self._text.tag_add(tag,
+                              "%d.%d" % (lineno, first_col),
+                              "%d.0" % (lineno+1))
+            
+        self._text.update_idletasks()
+        self._text.see("%d.0" % (first_line))
+
+        if last_line - first_line < 3:
+            # if it's safe to assume that whole code fits into screen
+            # then scroll it down a bit so that expression view doesn't hide behind
+            # lower edge of the editor
+            self._text.update_idletasks()
+            self._text.see("%d.0" % (first_line+3))
+            
+    def _get_text_range_block(self, text_range):
+        first_line = text_range.lineno - self._frame_info.firstlineno + 1
+        last_line = text_range.end_lineno - self._frame_info.firstlineno + 1
+        first_line_content = self._text.get("%d.0" % first_line, "%d.end" % first_line)
+        if first_line_content.strip().startswith("elif "):
+            first_col = first_line_content.find("elif ")
+        else:
+            first_col = text_range.col_offset
+        
+        return (first_line, first_col, last_line)
+    
+            
+    
+    def _create_next_frame_visualizer(self, next_frame_info):
+        if next_frame_info.code_name == "<module>":
+            return ModuleLoadDialog(self._text, next_frame_info)
+        else:
+            dialog = FunctionCallDialog(self._text.master, next_frame_info)
+            
+            if self._expression_box.winfo_ismapped():
+                dialog.title(self._expression_box.get_focused_text())
+            else:
+                dialog.title("Function call at " + hex(self._frame_id))
+                 
+            return dialog
+     
+
+
+class MainFrameVisualizer(FrameVisualizer):
+    """
+    Takes care of stepping in the main module
+    """
+    def __init__(self, frame_info):
+        editor = get_workbench().get_editor_notebook().show_file(frame_info.filename)
+        FrameVisualizer.__init__(self, editor.get_code_view(), frame_info)
+        
+
+class CallFrameVisualizer(FrameVisualizer):
+    def __init__(self, text_frame, frame_id):
+        self._dialog = FunctionCallDialog(text_frame)
+        FrameVisualizer.__init__(self, self._dialog.get_code_view(), frame_id)
+        
+    def close(self):
+        super().close()
+        self._dialog.destroy()
+
+class ExpressionBox(tk.Text):
+    def __init__(self, codeview):
+        tk.Text.__init__(self, codeview.winfo_toplevel(), #codeview.text,
+                         height=1,
+                         width=1,
+                         relief=tk.RAISED,
+                         background="#DCEDF2",
+                         borderwidth=1,
+                         highlightthickness=0,
+                         padx=7,
+                         pady=7,
+                         wrap=tk.NONE,
+                         font=get_workbench().get_font("EditorFont"))
+        self._codeview = codeview
+        
+        self._main_range = None
+        self._last_focus = None
+        
+        self.tag_config("value", foreground="Blue")
+        self.tag_configure('before', background="#F8FC9A", borderwidth=1, relief=tk.SOLID)
+        self.tag_configure('after', background="#D7EDD3", borderwidth=1, relief=tk.FLAT)
+        self.tag_configure('exception', background="#FFBFD6", borderwidth=1, relief=tk.SOLID)
+        self.tag_raise("exception", "before")
+        self.tag_raise("exception", "after")
+        
+        
+    def update_expression(self, msg, frame_info):
+        focus = frame_info.last_event_focus
+        event = frame_info.last_event
+        
+        if event in ("before_expression", "before_expression_again"):
+            # (re)load stuff
+            if self._main_range is None or focus.not_smaller_eq_in(self._main_range):
+                self._load_expression(frame_info.filename, focus)
+                self._update_position(focus)
+                self._update_size()
+                
+            self._highlight_range(focus, event, msg.exception)
+            
+        
+        elif event == "after_expression":
+            logging.debug("EV: after_expression %s", msg)
+            
+            self.tag_configure('after', background="#BBEDB2", borderwidth=1, relief=tk.FLAT)
+            start_mark = self._get_mark_name(focus.lineno, focus.col_offset)
+            end_mark = self._get_mark_name(focus.end_lineno, focus.end_col_offset)
+            
+            assert hasattr(msg, "value")
+            logging.debug("EV: replacing expression with value")
+
+            original_focus_text = self.get(start_mark, end_mark)
+            self.delete(start_mark, end_mark)
+            
+            id_str = memory.format_object_id(msg.value["id"])
+            if get_workbench().in_heap_mode():
+                value_str = id_str
+            elif "StringLiteral" in frame_info.last_event_args["node_tags"]:
+                # No need to show Python replacing double quotes with single quotes
+                value_str = original_focus_text
+            else:
+                value_str = shorten_repr(msg.value["repr"], 100)
+            
+            object_tag = "object_" + str(msg.value["id"])
+            self.insert(start_mark, value_str, ('value', 'after', object_tag))
+            if misc_utils.running_on_mac_os():
+                sequence = "<Command-Button-1>"
+            else:
+                sequence = "<Control-Button-1>"
+            self.tag_bind(object_tag, sequence,
+                          lambda _: get_workbench().event_generate("ObjectSelect", object_id=msg.value["id"]))
+                
+            self._update_size()
+                
+            
+                
+        elif (event == "before_statement_again"
+              and self._main_range is not None # TODO: shouldn't need this 
+              and self._main_range.is_smaller_eq_in(focus)):
+            # we're at final stage of executing parent statement 
+            # (eg. assignment after the LHS has been evaluated)
+            # don't close yet
+            self.tag_configure('after', background="#DCEDF2", borderwidth=1, relief=tk.FLAT)
+        
+        elif event == "exception":
+            "TODO:"   
+        
+        else:
+            # hide and clear on non-expression events
+            self.clear_debug_view()
+
+        self._last_focus = focus
+        
+        
+    def get_focused_text(self):
+        if self._last_focus: 
+            start_mark = self._get_mark_name(self._last_focus.lineno, self._last_focus.col_offset)
+            end_mark = self._get_mark_name(self._last_focus.end_lineno, self._last_focus.end_col_offset)
+            return self.get(start_mark, end_mark)
+        else:
+            return ""
+      
+    def clear_debug_view(self):
+        self.place_forget()
+        self._main_range = None
+        self._last_focus = None
+        for tag in self.tag_names():
+            self.tag_remove(tag, "1.0", "end")
+            
+        for mark in self.mark_names():
+            self.mark_unset(mark)
+            
+    
+    def _load_expression(self, filename, text_range):
+        with tokenize.open(filename) as fp:
+            whole_source = fp.read()
+            
+        root = ast_utils.parse_source(whole_source, filename)
+        self._main_range = text_range
+        assert self._main_range is not None
+        main_node = ast_utils.find_expression(root, text_range)
+        
+        source = ast_utils.extract_text_range(whole_source, text_range)
+        logging.debug("EV.load_exp: %s", (text_range, main_node, source))
+        
+        self.delete("1.0", "end")
+        self.insert("1.0", source)
+        
+        # create node marks
+        def _create_index(lineno, col_offset):
+            local_lineno = lineno - main_node.lineno + 1
+            if lineno == main_node.lineno:
+                local_col_offset = col_offset - main_node.col_offset
+            else:
+                local_col_offset = col_offset
+                
+            return str(local_lineno) + "." + str(local_col_offset)
+
+        for node in ast.walk(main_node):
+            if "lineno" in node._attributes:
+                index1 = _create_index(node.lineno, node.col_offset)
+                index2 = _create_index(node.end_lineno, node.end_col_offset)
+                
+                start_mark = self._get_mark_name(node.lineno, node.col_offset) 
+                if not start_mark in self.mark_names():
+                    self.mark_set(start_mark, index1)
+                    self.mark_gravity(start_mark, tk.LEFT)
+                
+                end_mark = self._get_mark_name(node.end_lineno, node.end_col_offset) 
+                if not end_mark in self.mark_names():
+                    self.mark_set(end_mark, index2)
+                    self.mark_gravity(end_mark, tk.RIGHT)
+                    
+    def _get_mark_name(self, lineno, col_offset):
+        return str(lineno) + "_" + str(col_offset)
+    
+    def _get_tag_name(self, node_or_text_range):
+        return (str(node_or_text_range.lineno)
+                + "_" + str(node_or_text_range.col_offset)
+                + "_" + str(node_or_text_range.end_lineno)
+                + "_" + str(node_or_text_range.end_col_offset))
+    
+    def _highlight_range(self, text_range, state, exception):
+        logging.debug("EV._highlight_range: %s", text_range)
+        self.tag_remove("after", "1.0", "end")
+        self.tag_remove("before", "1.0", "end")
+        self.tag_remove("exception", "1.0", "end")
+        
+        if state.startswith("after"):
+            tag = "after"
+        elif state.startswith("before"):
+            tag = "before"
+        else:
+            return
+        
+        start_index = self._get_mark_name(text_range.lineno, text_range.col_offset)
+        end_index = self._get_mark_name(text_range.end_lineno, text_range.end_col_offset) 
+        self.tag_add(tag, start_index, end_index)
+        
+        if exception:
+            self.tag_add("exception", start_index, end_index)
+            
+    def _update_position(self, text_range):
+        self._codeview.update_idletasks()
+        text_line_number = text_range.lineno - self._codeview._first_line_number + 1
+        bbox = self._codeview.text.bbox(str(text_line_number) + "." + str(text_range.col_offset))
+        
+        if isinstance(bbox, tuple):
+            x = bbox[0] - self._codeview.text.cget("padx") + 6 
+            y = bbox[1] - self._codeview.text.cget("pady") - 6
+        else:
+            x = 30
+            y = 30
+            
+        widget = self._codeview.text
+        while widget != self.winfo_toplevel():
+            x += widget.winfo_x()
+            y += widget.winfo_y()
+            widget = widget.master
+            
+        self.place(x=x, y=y, anchor=tk.NW)
+        self.update_idletasks()
+
+    
+    def _update_size(self):
+        content = self.get("1.0", tk.END)
+        lines = content.splitlines()
+        self["height"] = len(lines)
+        self["width"] = max(map(len, lines))
+    
+
+class FrameDialog(tk.Toplevel, FrameVisualizer):
+    def __init__(self, master, frame_info):
+        tk.Toplevel.__init__(self, master)
+        
+        self.transient(master)
+        if misc_utils.running_on_windows():
+            self.wm_attributes('-toolwindow', 1)
+        
+        
+        # TODO: take size from prefs
+        editor_notebook = get_workbench().get_editor_notebook()
+        if master.winfo_toplevel() == get_workbench():
+            position_reference = editor_notebook
+        else:
+            # align to previous frame
+            position_reference = master.winfo_toplevel()
+            
+        self.geometry("{}x{}+{}+{}".format(editor_notebook.winfo_width(),
+                                           editor_notebook.winfo_height()-20,
+                                           position_reference.winfo_rootx(),
+                                           position_reference.winfo_rooty()))
+        self.protocol("WM_DELETE_WINDOW", self._on_close)
+        
+        self._init_layout_widgets(master, frame_info)
+        FrameVisualizer.__init__(self, self._text_frame, frame_info)
+        
+        self._load_code(frame_info)
+        self._text_frame.text.focus()
+    
+    def _init_layout_widgets(self, master, frame_info):
+        self.main_frame= ttk.Frame(self) # just a backgroud behind padding of main_pw, without this OS X leaves white border
+        self.main_frame.grid(sticky=tk.NSEW)        
+        self.rowconfigure(0, weight=1)
+        self.columnconfigure(0, weight=1)
+        self.main_pw = ui_utils.AutomaticPanedWindow(self.main_frame, orient=tk.VERTICAL)
+        self.main_pw.grid(sticky=tk.NSEW, padx=10, pady=10)
+        self.main_frame.rowconfigure(0, weight=1)
+        self.main_frame.columnconfigure(0, weight=1)
+        
+        self._code_book = ttk.Notebook(self.main_pw)
+        self._text_frame = CodeView(self._code_book, 
+                                      first_line_number=frame_info.firstlineno,
+                                      font=get_workbench().get_font("EditorFont"))
+        self._code_book.add(self._text_frame, text="Source")
+        self.main_pw.add(self._code_book, minsize=100)
+        
+    
+    def _load_code(self, frame_info):
+        self._text_frame.set_content(frame_info.source)
+    
+    def _update_this_frame(self, msg, frame_info):
+        FrameVisualizer._update_this_frame(self, msg, frame_info)
+    
+    def _on_close(self):
+        showinfo("Can't close yet", 'Use "Stop" command if you want to cancel debugging')
+    
+    
+    def close(self):
+        FrameVisualizer.close(self)
+        self.destroy()
+
+class FunctionCallDialog(FrameDialog):
+    def __init__(self, master, frame_info):
+        FrameDialog.__init__(self, master, frame_info)
+    
+    def _init_layout_widgets(self, master, frame_info):
+        FrameDialog._init_layout_widgets(self, master, frame_info)
+        self._locals_book = ttk.Notebook(self.main_pw)
+        self._locals_frame = VariablesFrame(self._locals_book)
+        self._locals_book.add(self._locals_frame, text="Local variables")
+        self.main_pw.add(self._locals_book, minsize=100)
+
+    def _load_code(self, frame_info):
+        FrameDialog._load_code(self, frame_info)
+        
+        if hasattr(frame_info, "function"):
+            function_label = frame_info.function["repr"]
+        else:
+            function_label = frame_info.code_name
+        
+        # change tab label
+        self._code_book.tab(self._text_frame, text=function_label)
+    
+    def _update_this_frame(self, msg, frame_info):
+        FrameDialog._update_this_frame(self, msg, frame_info)
+        self._locals_frame.update_variables(frame_info.locals)
+
+        
+class ModuleLoadDialog(FrameDialog):
+    def __init__(self, text_frame, frame_info):
+        FrameDialog.__init__(self, text_frame)
+    
+    
+    
+def load_plugin():
+    Debugger()
+    
+        
\ No newline at end of file
diff --git a/thonny/plugins/editor_config_page.py b/thonny/plugins/editor_config_page.py
new file mode 100644
index 0000000..7e4dff3
--- /dev/null
+++ b/thonny/plugins/editor_config_page.py
@@ -0,0 +1,53 @@
+import tkinter as tk
+from tkinter import ttk
+
+from thonny.config_ui import ConfigurationPage
+from thonny.globals import get_workbench
+import logging
+
+
+class EditorConfigurationPage(ConfigurationPage):
+    
+    def __init__(self, master):
+        ConfigurationPage.__init__(self, master)
+        
+        try:
+            self.add_checkbox("view.name_highlighting", "Highlight matching names")
+        except:
+            # name matcher may have been disabled
+            logging.warning("Couldn't create name matcher checkbox")
+            
+        try:
+            self.add_checkbox("view.locals_highlighting", "Highlight local variables")
+        except:
+            # locals highlighter may have been disabled
+            logging.warning("Couldn't create name locals highlighter checkbox")
+            
+        self.add_checkbox("view.paren_highlighting", "Highlight parentheses")
+        self.add_checkbox("view.syntax_coloring", "Highlight syntax elements")
+        
+        self.add_checkbox("edit.tab_complete_in_editor", "Allow code completion with Tab-key in editors", 
+                          columnspan=2, pady=(20,0), )
+        self.add_checkbox("edit.tab_complete_in_shell",  "Allow code completion with Tab-key in Shell",
+                          columnspan=2)
+        
+        self.add_checkbox("view.show_line_numbers", "Show line numbers", pady=(20,0))
+        self._line_length_var = get_workbench().get_variable("view.recommended_line_length")
+        label = ttk.Label(self, text="Recommended maximum line length\n(Set to 0 to turn off margin line)")
+        label.grid(row=7, column=0, sticky=tk.W)
+        self._line_length_combo = ttk.Combobox(self, width=4,
+                                        exportselection=False,
+                                        textvariable=self._line_length_var,
+                                        state='readonly',
+                                        values=[0,60,70,80,90,100,110,120])
+        self._line_length_combo.grid(row=7, column=1, sticky=tk.E)
+        
+        self.columnconfigure(0, weight=1)
+    
+    def apply(self):
+        ConfigurationPage.apply(self)
+        get_workbench().get_editor_notebook().update_appearance()
+    
+
+def load_plugin():
+    get_workbench().add_configuration_page("Editor", EditorConfigurationPage)
diff --git a/thonny/plugins/event_logging.py b/thonny/plugins/event_logging.py
new file mode 100644
index 0000000..fb9a228
--- /dev/null
+++ b/thonny/plugins/event_logging.py
@@ -0,0 +1,209 @@
+import os.path
+import tkinter as tk
+import time
+from thonny.globals import get_workbench
+from thonny.workbench import WorkbenchEvent
+from datetime import datetime
+import zipfile 
+from tkinter.filedialog import asksaveasfilename
+import json
+from thonny.shell import ShellView
+from thonny import THONNY_USER_DIR
+
+
+class EventLogger:
+    def __init__(self, filename):
+        self._filename = filename
+        self._init_logging()
+        self._init_commands()
+    
+    def _init_commands(self):
+        get_workbench().add_command(
+            "export_usage_logs",
+            "tools",
+            "Export usage logs...",
+            self._cmd_export,
+            group=110
+        )
+
+    
+    def _init_logging(self):
+        self._events = []
+        
+        wb = get_workbench()
+        wb.bind("WorkbenchClose", self._on_worbench_close, True)
+        
+        for sequence in ["<<Undo>>",
+                         "<<Redo>>",
+                         "<<Cut>>",
+                         "<<Copy>>",
+                         "<<Paste>>",
+                         #"<<Selection>>",
+                         #"<Key>",
+                         #"<KeyRelease>",
+                         "<Button-1>",
+                         "<Button-2>",
+                         "<Button-3>"
+                         ]:
+            self._bind_all(sequence)
+        
+        for sequence in ["Command",
+                         "MagicCommand",
+                         "Open",
+                         "Save",
+                         "SaveAs",
+                         "NewFile",
+                         "EditorTextCreated",
+                         #"ShellTextCreated", # Too bad, this event happens before event_logging is loaded
+                         "ShellCommand",
+                         "ShellInput",
+                         "ShowView",
+                         "HideView",
+                         "TextInsert",
+                         "TextDelete",
+                         ]:
+            self._bind_workbench(sequence)
+
+        self._bind_workbench("<FocusIn>", True)
+        self._bind_workbench("<FocusOut>", True)
+        
+        ### log_user_event(KeyPressEvent(self, e.char, e.keysym, self.text.index(tk.INSERT)))
+
+        
+        # TODO: if event data includes an Editor, then look up also text id
+    
+    def _bind_workbench(self, sequence, only_workbench_widget=False):
+        def handle(event):
+            if not only_workbench_widget or event.widget == get_workbench():
+                self._log_event(sequence, event)
+        
+        get_workbench().bind(sequence, handle, True)
+    
+    def _bind_all(self, sequence):
+        
+        def handle(event):
+            self._log_event(sequence, event)
+        
+        tk._default_root.bind_all(sequence, handle, True)
+    
+    
+    def _extract_interesting_data(self, event, sequence):
+        attributes = vars(event)
+        
+        # generate some new attributes
+        if "text_widget" not in attributes:
+            if "editor" in attributes:
+                attributes["text_widget"] = attributes["editor"].get_text_widget()
+        
+            if "widget" in attributes and isinstance(attributes["widget"], tk.Text):
+                attributes["text_widget"] = attributes["widget"]
+            
+        
+        if "text_widget" in attributes:
+            widget = attributes["text_widget"]
+            if isinstance(widget.master, ShellView):
+                attributes["text_widget_context"] = "shell"
+            
+        
+        # select attributes      
+        data = {}
+        for name in attributes:
+            # skip some attributes
+            if (name.startswith("_")
+                or isinstance(event, WorkbenchEvent) and name in ["update", 
+                                                                  "setdefault"]
+                or isinstance(event, tk.Event) and name not in ["widget", 
+                                                                "text_widget", 
+                                                                "text_widget_context"]):
+                continue
+            
+            value = attributes[name]
+            
+            if isinstance(value, tk.BaseWidget) or isinstance(value, tk.Tk):
+                data[name + "_id"] = id(value)
+                data[name + "_class"] = value.__class__.__name__
+                
+            elif type(value) in [str, int, float]:
+                data[name] = value
+            
+            else:
+                data[name] = repr(value)
+        
+        return data
+    
+    def _get_log_dir(self):
+        return os.path.dirname(self._filename)
+    
+    def _cmd_export(self):
+        
+        filename = asksaveasfilename (
+                filetypes =  [('Zip-files', '.zip'), ('all files', '.*')], 
+                defaultextension = ".zip",
+                initialdir = get_workbench().get_option("run.working_directory"),
+                initialfile = time.strftime("ThonnyUsageLogs_%Y-%m-%d.zip")
+        )
+        
+        if not filename:
+            return
+        
+        log_dir = self._get_log_dir()
+        
+        with zipfile.ZipFile(filename, 'w', compression=zipfile.ZIP_DEFLATED) as zipf:
+            for item in os.listdir(log_dir):
+                if item.endswith(".txt") or item.endswith(".zip"):
+                    zipf.write(os.path.join(log_dir, item), arcname=item)
+            
+    
+    def _log_event(self, sequence, event):
+        data = self._extract_interesting_data(event, sequence)
+        data["sequence"] = sequence 
+        data["time"] = datetime.now().isoformat()
+        self._events.append(data)
+    
+    def _on_worbench_close(self, event=None):
+        with open(self._filename, encoding="UTF-8", mode="w") as fp:
+            json.dump(self._events, fp, indent="    ")
+        
+        self._check_compress_logs()
+    
+    def _check_compress_logs(self):
+        # if uncompressed logs have grown over 10MB,
+        # compress these into new zipfile
+        
+        log_dir = self._get_log_dir()
+        total_size = 0
+        uncompressed_files = []
+        for item in os.listdir(log_dir):
+            if item.endswith(".txt"):
+                full_name = os.path.join(log_dir, item)
+                total_size += os.stat(full_name).st_size
+                uncompressed_files.append((item, full_name))
+            
+        if total_size > 10*1024*1024:
+            zip_filename = _generate_timestamp_file_name("zip")            
+            with zipfile.ZipFile(zip_filename, 'w', compression=zipfile.ZIP_DEFLATED) as zipf:
+                for item, full_name in uncompressed_files:
+                    zipf.write(full_name, arcname=item)
+        
+            for _, full_name in uncompressed_files:
+                os.remove(full_name)
+            
+
+def _generate_timestamp_file_name(extension):
+    # generate log filename
+    folder = os.path.expanduser(os.path.join(THONNY_USER_DIR, "user_logs"))
+    if not os.path.exists(folder):
+        os.makedirs(folder)
+        
+    for i in range(100): 
+        filename = os.path.join(folder, time.strftime("%Y-%m-%d_%H-%M-%S_{}.{}".format(i, extension)));
+        if not os.path.exists(filename):
+            return filename
+    
+    raise RuntimeError()
+
+def load_plugin():
+    filename = _generate_timestamp_file_name("txt")
+    # create logger
+    EventLogger(filename)
+    
\ No newline at end of file
diff --git a/thonny/plugins/event_view.py b/thonny/plugins/event_view.py
new file mode 100644
index 0000000..9531aff
--- /dev/null
+++ b/thonny/plugins/event_view.py
@@ -0,0 +1,35 @@
+""" Helper view for Thonny developers
+"""
+
+from thonny.tktextext import TextFrame
+from thonny.globals import get_workbench
+
+class EventsView(TextFrame):
+    def __init__(self, master):
+        TextFrame.__init__(self, master)
+        #self.text.config(wrap=tk.WORD)
+        get_workbench().bind("ShowView", self._log_event, True)
+        get_workbench().bind("HideView", self._log_event, True)
+        get_workbench().bind("ToplevelResult", self._log_event, True)
+        get_workbench().bind("DebuggerProgress", self._log_event, True)
+        get_workbench().bind("ProgramOutput", self._log_event, True)
+        get_workbench().bind("InputRequest", self._log_event, True)
+    
+    
+    def _log_event(self, event):
+        self.text.insert("end", event.sequence + "\n")
+        for name in dir(event):
+            if name not in ["sequence", "setdefault", "update"] and not name.startswith("_"):
+                self.text.insert("end", "    " + name + ": " + repr(getattr(event, name))[:100] + "\n")
+        
+        if event.sequence == "DebuggerProgress":
+            frame = event.stack[-1]
+            self.text.insert("end", "    " + "event" + ": " + frame.last_event + "\n") 
+            self.text.insert("end", "    " + "focus" + ": " + str(frame.last_event_focus) + "\n") 
+            self.text.insert("end", "    " + "args" + ": " + str(frame.last_event_args) + "\n") 
+
+        self.text.see("end")
+
+def load_plugin():
+    if get_workbench().get_option("debug_mode"):
+        get_workbench().add_view(EventsView, "Events", "se")
\ No newline at end of file
diff --git a/thonny/plugins/find_replace.py b/thonny/plugins/find_replace.py
new file mode 100644
index 0000000..0219aa5
--- /dev/null
+++ b/thonny/plugins/find_replace.py
@@ -0,0 +1,355 @@
+# -*- coding: utf-8 -*-
+
+import tkinter as tk
+from tkinter import ttk
+
+from thonny import misc_utils
+from thonny.globals import get_workbench
+from thonny.ui_utils import select_sequence
+
+#TODO - consider moving the cmd_find method to main class in order to pass the editornotebook reference
+#TODO - logging
+#TODO - instead of text.see method create another one which attempts to center the line where the text was found
+#TODO - test on mac and linux
+
+# Handles the find dialog display and the logic of searching.
+#Communicates with the codeview that is passed to the constructor as a parameter.
+
+_active_find_dialog = None
+
+class FindDialog(tk.Toplevel): 
+    def __init__(self, master):
+        padx=15
+        pady=15
+        
+        tk.Toplevel.__init__(self, master, takefocus=1, background="pink")
+        main_frame = ttk.Frame(self)
+        main_frame.grid(row=1, column=1, sticky="nsew")
+        self.columnconfigure(1, weight=1)
+        self.rowconfigure(1, weight=1)
+        
+        self.codeview = master
+        self.codeview.text.tag_configure("hit", background="Yellow", foreground=None)
+        
+        self._init_found_tag_styles()  #sets up the styles used to highlight found strings
+        #references to the current set of passive found tags e.g. all words that match the searched term but are not the active string
+        self.passive_found_tags = set()
+        self.active_found_tag = None    #reference to the currently active (centered) found string
+
+        #if find dialog was used earlier then put the previous search word to the Find entry field
+        #TODO - refactor this, there must be a better way
+        try:
+            #if find dialog was used earlier then this is present
+            FindDialog.last_searched_word = FindDialog.last_searched_word
+        except:
+            FindDialog.last_searched_word = None #if this variable does not exist then this is the first time find dialog has been launched
+
+        #a tuple containing the start and indexes of the last processed string
+        #if the last action was find, then the end index is start index + 1
+        #if the last action was replace, then the indexes correspond to the start
+        #and end of the inserted word
+        self.last_processed_indexes = None
+        self.last_search_case = None    #case sensitivity value used during the last search
+        
+        #set up window display
+        self.geometry("+%d+%d" % (master.winfo_rootx() + master.winfo_width() // 2,
+                                  master.winfo_rooty() + master.winfo_height() // 2 - 150))
+
+        self.title("Find & Replace")
+        if misc_utils.running_on_mac_os():
+            self.configure(background="systemSheetBackground")
+        self.resizable(height=tk.FALSE, width=tk.FALSE)
+        self.transient(master) 
+        self.protocol("WM_DELETE_WINDOW", self._ok)
+      
+        #Find text label
+        self.find_label = ttk.Label(main_frame, text="Find:");   
+        self.find_label.grid(column=0, row=0, sticky="w", padx=(padx, 0), pady=(pady, 0));
+
+        #Find text field
+        self.find_entry_var = tk.StringVar()
+        self.find_entry = ttk.Entry(main_frame, textvariable=self.find_entry_var);
+        self.find_entry.grid(column=1, row=0, columnspan=2, padx=(0, 10), pady=(pady, 0));
+        if FindDialog.last_searched_word is not None:
+            self.find_entry.insert(0, FindDialog.last_searched_word)
+
+        #Replace text label
+        self.replace_label = ttk.Label(main_frame, text="Replace with:"); 
+        self.replace_label.grid(column=0, row=1, sticky="w", padx=(padx, 0));
+
+        #Replace text field
+        self.replace_entry = ttk.Entry(main_frame);
+        self.replace_entry.grid(column=1, row=1, columnspan=2, padx=(0, 10));
+
+        #Info text label (invisible by default, used to tell user that searched string was not found etc)
+        self.infotext_label_var = tk.StringVar();
+        self.infotext_label_var.set("");
+        self.infotext_label = ttk.Label(main_frame, textvariable=self.infotext_label_var, foreground="red"); #TODO - style to conf
+        self.infotext_label.grid(column=0, row=2, columnspan=3, pady=3, padx=(padx, 0));
+
+        #Case checkbox
+        self.case_var = tk.IntVar()
+        self.case_checkbutton = ttk.Checkbutton(main_frame,text="Case sensitive",variable=self.case_var)
+        self.case_checkbutton.grid(column=0, row=3, padx=(padx, 0), pady=(0, pady))
+
+        #Direction radiobuttons
+        self.direction_var = tk.IntVar()
+        self.up_radiobutton = ttk.Radiobutton(main_frame, text="Up", variable=self.direction_var, value=1)
+        self.up_radiobutton.grid(column=1, row=3, pady=(0, pady))
+        self.down_radiobutton = ttk.Radiobutton(main_frame, text="Down", variable=self.direction_var, value=2)
+        self.down_radiobutton.grid(column=2, row=3, pady=(0, pady))
+        self.down_radiobutton.invoke()
+
+        #Find button - goes to the next occurrence
+        self.find_button = ttk.Button(main_frame, text="Find", command=self._perform_find, default="active")
+        self.find_button.grid(column=3, row=0, sticky=tk.W + tk.E, pady=(pady, 0), padx=(0, padx));
+        self.find_button.config(state='disabled') 
+
+        #Replace button - replaces the current occurrence, if it exists
+        self.replace_button = ttk.Button(main_frame, text="Replace", command=self._perform_replace)
+        self.replace_button.grid(column=3, row=1, sticky=tk.W + tk.E, padx=(0, padx));
+        self.replace_button.config(state='disabled')
+
+        #Replace + find button - replaces the current occurence and goes to next
+        self.replace_and_find_button = ttk.Button(main_frame, text="Replace+Find", command=self._perform_replace_and_find) #TODO - text to resources
+        self.replace_and_find_button.grid(column=3, row=2, sticky=tk.W + tk.E, padx=(0, padx));
+        self.replace_and_find_button.config(state='disabled')  
+ 
+        #Replace all button - replaces all occurrences
+        self.replace_all_button = ttk.Button(main_frame, text="Replace all", command=self._perform_replace_all) #TODO - text to resources
+        self.replace_all_button.grid(column=3, row=3, sticky=tk.W + tk.E, padx=(0, padx), pady=(0, pady));        
+        if FindDialog.last_searched_word == None:
+            self.replace_all_button.config(state='disabled') 
+
+        #create bindings
+        self.bind('<Escape>', self._ok)
+        self.find_entry_var.trace('w', self._update_button_statuses)
+        self.find_entry.bind("<Return>", self._perform_find, True)
+        self.bind("<F3>", self._perform_find, True)
+        self.find_entry.bind("<KP_Enter>", self._perform_find, True)
+
+        self._update_button_statuses()
+        
+        global _active_find_dialog
+        _active_find_dialog = self
+        self.focus_set();
+
+    def focus_set(self):
+        self.find_entry.focus_set()
+        self.find_entry.selection_range(0, tk.END)
+
+    #callback for text modifications on the find entry object, used to dynamically enable and disable buttons
+    def _update_button_statuses(self, *args):
+        find_text = self.find_entry_var.get()
+        if len(find_text) == 0:
+            self.find_button.config(state='disabled')
+            self.replace_and_find_button.config(state='disabled')
+            self.replace_all_button.config(state='disabled')
+        else:
+            self.find_button.config(state='normal')
+            self.replace_all_button.config(state='normal')
+            if self.active_found_tag is not None: 
+                self.replace_and_find_button.config(state='normal')
+
+    #returns whether the next search is case sensitive based on the current value of the case sensitivity checkbox
+    def _is_search_case_sensitive(self):
+        return self.case_var.get() != 0
+
+    #returns whether the current search is a repeat of the last searched based on all significant values
+    def _repeats_last_search(self, tofind):
+        return tofind == FindDialog.last_searched_word and self.last_processed_indexes is not None and self.last_search_case == self._is_search_case_sensitive();
+
+
+    #performs the replace operation - replaces the currently active found word with what is entered in the replace field
+    def _perform_replace(self):
+
+        #nothing is currently in found status
+        if self.active_found_tag == None:
+            return
+
+        #get the found word bounds
+        del_start = self.active_found_tag[0]
+        del_end = self.active_found_tag[1]
+
+        #erase all tags - these would not be correct anyway after new word is inserted
+        self._remove_all_tags()
+        toreplace = self.replace_entry.get(); #get the text to replace
+
+        #delete the found word
+        self.codeview.text.delete(del_start, del_end)
+        #insert the new word
+        self.codeview.text.insert(del_start, toreplace)
+        #mark the inserted word boundaries 
+        self.last_processed_indexes = (del_start, self.codeview.text.index("%s+%dc" % (del_start, len(toreplace))))
+
+        get_workbench().event_generate("Replace",
+            widget=self.codeview.text,
+            old_text=self.codeview.text.get(del_start, del_end),
+            new_text=toreplace)
+
+    #performs the replace operation followed by a new find
+    def _perform_replace_and_find(self):
+        if self.active_found_tag == None:
+            return
+        self._perform_replace()
+        self._perform_find()
+
+    #replaces all occurences of the search string with the replace string
+    def _perform_replace_all(self):
+
+        tofind = self.find_entry.get();
+        if len(tofind) == 0:
+            self.infotext_label_var.set("Enter string to be replaced.")
+            return
+        
+        toreplace = self.replace_entry.get();
+   
+        self._remove_all_tags()
+
+        currentpos = 1.0;
+        end = self.codeview.text.index("end");
+
+        while True:
+            currentpos = self.codeview.text.search(tofind, currentpos, end, nocase = not self._is_search_case_sensitive()); 
+            if currentpos == "":
+                break
+
+            endpos = self.codeview.text.index("%s+%dc" % (currentpos, len(tofind)))
+
+            self.codeview.text.delete(currentpos, endpos)
+
+            if toreplace != "":
+                self.codeview.text.insert(currentpos, toreplace)
+                
+            currentpos = self.codeview.text.index("%s+%dc" % (currentpos, len(toreplace)))
+
+        get_workbench().event_generate("ReplaceAll",
+            widget=self.codeview.text,
+            old_text=tofind,
+            new_text=toreplace)
+        
+        
+    def _perform_find(self, event=None):
+        self.infotext_label_var.set("");    #reset the info label text
+        tofind = self.find_entry.get(); #get the text to find 
+        if len(tofind) == 0:    #in the case of empty string, cancel
+            return              #TODO - set warning text to info label?
+
+        search_backwards = self.direction_var.get() == 1 #True - search backwards ('up'), False - forwards ('down')
+        
+        if self._repeats_last_search(tofind): #continuing previous search, find the next occurrence
+            if search_backwards:
+                search_start_index = self.last_processed_indexes[0];
+            else:
+                search_start_index = self.last_processed_indexes[1];
+            
+            if self.active_found_tag is not None:
+                self.codeview.text.tag_remove("currentfound", self.active_found_tag[0], self.active_found_tag[1]);  #remove the active tag from the previously found string
+                self.passive_found_tags.add((self.active_found_tag[0], self.active_found_tag[1]))                   #..and set it to passive instead
+                self.codeview.text.tag_add("found", self.active_found_tag[0], self.active_found_tag[1]);
+        
+        else: #start a new search, start from the current insert line position
+            if self.active_found_tag is not None:
+                self.codeview.text.tag_remove("currentfound", self.active_found_tag[0], self.active_found_tag[1]); #remove the previous active tag if it was present
+            for tag in self.passive_found_tags:
+                self.codeview.text.tag_remove("found", tag[0], tag[1]);                                            #and remove all the previous passive tags that were present
+            search_start_index = self.codeview.text.index("insert");    #start searching from the current insert position
+            self._find_and_tag_all(tofind);                             #set the passive tag to ALL found occurences
+            FindDialog.last_searched_word = tofind;                     #set the data about last search
+            self.last_search_case = self._is_search_case_sensitive();       
+
+        
+
+        wordstart = self.codeview.text.search(tofind, search_start_index, backwards = search_backwards, forwards = not search_backwards, nocase = not self._is_search_case_sensitive()); #performs the search and sets the start index of the found string
+        if len(wordstart) == 0:
+            self.infotext_label_var.set("The specified text was not found!"); #TODO - better text, also move it to the texts resources list
+            self.replace_and_find_button.config(state='disabled')
+            self.replace_button.config(state='disabled')            
+            return
+        
+        self.last_processed_indexes = (wordstart, self.codeview.text.index("%s+1c" % wordstart)); #sets the data about last search      
+        self.codeview.text.see(wordstart); #moves the view to the found index
+        wordend = self.codeview.text.index("%s+%dc" % (wordstart, len(tofind))); #calculates the end index of the found string
+        self.codeview.text.tag_add("currentfound", wordstart, wordend); #tags the found word as active
+        self.active_found_tag = (wordstart, wordend);
+        self.replace_and_find_button.config(state='normal')
+        self.replace_button.config(state='normal')
+
+        get_workbench().event_generate("Find",
+            widget=self.codeview.text,
+            text=tofind,
+            backwards=search_backwards,
+            case_sensitive=self._is_search_case_sensitive())
+        
+     
+    def _ok(self, event=None):
+        """Called when the window is closed. responsible for handling all cleanup."""
+        self._remove_all_tags()
+        self.destroy()
+
+        global _active_find_dialog
+        _active_find_dialog = None
+        
+
+    #removes the active tag and all passive tags
+    def _remove_all_tags(self):
+        for tag in self.passive_found_tags:
+            self.codeview.text.tag_remove("found", tag[0], tag[1]); #removes the passive tags
+
+        if self.active_found_tag is not None:
+            self.codeview.text.tag_remove("currentfound", self.active_found_tag[0], self.active_found_tag[1]); #removes the currently active tag   
+
+        self.active_found_tag = None
+        self.replace_and_find_button.config(state='disabled')
+        self.replace_button.config(state='disabled')        
+        
+    #finds and tags all occurences of the searched term
+    def _find_and_tag_all(self, tofind, force=False): 
+        #TODO - to be improved so only whole words are matched - surrounded by whitespace, parentheses, brackets, colons, semicolons, points, plus, minus
+
+        if self._repeats_last_search(tofind) and not force:   #nothing to do, all passive tags already set
+            return
+
+        currentpos = 1.0;
+        end = self.codeview.text.index("end");
+
+        #searches and tags until the end of codeview
+        while True:
+            currentpos = self.codeview.text.search(tofind, currentpos, end, nocase = not self._is_search_case_sensitive()); 
+            if currentpos == "":
+                break
+
+            endpos = self.codeview.text.index("%s+%dc" % (currentpos, len(tofind)))
+            self.passive_found_tags.add((currentpos, endpos))
+            self.codeview.text.tag_add("found", currentpos, endpos);
+            
+            currentpos = self.codeview.text.index("%s+1c" % currentpos);
+
+    #initializes the tagging styles 
+    def _init_found_tag_styles(self):
+        self.codeview.text.tag_configure("found", foreground="blue", underline=True) #TODO - style
+        self.codeview.text.tag_configure("currentfound", foreground="white", background="red")  #TODO - style
+
+
+def load_plugin():
+    def cmd_open_find_dialog():
+        if _active_find_dialog is not None:
+            _active_find_dialog.focus_set()
+        else:
+            editor = get_workbench().get_editor_notebook().get_current_editor()
+            if editor:
+                FindDialog(editor._code_view)
+
+    
+    def find_f3(event):
+        if _active_find_dialog is None:
+            cmd_open_find_dialog()
+        else:
+            _active_find_dialog._perform_find(event)
+         
+    get_workbench().add_command("OpenFindDialog", "edit", 'Find & Replace',
+        cmd_open_find_dialog,
+        default_sequence=select_sequence("<Control-f>", "<Command-f>"))
+    
+    get_workbench().bind("<F3>", find_f3, True)
+    
\ No newline at end of file
diff --git a/thonny/plugins/font_config_page.py b/thonny/plugins/font_config_page.py
new file mode 100644
index 0000000..4ba783e
--- /dev/null
+++ b/thonny/plugins/font_config_page.py
@@ -0,0 +1,86 @@
+import tkinter as tk
+from tkinter import font as tk_font 
+from tkinter import ttk
+
+from thonny.config_ui import ConfigurationPage
+from thonny.globals import get_workbench
+from thonny.ui_utils import create_string_var
+import textwrap
+
+
+class FontConfigurationPage(ConfigurationPage):
+    
+    def __init__(self, master):
+        ConfigurationPage.__init__(self, master)
+        
+        self._family_variable = create_string_var(
+            get_workbench().get_option("view.editor_font_family"),
+            modification_listener=self._update_preview_font)
+        
+        self._size_variable = create_string_var(
+            get_workbench().get_option("view.editor_font_size"),
+            modification_listener=self._update_preview_font)
+        
+        ttk.Label(self, text="Editor font").grid(row=0, column=0, sticky="w")
+        
+        self._family_combo = ttk.Combobox(self,
+                                          exportselection=False,
+                                          state='readonly',
+                                          textvariable=self._family_variable,
+                                          values=self._get_families_to_show())
+        self._family_combo.grid(row=1, column=0, sticky=tk.NSEW, padx=(0,10))
+        
+        ttk.Label(self, text="Size").grid(row=0, column=1, sticky="w")
+        self._size_combo = ttk.Combobox(self, width=4,
+                                        exportselection=False,
+                                        textvariable=self._size_variable,
+                                        state='readonly',
+                                        values=[str(x) for x in range(3,73)])
+        
+        self._size_combo.grid(row=1, column=1)
+        
+        
+        ttk.Label(self, text="Preview").grid(row=2, column=0, sticky="w", pady=(10,0))
+        self._preview_font = tk_font.Font()
+        self._preview_text = tk.Text(self,
+                                height=10,
+                                borderwidth=1,
+                                font=self._preview_font,
+                                wrap=tk.WORD)
+        self._preview_text.insert("1.0", textwrap.dedent("""
+            The quick brown fox jumps over the lazy dog
+            
+            ABCDEFGHIJKLMNOPQRSTUVWXYZ
+            abcdefghijklmnopqrstuvwxyz
+            
+            1234567890
+            @$%()[]{}/\_-+
+            "Hello " + 'world!'""").strip())
+        self._preview_text.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW, pady=(0,5))
+        
+            
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(3, weight=1)
+        self._update_preview_font()
+    
+    def apply(self):
+        if (not self._family_variable.modified
+            and not self._size_variable.modified):
+            return
+        
+        get_workbench().set_option("view.editor_font_size", int(self._size_variable.get()))
+        get_workbench().set_option("view.editor_font_family", self._family_variable.get())
+        get_workbench().update_fonts()
+    
+    def _update_preview_font(self):
+        self._preview_font.configure(family=self._family_variable.get(),
+                                     size=int(self._size_variable.get()))
+    
+    def _get_families_to_show(self):
+        return sorted(filter(
+            lambda name : name[0].isalpha(),          
+            tk_font.families()
+        ))
+
+def load_plugin():
+    get_workbench().add_configuration_page("Font", FontConfigurationPage)
\ No newline at end of file
diff --git a/thonny/plugins/general_config_page.py b/thonny/plugins/general_config_page.py
new file mode 100644
index 0000000..e5107a3
--- /dev/null
+++ b/thonny/plugins/general_config_page.py
@@ -0,0 +1,38 @@
+import tkinter as tk
+from tkinter import ttk
+
+from thonny.config_ui import ConfigurationPage
+from thonny.globals import get_workbench
+
+
+class GeneralConfigurationPage(ConfigurationPage):
+    
+    def __init__(self, master):
+        ConfigurationPage.__init__(self, master)
+        
+        self._single_instance_var = get_workbench().get_variable("general.single_instance")
+        self._single_instance_checkbox = ttk.Checkbutton(self,
+                                                         text="Allow only single Thonny instance", 
+                                                      variable=self._single_instance_var)
+        self._single_instance_checkbox.grid(row=1, column=0, sticky=tk.W)
+        
+        self._expert_var = get_workbench().get_variable("general.expert_mode")
+        self._expert_checkbox = ttk.Checkbutton(self, text="Expert mode", variable=self._expert_var)
+        self._expert_checkbox.grid(row=2, column=0, sticky=tk.W)
+        
+        self._debug_var = get_workbench().get_variable("general.debug_mode")
+        self._debug_checkbox = ttk.Checkbutton(self, text="Debug mode", variable=self._debug_var)
+        self._debug_checkbox.grid(row=3, column=0, sticky=tk.W)
+        
+        reopen_label = ttk.Label(self, text="NB! Restart Thonny after changing these options"
+                                 + "\nin order to see the effect")
+        reopen_label.grid(row=4, column=0, sticky=tk.W, pady=20)
+        
+        
+        self.columnconfigure(0, weight=1)
+
+            
+    
+
+def load_plugin():
+    get_workbench().add_configuration_page("General", GeneralConfigurationPage)
diff --git a/thonny/plugins/goto_definition.py b/thonny/plugins/goto_definition.py
new file mode 100644
index 0000000..463a685
--- /dev/null
+++ b/thonny/plugins/goto_definition.py
@@ -0,0 +1,34 @@
+import tkinter as tk
+from jedi import Script
+from thonny.globals import get_workbench, get_runner
+from thonny.ui_utils import control_is_pressed
+
+
+def goto_definition(event):
+    if not control_is_pressed(event.state):
+        return
+    
+    assert isinstance(event.widget, tk.Text)
+    text = event.widget
+
+    source = text.get("1.0", "end") 
+    index = text.index("insert")
+    index_parts = index.split('.')
+    line, column = int(index_parts[0]), int(index_parts[1])
+    # TODO: find current editor filename
+    script = Script(source, line=line, column=column, path="")
+    defs = script.goto_definitions()
+    if len(defs) > 0:
+        module_path = defs[0].module_path
+        module_name = defs[0].module_name
+        line = defs[0].line
+        if module_path and line is not None:
+            get_workbench().get_editor_notebook().show_file(module_path, line)
+        elif module_name == "" and line is not None: # current editor
+            get_workbench().get_editor_notebook().get_current_editor().select_range(line)
+    
+    
+
+def load_plugin():
+    wb = get_workbench()  
+    wb.bind_class("CodeViewText", "<1>", goto_definition, True)
diff --git a/thonny/plugins/heap.py b/thonny/plugins/heap.py
new file mode 100644
index 0000000..e5fda6d
--- /dev/null
+++ b/thonny/plugins/heap.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+import tkinter as tk
+
+from thonny.memory import MemoryFrame, format_object_id, MAX_REPR_LENGTH_IN_GRID,\
+    parse_object_id
+from thonny.misc_utils import shorten_repr
+from thonny.globals import get_workbench, get_runner
+from thonny.common import InlineCommand
+
+class HeapView(MemoryFrame):
+    def __init__(self, master):
+        MemoryFrame.__init__(self, master, ("id", "value"))
+        
+        self.tree.column('id', width=100, anchor=tk.W, stretch=False)
+        self.tree.column('value', width=150, anchor=tk.W, stretch=True)
+        
+        self.tree.heading('id', text='ID', anchor=tk.W)
+        self.tree.heading('value', text='Value', anchor=tk.W) 
+        
+        get_workbench().bind("Heap", self._handle_heap_event, True)
+        
+        get_workbench().bind("DebuggerProgress", self._request_heap_data, True)
+        get_workbench().bind("ToplevelResult", self._request_heap_data, True)
+        # Showing new globals may introduce new interesting objects
+        get_workbench().bind("Globals", self._request_heap_data, True) 
+
+    def _update_data(self, data):
+        self._clear_tree()
+        for value_id in sorted(data.keys()):
+            node_id = self.tree.insert("", "end")
+            self.tree.set(node_id, "id", format_object_id(value_id))
+            self.tree.set(node_id, "value", shorten_repr(data[value_id]["repr"], MAX_REPR_LENGTH_IN_GRID))
+    
+    def before_show(self):
+        self._request_heap_data(even_when_hidden=True)
+
+    def on_select(self, event):
+        iid = self.tree.focus()
+        if iid != '':
+            object_id = parse_object_id(self.tree.item(iid)['values'][0])
+            get_workbench().event_generate("ObjectSelect", object_id=object_id)
+            
+    def _request_heap_data(self, msg=None, even_when_hidden=False):
+        if self.winfo_ismapped() or even_when_hidden:
+            # TODO: update itself also when it becomes visible
+            get_runner().send_command(InlineCommand("get_heap"))
+            
+    def _handle_heap_event(self, msg):
+        if self.winfo_ismapped():
+            if hasattr(msg, "heap"):
+                self._update_data(msg.heap)
+                
+def load_plugin():
+    get_workbench().add_view(HeapView, "Heap", "e")
\ No newline at end of file
diff --git a/thonny/plugins/help/__init__.py b/thonny/plugins/help/__init__.py
new file mode 100644
index 0000000..3fb5285
--- /dev/null
+++ b/thonny/plugins/help/__init__.py
@@ -0,0 +1,102 @@
+import tkinter as tk
+import tkinter.font
+import os.path
+from tkinter import ttk
+from thonny import tktextext
+from thonny.globals import get_workbench
+
+class HelpView(ttk.Frame):
+    def __init__(self, master):
+        ttk.Frame.__init__(self, master)
+        
+        main_font = tkinter.font.nametofont("TkDefaultFont")
+        
+        bold_font = main_font.copy()
+        bold_font.configure(weight="bold", size=main_font.cget("size"))
+        
+        h1_font = main_font.copy()
+        h1_font.configure(size=main_font.cget("size") * 2, weight="bold")
+        
+        h2_font = main_font.copy()
+        h2_font.configure(size=round(main_font.cget("size") * 1.5), weight="bold")
+        
+        h3_font = main_font.copy()
+        h3_font.configure(size=main_font.cget("size"), weight="bold")
+        
+        self.text = tktextext.TweakableText(self, border=0, padx=15, pady=15,
+                                            font=main_font,
+                                            wrap="word")
+        
+        self.text.tag_configure("h1", font=h1_font)
+        self.text.tag_configure("h2", font=h2_font)
+        self.text.tag_configure("h3", font=h3_font)
+        self.text.grid(row=0, column=0, sticky="nsew")
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        
+        self._vbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
+        self._vbar.grid(row=0, column=1, sticky=tk.NSEW)
+        self._vbar['command'] = self.text.yview 
+        self.text['yscrollcommand'] = self._vbar.set  
+        
+        
+        self.load_rst_file("help.rst")
+        
+    
+    def clear(self):
+        self.text.direct_delete("1.0", "end")
+    
+    def load_rst(self, source):
+        
+        def is_symbol_line(line, symbol, min_count=3):
+            line = line.rstrip()
+            return (line.startswith(symbol) 
+                    and line.replace(symbol, "") == ""
+                    and len(line) >= min_count)
+        
+        self.clear()
+        lines = source.splitlines(True)
+        i = 0
+        while i < len(lines):
+            if is_symbol_line(lines[i], "="):
+                self.append_chars(lines[i+1], "h1")
+                assert is_symbol_line(lines[i+2], "=")
+                i += 3
+            elif (i < len(lines)-1 
+                  and is_symbol_line(lines[i+1], "=")):
+                self.append_chars(lines[i], "h2")
+                i += 2
+            elif (i < len(lines)-1 
+                  and is_symbol_line(lines[i+1], "-")):
+                self.append_chars(lines[i], "h3")
+                i += 2
+            else:
+                self.append_rst_line(lines[i])
+                i += 1
+    
+    def append_chars(self, chars, tag=None):
+        if tag:
+            self.text.direct_insert("end", chars, (tag,))
+        else:
+            self.text.direct_insert("end", chars)
+    
+    def append_rst_line(self, source):
+        self.append_chars(source)
+    
+    def load_rst_file(self, filename):
+        if not os.path.isabs(filename):
+            filename = os.path.join(os.path.dirname(__file__), filename) 
+            
+        with open(filename, encoding="UTF-8") as fp:
+            self.load_rst(fp.read())
+
+
+def open_help():
+    get_workbench().show_view("HelpView")
+
+def load_plugin():
+    get_workbench().add_view(HelpView, "Help", "ne")
+    get_workbench().add_command("help_contents", "help", "Help contents",
+                                open_help,
+                                group=30)
+    
\ No newline at end of file
diff --git a/thonny/plugins/help/help.rst b/thonny/plugins/help/help.rst
new file mode 100644
index 0000000..517bd8b
--- /dev/null
+++ b/thonny/plugins/help/help.rst
@@ -0,0 +1,59 @@
+===========
+Thonny help
+===========
+
+
+
+Running programs step-wise
+==========================
+If you want to see how Python executes your program step-by-step then you should run it in *debug-mode*.
+
+Start by selecting *Debug current script* from the *Run* menu or by pressing Ctrl+F5. You'll see that first statement of the program gets highlighted and nothing more happens. In this mode you need to notify Thonny that you're ready to let Python make the next step. For this you have two main options:
+
+* *Run → Step over* (or F6) makes big steps, ie. it executes the highlighted code and highlights the next part of the code.
+* *Run → Step into* (or F7) tries to make smaller steps. If the highlighted code is made of smaller parts (statements or expressions), then first of these gets highlighted and Thonny waits for next command. If you have reached to a program component which doesn't have any sub-parts (eg. variable name) then *Step into* works like *Step over*, ie. executes (or evaluates) the code.
+
+If you have stepped into the depths of a statement or expression and want to move on faster, then you can use *Run → Step out* (or F8), which executes currently highlighted code and all following program parts on the same level.
+
+If you want to reach a specific part of the code, then you can speed up the process by placing your cursor on that line and selecting *Run → Run to cursor*. This makes Thonny automatically step until this line. You can take the command from there.
+
+Installing 3rd party packages
+==============================
+Thonny has two options for installing 3rd party libraries.
+
+With pip-GUI
+-------------
+From "Tools" menu select "Manage packages..." and follow the instructions.
+
+.. image:: https://bitbucket.org/repo/gXnbod/images/2226680569-pipgui_big.png
+   :alt: pipgui_big.png
+
+With pip on command line
+------------------------
+#. From "Tools" menu select "Open system shell...". You should get a new terminal window stating the correct name of *pip* command (usually ``pip`` or ``pip3``). In the following I've assumed the command name is ``pip``.
+#. Enter ``pip install <package name>`` (eg. ``pip install pygame``) and press ENTER. You should see *pip* downloading and installing the package and printing a success message.
+#. Close the terminal (optional)
+#. Return to Thonny
+#. Reset interpreter by selecting "Stop/Reset" from "Run menu" (this is required only first time you do pip install)
+#. Start using the package
+
+.. image:: https://bitbucket.org/repo/gXnbod/images/1183520217-pipinstall_cmdline.png
+   :alt: pipinstall_cmdline.png
+
+
+Using scientific Python packages
+================================
+Python distribution coming with Thonny doesn't contain scientific programming libraries (eg. `NumPy <http://numpy.org/>`_  and `Matplotlib <http://matplotlib.org/>`_). 
+
+Recent versions of most popular scientific Python packages (eg. numpy, pandas and matplotlib) have wheels available for popular platforms so you can most likely install them with pip but in case you have troubles, you could try using Thonny with separate Python distribution meant for scientific computing (eg. `Anaconda <https://www.continuum.io/downloads>`_, `Canopy <https://www.enthought.com/products/canopy/>`_ or `Pyzo <http://www.pyzo.org/>`_).
+
+
+Example: Using Anaconda
+------------------------------------
+Go to https://www.continuum.io/downloads and download a suitable binary distribution for your platform. Most likely you want graphical installer and 64-bit version (you may need 32-bit version if you have very old system). **Note that Thonny supports only on Python 3, so make sure you choose Python 3 version of Anaconda.**
+
+Install it and find out where it puts Python executable (*pythonw.exe* in Windows and *python3* or *python* in Linux and Mac). For example in Windows the full path is by default ``c:\anaconda\pythonw.exe``.
+
+In Thonny open "Tools" menu and select "Options...". In the options dialog open "Intepreter" tab, click "Select executable" and show the location of Anaconda's Python executable.
+
+After you have done this, next time you run your program, it will be run through Anaconda's Python and all the libraries installed there are available.
\ No newline at end of file
diff --git a/thonny/plugins/highlight_names.py b/thonny/plugins/highlight_names.py
new file mode 100644
index 0000000..70878e5
--- /dev/null
+++ b/thonny/plugins/highlight_names.py
@@ -0,0 +1,290 @@
+from jedi import Script
+import thonny.jedi_utils as jedi_utils
+import traceback
+tree = jedi_utils.import_tree()
+    
+from thonny.globals import get_workbench
+import tkinter as tk
+import logging
+
+NAME_CONF = {'background' : '#e6ecfe'}
+
+class BaseNameHighlighter:
+    def __init__(self, text):
+        self.text = text
+        self.text.tag_configure("NAME", NAME_CONF)
+        self.text.tag_raise("sel")
+        self._update_scheduled = False
+    
+    def get_positions_for_script(self, script):
+        raise NotImplementedError();
+        
+    def get_positions(self):
+        index = self.text.index("insert")
+        
+        # ignore if cursor in STRING_OPEN
+        if self.text.tag_prevrange("STRING_OPEN", index):
+            return set()
+
+        source = self.text.get("1.0", "end") 
+        index_parts = index.split('.')
+        line, column = int(index_parts[0]), int(index_parts[1])
+        script = Script(source + ")", line=line, column=column, path="") # https://github.com/davidhalter/jedi/issues/897
+
+        return self.get_positions_for_script(script)
+    
+    def schedule_update(self):
+        def perform_update():
+            try:
+                self.update()
+            finally:
+                self._update_scheduled = False
+        
+        if not self._update_scheduled:
+            self._update_scheduled = True
+            self.text.after_idle(perform_update)
+
+    def update(self):
+        self.text.tag_remove("NAME", "1.0", "end")
+        
+        if get_workbench().get_option("view.name_highlighting"):
+            try:
+                for pos in self.get_positions():
+                    start_index, end_index = pos[0], pos[1]
+                    self.text.tag_add("NAME", start_index, end_index)
+            except:
+                logging.exception("Problem when updating name highlighting")
+
+
+class VariablesHighlighter(BaseNameHighlighter):
+    """This is heavy, but more correct solution for variables, than Script.usages provides 
+    (at least for Jedi 0.10)"""
+    def _is_name_function_call_name(self, name):
+        stmt = name.get_definition()
+        return stmt.type == "power" and stmt.children[0] == name
+
+    def _is_name_function_definition(self, name):
+        scope = name.get_definition()
+        return (isinstance(scope, tree.Function) 
+                and hasattr(scope.children[1], "value")
+                and scope.children[1].value == name.value)
+
+    def _get_def_from_function_params(self, func_node, name):
+        params = jedi_utils.get_params(func_node)
+        for param in params:
+            if param.children[0].value == name.value:
+                return param.children[0]
+        return None
+
+    # copied from jedi's tree.py with a few modifications
+    def _get_statement_for_position(self, node, pos):
+        for c in node.children:
+            # sorted here, because the end_pos property depends on the last child having the last position,
+            # there seems to be a problem with jedi, where the children of a node are not always in the right order
+            if isinstance(c, tree.Class):
+                c.children.sort(key=lambda x: x.end_pos)
+            if c.start_pos <= pos <= c.end_pos:
+                if c.type not in ('decorated', 'simple_stmt', 'suite') \
+                        and not isinstance(c, (tree.Flow, tree.ClassOrFunc)):
+                    return c
+                else:
+                    try:
+                        return jedi_utils.get_statement_of_position(c, pos)
+                    except AttributeError:
+                        traceback.print_exc()
+        return None
+
+    def _is_global_stmt_with_name(self, node, name_str):
+        return isinstance(node, tree.BaseNode) and node.type == "simple_stmt" and \
+               isinstance(node.children[0], tree.GlobalStmt) and \
+               node.children[0].children[1].value == name_str
+
+    def _find_definition(self, scope, name):
+
+        # if the name is the name of a function definition
+        if isinstance(scope, tree.Function):
+            if scope.children[1] == name:
+                return scope.children[1]  # 0th child is keyword "def", 1st is name
+            else:
+                definition = self._get_def_from_function_params(scope, name)
+                if definition:
+                    return definition
+
+        for c in scope.children:
+            if isinstance(c, tree.BaseNode) and c.type == "simple_stmt" and isinstance(c.children[0], tree.ImportName):
+                for n in c.children[0].get_defined_names():
+                    if n.value == name.value:
+                        return n
+                # print(c.path_for_name(name.value))
+            if isinstance(c, tree.Function) and c.children[1].value == name.value and \
+                    not isinstance(jedi_utils.get_parent_scope(c), tree.Class):
+                return c.children[1]
+            if isinstance(c, tree.BaseNode) and c.type == "suite":
+                for x in c.children:
+                    if self._is_global_stmt_with_name(x, name.value):
+                        return self._find_definition(jedi_utils.get_parent_scope(scope), name)
+                    if isinstance(x, tree.Name) and x.is_definition() and x.value == name.value:
+                        return x
+                    def_candidate = self._find_def_in_simple_node(x, name)
+                    if def_candidate:
+                        return def_candidate
+
+        if not isinstance(scope, tree.Module):
+            return self._find_definition(jedi_utils.get_parent_scope(scope), name)
+
+        # if name itself is the left side of an assignment statement, then the name is the definition
+        if name.is_definition():
+            return name
+
+        return None
+
+    def _find_def_in_simple_node(self, node, name):
+        if isinstance(node, tree.Name) and node.is_definition() and node.value == name.value:
+            return name
+        if not isinstance(node, tree.BaseNode):
+            return None
+        for c in node.children:
+            return self._find_def_in_simple_node(c, name)
+
+    def _get_dot_names(self, stmt):
+        try:
+            if stmt.children[1].children[0].value == ".":
+                return stmt.children[0], stmt.children[1].children[1]
+        except:
+            return ()
+        return ()
+
+    def _find_usages(self, name, stmt, module):
+        # check if stmt is dot qualified, disregard these
+        dot_names = self._get_dot_names(stmt)
+        if len(dot_names) > 1 and dot_names[1].value == name.value:
+            return set()
+
+        # search for definition
+        definition = self._find_definition(jedi_utils.get_parent_scope(name), name)
+
+        searched_scopes = set()
+
+        is_function_definition = self._is_name_function_definition(definition) if definition else False
+
+        def find_usages_in_node(node, global_encountered=False):
+            names = []
+            if isinstance(node, tree.BaseNode):
+                if jedi_utils.is_scope(node):
+                    global_encountered = False
+                    if node in searched_scopes:
+                        return names
+                    searched_scopes.add(node)
+                    if isinstance(node, tree.Function):
+                        d = self._get_def_from_function_params(node, name)
+                        if d and d != definition:
+                            return []
+
+                for c in node.children:
+                    dot_names = self._get_dot_names(c)
+                    if len(dot_names) > 1 and dot_names[1].value == name.value:
+                        continue
+                    sub_result = find_usages_in_node(c, global_encountered=global_encountered)
+
+                    if sub_result is None:
+                        if not jedi_utils.is_scope(node):
+                            return None if definition and node != jedi_utils.get_parent_scope(definition) else [definition]
+                        else:
+                            sub_result = []
+                    names.extend(sub_result)
+                    if self._is_global_stmt_with_name(c, name.value):
+                        global_encountered = True
+            elif isinstance(node, tree.Name) and node.value == name.value:
+                if definition and definition != node:
+                    if self._is_name_function_definition(node):
+                        if isinstance(jedi_utils.get_parent_scope(jedi_utils.get_parent_scope(node)), tree.Class):
+                            return []
+                        else:
+                            return None
+                    if node.is_definition() and not global_encountered and \
+                            (is_function_definition or jedi_utils.get_parent_scope(node) != jedi_utils.get_parent_scope(definition)):
+                            return None
+                    if self._is_name_function_definition(definition) and \
+                            isinstance(jedi_utils.get_parent_scope(jedi_utils.get_parent_scope(definition)), tree.Class):
+                        return None
+                names.append(node)
+            return names
+
+        if definition:
+            if self._is_name_function_definition(definition):
+                scope = jedi_utils.get_parent_scope(jedi_utils.get_parent_scope(definition))
+            else:
+                scope = jedi_utils.get_parent_scope(definition)
+        else:
+            scope = jedi_utils.get_parent_scope(name)
+
+        usages = find_usages_in_node(scope)
+        return usages
+    
+    def get_positions_for_script(self, script):
+        name = None
+        module_node = jedi_utils.get_module_node(script)
+        stmt = self._get_statement_for_position(module_node, script._pos)
+
+        if isinstance(stmt, tree.Name):
+            name = stmt
+        elif isinstance(stmt, tree.BaseNode):
+            name = jedi_utils.get_name_of_position(stmt, script._pos)
+
+        if not name:
+            return set()
+
+        # format usage positions as tkinter text widget indices
+        return set(("%d.%d" % (usage.start_pos[0], usage.start_pos[1]),
+                      "%d.%d" % (usage.start_pos[0], usage.start_pos[1] + len(name.value)))
+                        for usage in self._find_usages(name, stmt, module_node))
+
+class UsagesHighlighter(BaseNameHighlighter):
+    """Script.usages looks tempting method to use for finding variable ocurrences,
+    but it only returns last
+    assignments to a variable, not really all usages (with Jedi 0.10).
+    But it finds attribute usages quite nicely.
+    
+    TODO: check if this gets fixed in later versions of Jedi"""
+    
+    def get_positions_for_script(self, script):
+        usages = script.usages()
+        
+        result = {("%d.%d" % (usage.line, usage.column),
+                  "%d.%d" % (usage.line, usage.column + len(usage.name)))
+                for usage in usages if usage.module_name == ""}
+        
+        return result
+        
+
+class CombinedHighlighter(VariablesHighlighter, UsagesHighlighter):
+    def get_positions_for_script(self, script):
+        usages = UsagesHighlighter.get_positions_for_script(self, script)
+        variables = VariablesHighlighter.get_positions_for_script(self, script) 
+        return usages | variables
+
+def update_highlighting(event):
+    assert isinstance(event.widget, tk.Text)
+    text = event.widget
+    
+    if not hasattr(text, "name_highlighter"):
+        text.name_highlighter = VariablesHighlighter(text)
+        # Alternatives:
+        # NB! usages() is too slow when used on library names 
+        #text.name_highlighter = CombinedHighlighter(text)
+        #text.name_highlighter = UsagesHighlighter(text)
+        
+    text.name_highlighter.schedule_update()
+
+
+def load_plugin():
+    if jedi_utils.get_version_tuple() < (0, 9):
+        logging.warning("Jedi version is too old. Disabling name highlighter")
+        return
+     
+    wb = get_workbench()  # type:Workbench
+    wb.set_default("view.name_highlighting", False)
+    wb.bind_class("CodeViewText", "<<CursorMove>>", update_highlighting, True)
+    wb.bind_class("CodeViewText", "<<TextChange>>", update_highlighting, True)
+    wb.bind("<<UpdateAppearance>>", update_highlighting, True)
+    
\ No newline at end of file
diff --git a/thonny/plugins/interpreter_config_page.py b/thonny/plugins/interpreter_config_page.py
new file mode 100644
index 0000000..e96a196
--- /dev/null
+++ b/thonny/plugins/interpreter_config_page.py
@@ -0,0 +1,85 @@
+import tkinter as tk
+import os.path
+from tkinter import filedialog
+from tkinter import ttk
+
+from thonny.config_ui import ConfigurationPage
+from thonny.globals import get_workbench, get_runner
+from thonny.ui_utils import create_string_var
+from thonny.misc_utils import running_on_windows
+from thonny.running import parse_configuration
+
+
+class InterpreterConfigurationPage(ConfigurationPage):
+    
+    def __init__(self, master):
+        ConfigurationPage.__init__(self, master)
+        
+        self._configuration_variable = create_string_var(
+            get_workbench().get_option("run.backend_configuration"))
+        
+        entry_label = ttk.Label(self, text="Which interpreter to use for running programs?")
+        entry_label.grid(row=0, column=0, columnspan=2, sticky=tk.W)
+        
+        self._entry = ttk.Combobox(self,
+                              exportselection=False,
+                              textvariable=self._configuration_variable,
+                              values=self._get_configurations())
+        
+        self._entry.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW)
+        self._entry.state(['!disabled', 'readonly'])
+        
+        another_label = ttk.Label(self, text="Your interpreter isn't in the list?")
+        another_label.grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(10,0))
+        self._select_button = ttk.Button(self,
+                                         text="Locate another executable "
+                                         + ("(python.exe) ..." if running_on_windows() else "(python3) ...")
+                                         + "\nNB! Thonny only supports Python 3.4 and later",
+                                         command=self._select_executable)
+        
+        self._select_button.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW)
+        
+        self.columnconfigure(0, weight=1)
+        self.columnconfigure(1, weight=1)
+    
+    def _get_configurations(self):
+        result = []
+        
+        backends = get_workbench().get_backends()
+        for backend_name in sorted(backends.keys()):
+            backend_class = backends[backend_name]
+            for configuration_option in backend_class.get_configuration_options():
+                if configuration_option is None or configuration_option == "":
+                    result.append(backend_name)
+                else:
+                    result.append("%s (%s)" % (backend_name, configuration_option))
+        
+        return result
+    
+    def _select_executable(self):
+        if running_on_windows():
+            options = {"filetypes" : [('Exe-files', '.exe'), ('all files', '.*')]}
+        else:
+            options = {}
+
+        # TODO: get dir of current interpreter
+            
+        filename = filedialog.askopenfilename(**options)
+        
+        if filename:
+            self._configuration_variable.set("Python (%s)" % os.path.realpath(filename))
+    
+    
+    def apply(self):
+        if not self._configuration_variable.modified:
+            return
+        
+        configuration = self._configuration_variable.get()
+        get_workbench().set_option("run.backend_configuration", configuration)
+        
+        get_runner().reset_backend()
+        
+    
+
+def load_plugin():
+    get_workbench().add_configuration_page("Interpreter", InterpreterConfigurationPage)
diff --git a/thonny/plugins/locals_marker.py b/thonny/plugins/locals_marker.py
new file mode 100644
index 0000000..ea6a1c2
--- /dev/null
+++ b/thonny/plugins/locals_marker.py
@@ -0,0 +1,149 @@
+import tkinter as tk
+from thonny.globals import get_workbench
+import logging
+import thonny.jedi_utils as jedi_utils
+
+class LocalsHighlighter:
+
+    def __init__(self, text, local_variable_font=None):
+        self.text = text
+        
+        if local_variable_font:
+            self.local_variable_font=local_variable_font
+        else:
+            self.local_variable_font = self.text["font"]
+        
+        self._configure_tags()
+        self._update_scheduled = False
+    
+    def get_positions(self):
+        return self._get_positions_correct_but_using_private_parts()
+    
+    def _get_positions_simple_but_incorrect(self):
+        # goto_assignments only gives you last assignment to given node
+        import jedi
+        defs = jedi.names(self.text.get('1.0', 'end'), path="",
+                           all_scopes=True, definitions=True, references=True)
+        result = set()
+        for definition in defs:
+            if definition.parent().type == "function": # is located in a function
+                ass = definition.goto_assignments()
+                if len(ass) > 0 and ass[0].parent().type == "function": # is assigned to in a function
+                    pos = ("%d.%d" % (definition.line, definition.column),
+                           "%d.%d" % (definition.line, definition.column+len(definition.name)))
+                    result.add(pos)
+        return result
+        
+    
+    def _get_positions_correct_but_using_private_parts(self):
+        from jedi import Script
+        
+        tree = jedi_utils.import_tree()
+
+        locs = []
+
+        def process_scope(scope):
+            if isinstance(scope, tree.Function):
+                # process all children after name node,
+                # (otherwise name of global function will be marked as local def)
+                local_names = set() 
+                global_names = set()
+                for child in scope.children[2:]:
+                    process_node(child, local_names, global_names)
+            else:
+                for child in scope.subscopes:
+                    process_scope(child)
+        
+        def process_node(node, local_names, global_names):
+            if isinstance(node, tree.GlobalStmt):
+                global_names.update([n.value for n in node.get_global_names()])
+                
+            elif isinstance(node, tree.Name):
+                if node.value in global_names:
+                    return
+                
+                if node.is_definition(): # local def
+                    locs.append(node)
+                    local_names.add(node.value)
+                elif node.value in local_names: # use of local 
+                    locs.append(node)
+                    
+            elif isinstance(node, tree.BaseNode):
+                # ref: jedi/parser/grammar*.txt
+                if node.type == "trailer" and node.children[0].value == ".":
+                    # this is attribute
+                    return
+                
+                if isinstance(node, tree.Function):
+                    global_names = set() # outer global statement doesn't have effect anymore
+                
+                for child in node.children:
+                    process_node(child, local_names, global_names)
+
+        source = self.text.get('1.0', 'end')
+        script = Script(source + ")") # https://github.com/davidhalter/jedi/issues/897
+        module = jedi_utils.get_module_node(script)
+        for child in module.children:
+            if isinstance(child, tree.BaseNode) and jedi_utils.is_scope(child):
+                process_scope(child)
+
+        loc_pos = set(("%d.%d" % (usage.start_pos[0], usage.start_pos[1]),
+                "%d.%d" % (usage.start_pos[0], usage.start_pos[1] + len(usage.value)))
+                for usage in locs)
+
+        return loc_pos
+
+    def _configure_tags(self):
+        self.text.tag_configure("LOCAL_NAME",
+                                font=self.local_variable_font, 
+                                foreground="#000055")
+        self.text.tag_raise("sel")
+        
+    def _highlight(self, pos_info):
+        for pos in pos_info:
+            start_index, end_index = pos[0], pos[1]
+            self.text.tag_add("LOCAL_NAME", start_index, end_index)
+
+    def schedule_update(self):
+        def perform_update():
+            try:
+                self.update()
+            finally:
+                self._update_scheduled = False
+        
+        if not self._update_scheduled:
+            self._update_scheduled = True
+            self.text.after_idle(perform_update)
+            
+    def update(self):
+        self.text.tag_remove("LOCAL_NAME", "1.0", "end")
+        
+        if get_workbench().get_option("view.locals_highlighting"):
+            try:
+                highlight_positions = self.get_positions()
+                self._highlight(highlight_positions)
+            except:
+                logging.exception("Problem when updating local variable tags")
+
+
+def update_highlighting(event):
+    assert isinstance(event.widget, tk.Text)
+    text = event.widget
+    
+    if not hasattr(text, "local_highlighter"):
+        text.local_highlighter = LocalsHighlighter(text,
+            get_workbench().get_font("ItalicEditorFont"))
+        
+    text.local_highlighter.schedule_update()
+
+
+def load_plugin():
+    if jedi_utils.get_version_tuple() < (0, 9):
+        logging.warning("Jedi version is too old. Disabling locals marker")
+        return 
+    
+    wb = get_workbench()
+    wb.set_default("view.locals_highlighting", False)
+    wb.bind_class("CodeViewText", "<<TextChange>>", update_highlighting, True)
+    wb.bind("<<UpdateAppearance>>", update_highlighting, True)
+    
diff --git a/thonny/plugins/main_file_browser.py b/thonny/plugins/main_file_browser.py
new file mode 100644
index 0000000..57114f2
--- /dev/null
+++ b/thonny/plugins/main_file_browser.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+
+import os
+import tkinter as tk
+from tkinter.messagebox import showerror
+
+#from thonny.ui_utils import askstring TODO: doesn't work
+from tkinter.simpledialog import askstring
+
+from thonny import misc_utils
+from thonny.globals import get_workbench
+from thonny.base_file_browser import BaseFileBrowser
+
+        
+class MainFileBrowser(BaseFileBrowser):
+    def __init__(self, master, show_hidden_files=False):
+        BaseFileBrowser.__init__(self, master, show_hidden_files, "file.last_browser_folder")
+        
+        self.menu = tk.Menu(tk._default_root, tearoff=False)
+        self.menu.add_command(label="Create new file", command=self.create_new_file)
+        
+        self.tree.bind('<3>', self.on_secondary_click, True)
+        if misc_utils.running_on_mac_os():
+            self.tree.bind('<2>', self.on_secondary_click, True)
+            self.tree.bind('<Control-1>', self.on_secondary_click, True)
+    
+                
+    def create_new_file(self):
+        selected_path = self.get_selected_path()
+        
+        if not selected_path:
+            return
+        
+        if os.path.isdir(selected_path):
+            parent_path = selected_path
+        else:
+            parent_path = os.path.dirname(selected_path)
+        
+        
+        initial_name = self.get_proposed_new_file_name(parent_path, ".py")
+        name = askstring("File name",
+                        "Provide filename",
+                        initialvalue=initial_name,
+                        #selection_range=(0, len(initial_name)-3)
+                        )
+        
+        if not name:
+            return
+                        
+        path = os.path.join(parent_path, name)
+        
+        if os.path.exists(path):
+            showerror("Error", "The file '"+path+"' already exists")
+        else:
+            open(path, 'w').close()
+        
+        self.open_path_in_browser(path, True)
+        get_workbench().get_editor_notebook().show_file(path)
+
+    def get_proposed_new_file_name(self, folder, extension):
+        base = "new_file"
+        
+        if os.path.exists(os.path.join(folder, base + extension)):
+            i = 2
+            
+            while True:
+                name = base + "_" + str(i) + extension
+                path = os.path.join(folder, name)
+                if os.path.exists(path):
+                    i += 1
+                else:
+                    return name
+        else:
+            return base + extension
+         
+    def on_secondary_click(self, event):
+        node_id = self.tree.identify_row(event.y)
+        
+        if node_id:
+            self.tree.selection_set(node_id)
+            self.tree.focus(node_id)
+            self.menu.tk_popup(event.x_root, event.y_root)
+    
+    def on_double_click(self, event):
+        path = self.get_selected_path()
+        if os.path.isfile(path):
+            get_workbench().get_editor_notebook().show_file(path)
+            self.save_current_folder()
+        elif os.path.isdir(path):
+            self.refresh_tree(self.get_selected_node(), True)
+    
+    
+    
+def load_plugin(): 
+    get_workbench().set_default("file.last_browser_folder", None)
+    get_workbench().add_view(MainFileBrowser, "Files", "nw")
diff --git a/thonny/plugins/object_inspector.py b/thonny/plugins/object_inspector.py
new file mode 100644
index 0000000..4ca0d32
--- /dev/null
+++ b/thonny/plugins/object_inspector.py
@@ -0,0 +1,468 @@
+# -*- coding: utf-8 -*-
+
+import tkinter as tk
+
+from thonny.memory import format_object_id, VariablesFrame, MemoryFrame,\
+    MAX_REPR_LENGTH_IN_GRID
+from thonny.misc_utils import shorten_repr
+from thonny.ui_utils import ScrollableFrame, CALM_WHITE, update_entry_text
+from thonny.tktextext import TextFrame
+from thonny.common import InlineCommand
+import ast
+from thonny.globals import get_workbench, get_runner
+from logging import exception
+
+
+
+class AttributesFrame(VariablesFrame):
+    def __init__(self, master):
+        VariablesFrame.__init__(self, master)
+        self.configure(border=1)
+        self.vert_scrollbar.grid_remove()
+       
+    def on_select(self, event):
+        pass
+    
+    def on_double_click(self, event):
+        self.show_selected_object_info()
+    
+
+
+        
+    
+
+class ObjectInspector(ScrollableFrame):
+    def __init__(self, master):
+        
+        ScrollableFrame.__init__(self, master)
+        
+        self.object_id = None
+        self.object_info = None
+        get_workbench().bind("ObjectSelect", self.show_object, True)
+        
+        self.grid_frame = tk.Frame(self.interior, bg=CALM_WHITE) 
+        self.grid_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=(10,0), pady=15)
+        self.grid_frame.columnconfigure(1, weight=1)
+        
+        def _add_main_attribute(row, caption):
+            label = tk.Label(self.grid_frame, text=caption + ":  ",
+                             background=CALM_WHITE,
+                             justify=tk.LEFT)
+            label.grid(row=row, column=0, sticky=tk.NW)
+            
+            value = tk.Entry(self.grid_frame,
+                             background=CALM_WHITE,
+                             bd=0,
+                             readonlybackground=CALM_WHITE,
+                             highlightthickness = 0,
+                             state="readonly"
+                             )
+            if row > 0:
+                value.grid(row=row, column=1, columnspan=3, 
+                       sticky=tk.NSEW, pady=2)
+            else:
+                value.grid(row=row, column=1, sticky=tk.NSEW, pady=2)
+            return value
+        
+        self.id_entry   = _add_main_attribute(0, "id")
+        self.repr_entry = _add_main_attribute(1, "repr")
+        self.type_entry = _add_main_attribute(2, "type")
+        self.type_entry.config(cursor="hand2", fg="dark blue")
+        self.type_entry.bind("<Button-1>", self.goto_type)
+        
+        self._add_block_label(5, "Attributes")
+        self.attributes_frame = AttributesFrame(self.grid_frame)
+        self.attributes_frame.grid(row=6, column=0, columnspan=4, sticky=tk.NSEW, padx=(0,10))
+        
+        self.grid_frame.grid_remove()
+        
+        # navigation 
+        self.back_label = self.create_navigation_link(2, " << ", self.navigate_back)
+        self.forward_label = self.create_navigation_link(3, " >> ", self.navigate_forward, (0,10))
+        self.back_links = []
+        self.forward_links = []
+        
+        # type-specific inspectors
+        self.current_type_specific_inspector = None
+        self.current_type_specific_label = None
+        self.type_specific_inspectors = [ 
+            FileHandleInspector(self.grid_frame),
+            FunctionInspector(self.grid_frame),
+            StringInspector(self.grid_frame),
+            ElementsInspector(self.grid_frame),
+            DictInspector(self.grid_frame),
+        ]
+
+        get_workbench().bind("ObjectInfo", self._handle_object_info_event, True)
+        get_workbench().bind("DebuggerProgress", self._handle_progress_event, True)
+        get_workbench().bind("ToplevelResult", self._handle_progress_event, True)
+
+
+    
+    def create_navigation_link(self, col, text, action, padx=0):
+        link = tk.Label(self.grid_frame,
+                        text=text,
+                        background=CALM_WHITE,
+                        foreground="blue",
+                        cursor="hand2")
+        link.grid(row=0, column=col, sticky=tk.NE, padx=padx)
+        link.bind("<Button-1>", action)
+        return link
+    
+    def navigate_back(self, event):
+        if len(self.back_links) == 0:
+            return
+        
+        self.forward_links.append(self.object_id)
+        self._show_object_by_id(self.back_links.pop(), True)
+    
+    def navigate_forward(self, event):
+        if len(self.forward_links) == 0:
+            return
+    
+        self.back_links.append(self.object_id)
+        self._show_object_by_id(self.forward_links.pop(), True)
+
+    def show_object(self, event):
+        self._show_object_by_id(event.object_id)
+        
+    def _show_object_by_id(self, object_id, via_navigation=False):
+        
+        if self.winfo_ismapped() and self.object_id != object_id:
+            if not via_navigation and self.object_id is not None:
+                if self.object_id in self.back_links:
+                    self.back_links.remove(self.object_id)
+                self.back_links.append(self.object_id)
+                del self.forward_links[:]
+                
+            self.object_id = object_id
+            update_entry_text(self.id_entry, format_object_id(object_id))
+            self.set_object_info(None)
+            self.request_object_info()
+    
+    def _handle_object_info_event(self, msg):
+        if self.winfo_ismapped():
+            if msg.info["id"] == self.object_id:
+                if hasattr(msg, "not_found") and msg.not_found:
+                    self.object_id = None
+                    self.set_object_info(None)
+                else:
+                    self.set_object_info(msg.info)
+    
+    def _handle_progress_event(self, event):
+        if self.object_id is not None:
+            self.request_object_info()
+                
+                
+    def request_object_info(self): 
+        get_runner().send_command(InlineCommand("get_object_info",
+                                            object_id=self.object_id,
+                                            all_attributes=False)) 
+                    
+    def set_object_info(self, object_info):
+        self.object_info = object_info
+        if object_info is None:
+            update_entry_text(self.repr_entry, "")
+            update_entry_text(self.type_entry, "")
+            self.grid_frame.grid_remove()
+        else:
+            update_entry_text(self.repr_entry, object_info["repr"])
+            update_entry_text(self.type_entry, object_info["type"])
+            self.attributes_frame.tree.configure(height=len(object_info["attributes"]))
+            self.attributes_frame.update_variables(object_info["attributes"])
+            self.update_type_specific_info(object_info)
+                
+            
+            # update layout
+            self._expose(None)
+            if not self.grid_frame.winfo_ismapped():
+                self.grid_frame.grid()
+    
+        if self.back_links == []:
+            self.back_label.config(foreground="lightgray", cursor="arrow")
+        else:
+            self.back_label.config(foreground="blue", cursor="hand2")
+    
+        if self.forward_links == []:
+            self.forward_label.config(foreground="lightgray", cursor="arrow")
+        else:
+            self.forward_label.config(foreground="blue", cursor="hand2")
+    
+    def update_type_specific_info(self, object_info):
+        type_specific_inspector = None
+        for insp in self.type_specific_inspectors:
+            if insp.applies_to(object_info):
+                type_specific_inspector = insp
+                break
+        
+        if type_specific_inspector != self.current_type_specific_inspector:
+            if self.current_type_specific_inspector is not None:
+                self.current_type_specific_inspector.grid_remove() # TODO: or forget?
+                self.current_type_specific_label.destroy()
+                self.current_type_specific_inspector = None
+                self.current_type_specific_label = None
+                
+            if type_specific_inspector is not None:
+                self.current_type_specific_label = self._add_block_label (3, "")
+                
+                type_specific_inspector.grid(row=4, 
+                                             column=0, 
+                                             columnspan=4, 
+                                             sticky=tk.NSEW,
+                                             padx=(0,10))
+                
+            self.current_type_specific_inspector = type_specific_inspector
+        
+        if self.current_type_specific_inspector is not None:
+            self.current_type_specific_inspector.set_object_info(object_info,
+                                                             self.current_type_specific_label)
+    
+    def goto_type(self, event):
+        if self.object_info is not None:
+            get_workbench().event_generate("ObjectSelect", object_id=self.object_info["type_id"])
+    
+    
+    
+    def _add_block_label(self, row, caption):
+        label = tk.Label(self.grid_frame, bg=CALM_WHITE, text=caption)
+        label.grid(row=row, column=0, columnspan=4, sticky="nsew", pady=(20,0))
+        return label
+            
+
+class TypeSpecificInspector:
+    def __init__(self, master):
+        pass
+    
+    def set_object_info(self, object_info, label):
+        pass
+    
+    def applies_to(self, object_info):
+        return False
+    
+class FileHandleInspector(TextFrame, TypeSpecificInspector):
+    
+    def __init__(self, master):
+        TypeSpecificInspector.__init__(self, master)
+        TextFrame.__init__(self, master, read_only=True)
+        self.cache = {} # stores file contents for handle id-s
+        self.config(borderwidth=1)
+        self.text.configure(background="white")
+        self.text.tag_configure("read", foreground="lightgray")
+    
+    def applies_to(self, object_info):
+        return ("file_content" in object_info
+                or "file_error" in object_info)
+    
+    def set_object_info(self, object_info, label):
+        
+        if "file_content" not in object_info:
+            exception("File error: " + object_info["file_error"])
+            return
+        
+        assert "file_content" in object_info
+        content = object_info["file_content"]
+        line_count_sep = len(content.split("\n"))
+        line_count_term = len(content.splitlines())
+        char_count = len(content)
+        self.text.configure(height=min(line_count_sep, 10))
+        self.text.set_content(content)
+        
+        assert "file_tell" in object_info
+        # f.tell() gives num of bytes read (minus some magic with linebreaks)
+        
+        file_bytes = content.encode(encoding=object_info["file_encoding"])
+        bytes_read = file_bytes[0:object_info["file_tell"]]
+        read_content = bytes_read.decode(encoding=object_info["file_encoding"])
+        read_char_count = len(read_content)
+        read_line_count_term = (len(content.splitlines())
+                                - len(content[read_char_count:].splitlines()))
+        
+        pos_index = "1.0+" + str(read_char_count) + "c"
+        self.text.tag_add("read", "1.0", pos_index)
+        self.text.see(pos_index)
+        
+        label.configure(text="Read %d/%d %s, %d/%d %s" 
+                        % (read_char_count,
+                           char_count,
+                           "symbol" if char_count == 1 else "symbols",  
+                           read_line_count_term,
+                           line_count_term,
+                           "line" if line_count_term == 1 else "lines"))
+            
+            
+            
+class FunctionInspector(TextFrame, TypeSpecificInspector):
+    
+    def __init__(self, master):
+        TypeSpecificInspector.__init__(self, master)
+        TextFrame.__init__(self, master, read_only=True)
+        self.config(borderwidth=1)
+        self.text.configure(background="white")
+
+    def applies_to(self, object_info):
+        return "source" in object_info
+    
+    def set_object_info(self, object_info, label):
+        line_count = len(object_info["source"].split("\n"))
+        self.text.configure(height=min(line_count, 15))
+        self.text.set_content(object_info["source"])
+        label.configure(text="Code")
+                
+            
+class StringInspector(TextFrame, TypeSpecificInspector):
+    
+    def __init__(self, master):
+        TypeSpecificInspector.__init__(self, master)
+        TextFrame.__init__(self, master, read_only=True)
+        self.config(borderwidth=1)
+        self.text.configure(background="white")
+
+    def applies_to(self, object_info):
+        return object_info["type"] == repr(str)
+    
+    def set_object_info(self, object_info, label):
+        # TODO: don't show too big string
+        content = ast.literal_eval(object_info["repr"])
+        line_count_sep = len(content.split("\n"))
+        line_count_term = len(content.splitlines())
+        self.text.configure(height=min(line_count_sep, 10))
+        self.text.set_content(content)
+        label.configure(text="%d %s, %d %s" 
+                        % (len(content),
+                           "symbol" if len(content) == 1 else "symbols",
+                           line_count_term, 
+                           "line" if line_count_term == 1 else "lines"))
+        
+
+class ElementsInspector(MemoryFrame, TypeSpecificInspector):
+    def __init__(self, master):
+        TypeSpecificInspector.__init__(self, master)
+        MemoryFrame.__init__(self, master, ('index', 'id', 'value'))
+        self.configure(border=1)
+        
+        #self.vert_scrollbar.grid_remove()
+        self.tree.column('index', width=40, anchor=tk.W, stretch=False)
+        self.tree.column('id', width=750, anchor=tk.W, stretch=True)
+        self.tree.column('value', width=750, anchor=tk.W, stretch=True)
+        
+        self.tree.heading('index', text='Index', anchor=tk.W) 
+        self.tree.heading('id', text='Value ID', anchor=tk.W)
+        self.tree.heading('value', text='Value', anchor=tk.W)
+    
+        self.elements_have_indices = None
+        self.update_memory_model()
+
+        get_workbench().bind("ShowView", self.update_memory_model, True)
+        get_workbench().bind("HideView", self.update_memory_model, True)
+        
+        
+    def update_memory_model(self, event=None):
+        self._update_columns()
+        
+    def _update_columns(self):
+        if get_workbench().in_heap_mode():
+            if self.elements_have_indices:
+                self.tree.configure(displaycolumns=("index", "id"))
+            else:
+                self.tree.configure(displaycolumns=("id",))
+        else:
+            if self.elements_have_indices:
+                self.tree.configure(displaycolumns=("index", "value"))
+            else:
+                self.tree.configure(displaycolumns=("value"))
+
+    def applies_to(self, object_info):
+        return "elements" in object_info
+    
+    def on_select(self, event):
+        pass
+    
+    def on_double_click(self, event):
+        self.show_selected_object_info()
+    
+    def set_object_info(self, object_info, label):
+        assert "elements" in object_info
+        
+        self.elements_have_indices = object_info["type"] in (repr(tuple), repr(list))
+        self._update_columns()
+        
+        self._clear_tree()
+        index = 0
+        # TODO: don't show too big number of elements
+        for element in object_info["elements"]:
+            node_id = self.tree.insert("", "end")
+            if self.elements_have_indices:
+                self.tree.set(node_id, "index", index)
+            else:
+                self.tree.set(node_id, "index", "")
+                
+            self.tree.set(node_id, "id", format_object_id(element["id"]))
+            self.tree.set(node_id, "value", shorten_repr(element["repr"], MAX_REPR_LENGTH_IN_GRID))
+            index += 1
+
+        count = len(object_info["elements"])
+        self.tree.config(height=min(count,10))
+        
+        
+        label.configure (
+            text=("%d element" if count == 1 else "%d elements") % count
+        ) 
+        
+
+class DictInspector(MemoryFrame, TypeSpecificInspector):
+    def __init__(self, master):
+        TypeSpecificInspector.__init__(self, master)
+        MemoryFrame.__init__(self, master, ('key_id', 'id', 'key', 'value'))
+        self.configure(border=1)
+        #self.vert_scrollbar.grid_remove()
+        self.tree.column('key_id', width=100, anchor=tk.W, stretch=False)
+        self.tree.column('key', width=100, anchor=tk.W, stretch=False)
+        self.tree.column('id', width=750, anchor=tk.W, stretch=True)
+        self.tree.column('value', width=750, anchor=tk.W, stretch=True)
+        
+        self.tree.heading('key_id', text='Key ID', anchor=tk.W) 
+        self.tree.heading('key', text='Key', anchor=tk.W) 
+        self.tree.heading('id', text='Value ID', anchor=tk.W)
+        self.tree.heading('value', text='Value', anchor=tk.W)
+    
+        self.update_memory_model()
+        
+    def update_memory_model(self, event=None):
+        if get_workbench().in_heap_mode():
+            self.tree.configure(displaycolumns=("key_id", "id"))
+        else:
+            self.tree.configure(displaycolumns=("key", "value"))
+
+    def applies_to(self, object_info):
+        return "entries" in object_info
+
+    def on_select(self, event):
+        pass
+    
+    def on_double_click(self, event):
+        # NB! this selects value
+        self.show_selected_object_info()
+
+    def set_object_info(self, object_info, label):
+        assert "entries" in object_info
+        
+        self._clear_tree()
+        # TODO: don't show too big number of elements
+        for key, value in object_info["entries"]:
+            node_id = self.tree.insert("", "end")
+            self.tree.set(node_id, "key_id", format_object_id(key["id"]))
+            self.tree.set(node_id, "key", shorten_repr(key["repr"], MAX_REPR_LENGTH_IN_GRID))
+            self.tree.set(node_id, "id", format_object_id(value["id"]))
+            self.tree.set(node_id, "value", shorten_repr(value["repr"], MAX_REPR_LENGTH_IN_GRID))
+
+        count = len(object_info["entries"])
+        self.tree.config(height=min(count,10))
+        
+        label.configure (
+            text=("%d entry" if count == 1 else "%d entries") % count
+        ) 
+        
+        self.update_memory_model()
+
+def load_plugin():
+    get_workbench().add_view(ObjectInspector, "Object inspector", "se")
\ No newline at end of file
diff --git a/thonny/plugins/outline.py b/thonny/plugins/outline.py
new file mode 100644
index 0000000..a5b273e
--- /dev/null
+++ b/thonny/plugins/outline.py
@@ -0,0 +1,125 @@
+import re
+import tkinter as tk
+from tkinter import ttk
+from thonny.globals import get_workbench
+from thonny.ui_utils import SafeScrollbar
+
+class OutlineView(ttk.Frame):
+    def __init__(self, master):
+        ttk.Frame.__init__(self, master)
+        self._init_widgets()
+        
+        self._tab_changed_binding = get_workbench().get_editor_notebook().bind("<<NotebookTabChanged>>", self._update_frame_contents ,True)
+        get_workbench().bind("Save", self._update_frame_contents, True)
+        get_workbench().bind("SaveAs", self._update_frame_contents, True)
+        get_workbench().bind_class("Text", "<<NewLine>>", self._update_frame_contents, True)
+        
+        self._update_frame_contents()
+    
+    def destroy(self):
+        try:
+            # Not sure if editor notebook is still living
+            get_workbench().get_editor_notebook().unbind("<<NotebookTabChanged>>", self._tab_changed_binding)
+        except:
+            pass
+        self.vert_scrollbar["command"] = None
+        ttk.Frame.destroy(self)
+    
+    def _init_widgets(self):
+        #init and place scrollbar
+        self.vert_scrollbar = SafeScrollbar(self, orient=tk.VERTICAL)
+        self.vert_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
+
+        #init and place tree
+        self.tree = ttk.Treeview(self, yscrollcommand=self.vert_scrollbar.set)
+        self.tree.grid(row=0, column=0, sticky=tk.NSEW)
+        self.vert_scrollbar['command'] = self.tree.yview
+
+        #set single-cell frame
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+
+        #init tree events
+        self.tree.bind("<Double-Button-1>", self._on_double_click, "+")
+
+        #configure the only tree column
+        self.tree.column('#0', anchor=tk.W, stretch=True)
+        # self.tree.heading('#0', text='Item (type @ line)', anchor=tk.W)
+        self.tree['show'] = ('tree',)
+        
+        self._class_img = get_workbench().get_image("class.gif")
+        self._method_img = get_workbench().get_image("method.gif")
+
+    def _update_frame_contents(self, event=None):
+        self._clear_tree()
+        
+        editor = get_workbench().get_editor_notebook().get_current_editor()
+        if editor is None:
+            return
+        
+        root = self._parse_source(editor.get_code_view().get_content())
+        for child in root[2]:
+            self._add_item_to_tree('', child)
+    
+    def _parse_source(self, source):
+        #all nodes in format (parent, node_indent, node_children, name, type, linenumber)
+        root_node = (None, 0, [], None, None, None) #name, type and linenumber not needed for root
+        active_node = root_node
+
+        lineno = 0
+        for line in source.split('\n'):
+            lineno += 1
+            m = re.match('[ ]*[\w]{1}', line)
+            if m:
+                indent = len(m.group(0))
+                while indent <= active_node[1]:
+                    active_node = active_node[0]
+
+                t = re.match('[ ]*(?P<type>(def|class){1})[ ]+(?P<name>[\w]+)', line)
+                if t:
+                    current = (active_node, indent, [], t.group('name'), t.group('type'), lineno)
+                    active_node[2].append(current)
+                    active_node = current
+        
+        return root_node
+
+
+    #adds a single item to the tree, recursively calls itself to add any child nodes
+    def _add_item_to_tree(self, parent, item):
+        #create the text to be played for this item
+        item_type = item[4]
+        item_text = " " + item[3]
+        
+        if item_type == "class":
+            image = self._class_img
+        elif item_type == "def":
+            image = self._method_img
+        else:
+            image = None
+        
+        #insert the item, set lineno as a 'hidden' value
+        current = self.tree.insert(parent, 'end', text=item_text, values = item[5], image=image)
+
+        for child in item[2]:
+            self._add_item_to_tree(current, child)
+        
+    #clears the tree by deleting all items      
+    def _clear_tree(self):
+        for child_id in self.tree.get_children():
+            self.tree.delete(child_id)
+
+    #called when a double-click is performed on any items
+    def _on_double_click(self, event):
+        editor = get_workbench().get_editor_notebook().get_current_editor()
+        if editor:
+            code_view = editor.get_code_view() 
+            lineno = self.tree.item(self.tree.focus())['values'][0]
+            index = code_view.text.index(str(lineno) + '.0')
+            code_view.text.see(index) #make sure that the double-clicked item is visible
+            code_view.select_lines(lineno, lineno)
+            
+            get_workbench().event_generate("OutlineDoubleClick",
+                item_text=self.tree.item(self.tree.focus(), option='text'))
+
+def load_plugin(): 
+    get_workbench().add_view(OutlineView, "Outline", "ne")
diff --git a/thonny/plugins/paren_matcher.py b/thonny/plugins/paren_matcher.py
new file mode 100644
index 0000000..447e9c6
--- /dev/null
+++ b/thonny/plugins/paren_matcher.py
@@ -0,0 +1,155 @@
+from thonny.globals import get_workbench
+import tokenize
+import io
+from thonny.codeview import CodeViewText
+from thonny.shell import ShellText
+
+
+_OPENERS = {')': '(', ']': '[', '}': '{'}
+
+class ParenMatcher:
+
+    def __init__(self, text, paren_highlight_font=None):
+        self.text = text
+        if paren_highlight_font:
+            self._paren_highlight_font = paren_highlight_font
+        else:
+            self._paren_highlight_font = self.text["font"]    
+        self._configure_tags()
+        self._update_scheduled = False
+    
+    def schedule_update(self):
+        def perform_update():
+            try:
+                self.update_highlighting()
+            finally:
+                self._update_scheduled = False
+        
+        if not self._update_scheduled:
+            self._update_scheduled = True
+            self.text.after_idle(perform_update)
+
+    def update_highlighting(self):
+        self.text.tag_remove("SURROUNDING_PARENS", "0.1", "end")
+        self.text.tag_remove("UNCLOSED", "0.1", "end")
+
+        if get_workbench().get_option("view.paren_highlighting"):
+            self._update_highlighting_for_active_range()
+    
+    def _update_highlighting_for_active_range(self):
+        start_index = "1.0"
+        end_index = self.text.index("end")
+        remaining = self._highlight_surrounding(start_index, end_index)
+        self._highlight_unclosed(remaining, start_index, end_index)
+    
+    def _configure_tags(self):
+        self.text.tag_configure("SURROUNDING_PARENS",
+                                foreground="Blue", 
+                                font=self._paren_highlight_font)
+        
+        self.text.tag_configure("UNCLOSED", background="LightGray")
+        
+        self.text.tag_lower("UNCLOSED")
+        self.text.tag_raise("sel")
+        
+
+    def _highlight_surrounding(self, start_index, end_index):
+        open_index, close_index, remaining = self.find_surrounding(start_index, end_index)
+        if None not in [open_index, close_index]:
+            self.text.tag_add("SURROUNDING_PARENS", open_index)
+            self.text.tag_add("SURROUNDING_PARENS", close_index)
+        
+        return remaining
+
+    # highlights an unclosed bracket
+    def _highlight_unclosed(self, remaining, start_index, end_index):
+        # anything remaining in the stack is an unmatched opener
+        # since the list is ordered, we can highlight everything starting from the first element
+        if len(remaining) > 0:
+            opener = remaining[0]
+            open_index = "%d.%d" % (opener.start[0], opener.start[1])
+            self.text.tag_add("UNCLOSED", open_index, end_index) 
+    
+    def _get_paren_tokens(self, source):
+        result = []
+        try: 
+            tokens = tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline)
+            for token in tokens:
+                if token.string != "" and token.string in "()[]{}":
+                    result.append(token)
+        except:
+            # happens eg when parens are unbalanced or there is indentation error or ...
+            pass
+        
+        return result
+
+    def find_surrounding(self, start_index, end_index):
+                
+        stack = []
+        opener, closer = None, None
+        open_index, close_index = None, None
+        
+        start_row, start_col = map(int, start_index.split(".")) 
+        source = self.text.get(start_index, end_index)
+        
+        # prepend source with empty lines and spaces to make 
+        # token rows and columns match with widget indices
+        source = ("\n" * (start_row-1)) + (" "*start_col) + source 
+        
+        for t in self._get_paren_tokens(source):
+            if t.string == "" or t.string not in "()[]{}":
+                continue
+            if t.string in "([{":
+                stack.append(t)
+            elif len(stack) > 0:
+                if stack[-1].string != _OPENERS[t.string]:
+                    continue
+                if not closer:
+                    opener = stack.pop()
+                    open_index = "%d.%d" % (opener.start[0], opener.start[1])
+                    token_index = "%d.%d" % (t.start[0], t.start[1])
+                    if self._is_insert_between_indices(open_index, token_index):
+                        closer = t
+                        close_index = token_index
+                else:
+                    stack.pop()
+        
+        return open_index, close_index, stack
+        
+
+    def _is_insert_between_indices(self, index1, index2):
+        return self.text.compare("insert", ">=", index1) and \
+               self.text.compare("insert-1c", "<=", index2)
+
+class ShellParenMatcher(ParenMatcher):
+    def _update_highlighting_for_active_range(self):
+    
+        # TODO: check that cursor is in this range
+        index_parts = self.text.tag_prevrange("command", "end")
+        
+        if index_parts:
+            start_index, end_index = index_parts
+            remaining = self._highlight_surrounding(start_index, end_index)
+            self._highlight_unclosed(remaining, start_index, "end")
+            
+def update_highlighting(event=None):
+    text = event.widget
+    if not hasattr(text, "paren_matcher"):
+        if isinstance(text, CodeViewText):
+            text.paren_matcher = ParenMatcher(text, get_workbench().get_font("BoldEditorFont"))
+        elif isinstance(text, ShellText):
+            text.paren_matcher = ShellParenMatcher(text, get_workbench().get_font("BoldEditorFont"))
+        else:
+            return
+    
+    text.paren_matcher.schedule_update()
+
+def load_plugin():
+    wb = get_workbench()  
+    
+    wb.set_default("view.paren_highlighting", True)
+    wb.bind_class("CodeViewText", "<<CursorMove>>", update_highlighting, True)
+    wb.bind_class("CodeViewText", "<<TextChange>>", update_highlighting, True)
+    wb.bind_class("ShellText", "<<CursorMove>>", update_highlighting, True)
+    wb.bind_class("ShellText", "<<TextChange>>", update_highlighting, True)
+    wb.bind("<<UpdateAppearance>>", update_highlighting, True)
diff --git a/thonny/plugins/pip_gui.py b/thonny/plugins/pip_gui.py
new file mode 100644
index 0000000..61ace6e
--- /dev/null
+++ b/thonny/plugins/pip_gui.py
@@ -0,0 +1,825 @@
+# -*- coding: utf-8 -*-
+
+import webbrowser
+
+import tkinter as tk
+from tkinter import ttk, messagebox
+
+from thonny import misc_utils, tktextext, ui_utils, THONNY_USER_BASE
+from thonny.globals import get_workbench, get_runner
+import subprocess
+from urllib.request import urlopen, urlretrieve
+import urllib.error
+import urllib.parse
+from concurrent.futures.thread import ThreadPoolExecutor
+import os
+import json
+from distutils.version import LooseVersion, StrictVersion
+import logging
+import re
+from tkinter.filedialog import askopenfilename
+from logging import exception
+from thonny.ui_utils import SubprocessDialog, AutoScrollbar, get_busy_cursor
+from thonny.misc_utils import running_on_windows
+import sys
+
+LINK_COLOR="#3A66DD"
+PIP_INSTALLER_URL="https://bootstrap.pypa.io/get-pip.py"
+
+class PipDialog(tk.Toplevel):
+    def __init__(self, master, only_user=False):
+        self._state = None # possible values: "listing", "fetching", "idle"
+        self._process = None
+        self._installed_versions = {}
+        self._only_user = only_user
+        self.current_package_data = None
+        
+        tk.Toplevel.__init__(self, master)
+        
+        main_frame = ttk.Frame(self)
+        main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15)
+        self.rowconfigure(0, weight=1)
+        self.columnconfigure(0, weight=1)
+
+        self.title(self._get_title())
+        if misc_utils.running_on_mac_os():
+            self.configure(background="systemSheetBackground")
+        self.transient(master)
+        self.grab_set() # to make it active
+        #self.grab_release() # to allow eg. copy something from the editor 
+        
+        self._create_widgets(main_frame)
+        
+        self.search_box.focus_set()
+        
+        self.bind('<Escape>', self._on_close, True) 
+        self.protocol("WM_DELETE_WINDOW", self._on_close)
+        self._show_instructions()
+        ui_utils.center_window(self, master)
+        
+        self._start_update_list()
+    
+    
+    def _create_widgets(self, parent):
+        
+        header_frame = ttk.Frame(parent)
+        header_frame.grid(row=1, column=0, sticky="nsew", padx=15, pady=(15,0))
+        header_frame.columnconfigure(0, weight=1)
+        header_frame.rowconfigure(1, weight=1)
+        
+        name_font = tk.font.nametofont("TkDefaultFont").copy()
+        name_font.configure(size=16)
+        self.search_box = ttk.Entry(header_frame, background=ui_utils.CALM_WHITE)
+        self.search_box.grid(row=1, column=0, sticky="nsew")
+        self.search_box.bind("<Return>", self._on_search, False)
+        
+        self.search_button = ttk.Button(header_frame, text="Search", command=self._on_search)
+        self.search_button.grid(row=1, column=1, sticky="nse", padx=(10,0))
+        
+        
+        main_pw = tk.PanedWindow(parent, orient=tk.HORIZONTAL,
+                                 background=ui_utils.get_main_background(),
+                                 sashwidth=10)
+        main_pw.grid(row=2, column=0, sticky="nsew", padx=15, pady=15)
+        parent.rowconfigure(2, weight=1)
+        parent.columnconfigure(0, weight=1)
+        
+        listframe = ttk.Frame(main_pw, relief="groove", borderwidth=1)
+        listframe.rowconfigure(0, weight=1)
+        listframe.columnconfigure(0, weight=1)
+        
+        self.listbox = tk.Listbox(listframe, activestyle="dotbox", 
+                                  width=20, height=10,
+                                  background=ui_utils.CALM_WHITE,
+                                  selectborderwidth=0, relief="flat",
+                                  highlightthickness=0, borderwidth=0)
+        self.listbox.insert("end", " <INSTALL>")
+        self.listbox.bind("<<ListboxSelect>>", self._on_listbox_select, True)
+        self.listbox.grid(row=0, column=0, sticky="nsew")
+        list_scrollbar = AutoScrollbar(listframe, orient=tk.VERTICAL)
+        list_scrollbar.grid(row=0, column=1, sticky="ns")
+        list_scrollbar['command'] = self.listbox.yview
+        self.listbox["yscrollcommand"] = list_scrollbar.set
+        
+        info_frame = ttk.Frame(main_pw)
+        info_frame.columnconfigure(0, weight=1)
+        info_frame.rowconfigure(1, weight=1)
+        
+        main_pw.add(listframe)
+        main_pw.add(info_frame)
+        
+        self.name_label = ttk.Label(info_frame, text="", font=name_font)
+        self.name_label.grid(row=0, column=0, sticky="w", padx=5)
+        
+
+        
+        info_text_frame = tktextext.TextFrame(info_frame, read_only=True,
+                                              horizontal_scrollbar=False,
+                                              vertical_scrollbar_class=AutoScrollbar,
+                                              width=60, height=10)
+        info_text_frame.configure(borderwidth=1)
+        info_text_frame.grid(row=1, column=0, columnspan=4, sticky="nsew", pady=(0,20))
+        self.info_text = info_text_frame.text
+        self.info_text.tag_configure("url", foreground=LINK_COLOR, underline=True)
+        self.info_text.tag_bind("url", "<ButtonRelease-1>", self._handle_url_click)
+        self.info_text.tag_bind("url", "<Enter>", lambda e: self.info_text.config(cursor="hand2"))
+        self.info_text.tag_bind("url", "<Leave>", lambda e: self.info_text.config(cursor=""))
+        self.info_text.tag_configure("install_file", foreground=LINK_COLOR, underline=True)
+        self.info_text.tag_bind("install_file", "<ButtonRelease-1>", self._handle_install_file_click)
+        self.info_text.tag_bind("install_file", "<Enter>", lambda e: self.info_text.config(cursor="hand2"))
+        self.info_text.tag_bind("install_file", "<Leave>", lambda e: self.info_text.config(cursor=""))
+        
+        default_font = tk.font.nametofont("TkDefaultFont")
+        self.info_text.configure(background=ui_utils.get_main_background(),
+                                 font=default_font,
+                                 wrap="word")
+
+        bold_font = default_font.copy()
+        # need to explicitly copy size, because Tk 8.6 on certain Ubuntus use bigger font in copies
+        bold_font.configure(weight="bold", size=default_font.cget("size"))
+        self.info_text.tag_configure("caption", font=bold_font)
+        self.info_text.tag_configure("bold", font=bold_font)
+        
+        
+        self.command_frame = ttk.Frame(info_frame)
+        self.command_frame.grid(row=2, column=0, sticky="w")
+        
+        self.install_button = ttk.Button(self.command_frame, text=" Upgrade ",
+                                         command=self._on_click_install)
+        
+        if not self._read_only():
+            self.install_button.grid(row=0, column=0, sticky="w", padx=0)
+        
+        self.uninstall_button = ttk.Button(self.command_frame, text="Uninstall",
+                                           command=lambda: self._perform_action("uninstall"))
+        
+        if not self._read_only():
+            self.uninstall_button.grid(row=0, column=1, sticky="w", padx=(5,0))
+        
+        self.advanced_button = ttk.Button(self.command_frame, text="...", width=3,
+                                          command=lambda: self._perform_action("advanced"))
+        
+        if not self._read_only():
+            self.advanced_button.grid(row=0, column=2, sticky="w", padx=(5,0))
+        
+        self.close_button = ttk.Button(info_frame, text="Close", command=self._on_close)
+        self.close_button.grid(row=2, column=3, sticky="e")
+    
+    def _on_click_install(self):
+        name = self.current_package_data["info"]["name"]
+        if self._confirm_install(name):
+            self._perform_action("install")
+
+    def _set_state(self, state, normal_cursor=False):
+        self._state = state
+        widgets = [self.listbox, 
+                           # self.search_box, # looks funny when disabled 
+                           self.search_button,
+                           self.install_button, self.advanced_button, self.uninstall_button]
+        
+        if state == "idle":
+            self.config(cursor="")
+            for widget in widgets:
+                widget["state"] = tk.NORMAL
+        else:
+            self.config(cursor=get_busy_cursor())
+            for widget in widgets:
+                widget["state"] = tk.DISABLED
+        
+        if normal_cursor:
+            self.config(cursor="")
+    
+    def _get_state(self):
+        return self._state
+    
+    def _handle_outdated_or_missing_pip(self):
+        raise NotImplementedError()
+        
+    def _install_pip(self):
+        self._clear()
+        self.info_text.direct_insert("end", "Installing pip\n\n", ("caption", ))
+        self.info_text.direct_insert("end", "pip, a required module for managing packages is missing or too old.\n\n"
+                                + "Downloading pip installer (about 1.5 MB), please wait ...\n")
+        self.update()
+        self.update_idletasks()
+        
+        installer_filename, _ = urlretrieve(PIP_INSTALLER_URL)
+        
+        self.info_text.direct_insert("end", "Installing pip, please wait ...\n")
+        self.update()
+        self.update_idletasks()
+        
+        proc, _ = self._create_backend_process([installer_filename], stderr=subprocess.PIPE)
+        out, err = proc.communicate()
+        os.remove(installer_filename)
+        
+        if err != "":
+            raise RuntimeError("Error while installing pip:\n" + err)
+        
+        self.info_text.direct_insert("end", out  + "\n")
+        self.update()
+        self.update_idletasks()
+        
+        # update list
+        self._start_update_list()
+        
+        
+    def _provide_pip_install_instructions(self):
+        self._clear()
+        self.info_text.direct_insert("end", "Outdated or missing pip\n\n", ("caption", ))
+        self.info_text.direct_insert("end", "pip, a required module for managing packages is missing or too old for Thonny.\n\n"
+                                + "If your system package manager doesn't provide recent pip (9.0.0 or later), "
+                                + "then you can install newest version by downloading ")
+        self.info_text.direct_insert("end", PIP_INSTALLER_URL, ("url",))
+        self.info_text.direct_insert("end", " and running it with " 
+                                     + self._get_interpreter()
+                                     + " (probably needs admin privileges).\n\n")
+        
+        self.info_text.direct_insert("end", self._instructions_for_command_line_install())
+        self._set_state("disabled", True)
+    
+    def _instructions_for_command_line_install(self):
+        return ("Alternatively, if you have an older pip installed, then you can install packages "
+                                     + "on the command line (Tools → Open system shell...)")
+    
+    def _start_update_list(self, name_to_show=None):
+        assert self._get_state() in [None, "idle"]
+        self._set_state("listing")
+        args = ["list"]
+        if self._only_user:
+            args.append("--user")
+        args.extend(["--pre", "--format", "json"])
+        self._process, _ = self._create_pip_process(args)
+        
+        def poll_completion():
+            if self._process == None:
+                return
+            else:
+                returncode = self._process.poll()
+                if returncode is None:
+                    # not done yet
+                    self.after(200, poll_completion)
+                else:
+                    self._set_state("idle")
+                    if returncode == 0:
+                        raw_data = self._process.stdout.read()
+                        self._update_list(json.loads(raw_data))
+                        if name_to_show is None:
+                            self._show_instructions()
+                        else:
+                            self._start_show_package_info(name_to_show)
+                    else:   
+                        error = self._process.stdout.read()
+                        if ("no module named pip" in error.lower() # pip not installed
+                            or "no such option" in error.lower()): # too old pip
+                            self._handle_outdated_or_missing_pip()
+                            return
+                        else:
+                            messagebox.showerror("pip list error", error)
+                    
+                    self._process = None
+        
+        poll_completion()
+    
+    def _update_list(self, data):
+        self.listbox.delete(1, "end")
+        self._installed_versions = {entry["name"] : entry["version"] for entry in data}
+        for name in sorted(self._installed_versions.keys(), key=str.lower):
+            self.listbox.insert("end", " " + name)
+        
+        
+    
+    def _on_listbox_select(self, event):
+        selection = self.listbox.curselection()
+        if len(selection) == 1:
+            if selection[0] == 0: # special first item
+                self._show_instructions()
+            else:
+                self._start_show_package_info(self.listbox.get(selection[0]).strip())
+    
+    def _on_search(self, event=None):
+        if not self._get_state() == "idle":
+            # Search box is not made inactive for busy-states
+            return
+        
+        if self.search_box.get().strip() == "":
+            return
+        
+        self._start_show_package_info(self.search_box.get().strip())
+    
+    def _clear(self):
+        self.current_package_data = None
+        self.name_label.grid_remove()
+        self.command_frame.grid_remove()
+        self.info_text.direct_delete("1.0", "end")
+    
+    def _show_instructions(self):
+        self._clear()
+        if self._read_only():
+            self.info_text.direct_insert("end", "With current interpreter you can only browse the packages here.\n"
+                                       + "Use 'Tools → Open system shell...' for installing, upgrading or uninstalling.")
+        else:            
+            self.info_text.direct_insert("end", "Install from PyPI\n", ("caption",))
+            self.info_text.direct_insert("end", "If you don't know where to get the package from, "
+                                         + "then most likely you'll want to search the Python Package Index. "
+                                         + "Start by entering the name of the package in the search box above and pressing ENTER.\n\n")
+            
+            self.info_text.direct_insert("end", "Install from local file\n", ("caption",))
+            self.info_text.direct_insert("end", "Click ")
+            self.info_text.direct_insert("end", "here", ("install_file",))
+            self.info_text.direct_insert("end", " to locate and install the package file (usually with .whl, .tar.gz or .zip extension).\n\n")
+            
+            self.info_text.direct_insert("end", "Upgrade or uninstall\n", ("caption",))
+            self.info_text.direct_insert("end", 'Start by selecting the package from the left.')
+        self._select_list_item(0)
+    
+    def _start_show_package_info(self, name):
+        self.current_package_data = None
+        self.info_text.direct_delete("1.0", "end")
+        self.name_label["text"] = ""
+        self.name_label.grid()
+        self.command_frame.grid()
+        
+        installed_version = self._get_installed_version(name) 
+        if installed_version is not None:
+            self.name_label["text"] = name
+            self.info_text.direct_insert("end", "Installed version: ", ('caption',))
+            self.info_text.direct_insert("end", installed_version + "\n")
+        
+        
+        # Fetch info from PyPI  
+        self._set_state("fetching")
+        # Follwing url fetches info about latest version.
+        # This is OK even when we're looking an installed older version
+        # because new version may have more relevant and complete info.
+        url = "https://pypi.python.org/pypi/{}/json".format(urllib.parse.quote(name))
+        url_future = _fetch_url_future(url)
+            
+        def poll_fetch_complete():
+            if url_future.done():
+                self._set_state("idle")
+                try:
+                    _, bin_data = url_future.result()
+                    raw_data = bin_data.decode("UTF-8")
+                    self._show_package_info(name, json.loads(raw_data))
+                except urllib.error.HTTPError as e:
+                    self._show_package_info(name, self._generate_minimal_data(name), e.code)
+                        
+            else:
+                self.after(200, poll_fetch_complete)
+        
+        poll_fetch_complete()
+    
+    def _generate_minimal_data(self, name):
+        return {
+            "info" : {'name' : name},
+            "releases" : {}
+            }
+
+    def _show_package_info(self, name, data, error_code=None):
+        self.current_package_data = data
+        
+        def write(s, tag=None):
+            if tag is None:
+                tags = ()
+            else:
+                tags = (tag,)
+            self.info_text.direct_insert("end", s, tags)
+        
+        def write_att(caption, value, value_tag=None):
+            write(caption + ": ", "caption")
+            write(value, value_tag)
+            write("\n")
+            
+        if error_code is not None:
+            if error_code == 404:
+                write("Could not find the package from PyPI.")
+                if not self._get_installed_version(name):
+                    # new package
+                    write("\nPlease check your spelling!"
+                          + "\nYou need to enter ")
+                    write("exact package name", "bold")
+                    write("!")
+                    
+            else:
+                write("Could not find the package info from PyPI. Error code: " + str(error_code))
+            return
+        
+        info = data["info"]
+        self.name_label["text"] = info["name"] # search name could have been a bit different
+        latest_stable_version = _get_latest_stable_version(data["releases"].keys())
+        if latest_stable_version is not None:
+            write_att("Latest stable version", latest_stable_version)
+        else:
+            write_att("Latest version", data["info"]["version"])
+        write_att("Summary", info["summary"])
+        write_att("Author", info["author"])
+        write_att("Homepage", info["home_page"], "url")
+        if info.get("bugtrack_url", None):
+            write_att("Bugtracker", info["bugtrack_url"], "url")
+        if info.get("docs_url", None):
+            write_att("Documentation", info["docs_url"], "url")
+        if info.get("package_url", None):
+            write_att("PyPI page", info["package_url"], "url")
+        
+        if self._get_installed_version(info["name"]) is not None:
+            self.install_button["text"] = "Upgrade"
+            if not self._read_only():
+                self.uninstall_button.grid(row=0, column=1)
+            
+            self._select_list_item(info["name"])
+            if self._get_installed_version(info["name"]) == latest_stable_version:
+                self.install_button["state"] = "disabled"
+            else: 
+                self.install_button["state"] = "normal"
+        else:
+            self.install_button["text"] = "Install"
+            self.uninstall_button.grid_forget()
+            self._select_list_item(0)
+            
+    
+    def _normalize_name(self, name):
+        # looks like (in some cases?) pip list gives the name as it was used during install
+        # ie. the list may contain lowercase entry, when actual metadata has uppercase name 
+        # Example: when you "pip install cx-freeze", then "pip list"
+        # really returns "cx-freeze" although correct name is "cx_Freeze"
+        
+        # https://www.python.org/dev/peps/pep-0503/#id4
+        return re.sub(r"[-_.]+", "-", name).lower().strip()
+    
+    def _select_list_item(self, name_or_index):
+        if isinstance(name_or_index, int):
+            index = name_or_index
+        else:
+            normalized_items = list(map(self._normalize_name, self.listbox.get(0, "end")))
+            try:
+                index = normalized_items.index(self._normalize_name(name_or_index))
+            except:
+                exception("Can't find package name from the list: " + name_or_index)
+                return
+        
+        self.listbox.select_clear(0, "end")
+        self.listbox.select_set(index)
+        self.listbox.see(index)
+        
+        
+    
+    def _perform_action(self, action):
+        assert self._get_state() == "idle"
+        assert self.current_package_data is not None
+        data = self.current_package_data
+        name = self.current_package_data["info"]["name"]
+        install_args = ["install", "--no-cache-dir"] 
+        if self._only_user:
+            install_args.append("--user")
+        
+        if action == "install":
+            args = install_args
+            if self._get_installed_version(name) is not None:
+                args.append("--upgrade")
+            
+            args.append(name)
+        elif action == "uninstall":
+            if (name in ["pip", "setuptools"]
+                and not messagebox.askyesno("Really uninstall?",
+                    "Package '{}' is required for installing and uninstalling other packages.\n\n".format(name)
+                    + "Are you sure you want to uninstall it?")):
+                return
+            args = ["uninstall", "-y", name]
+        elif action == "advanced":
+            details = _ask_installation_details(self, data, 
+                        _get_latest_stable_version(list(data["releases"].keys())))
+            if details is None: # Cancel
+                return
+            
+            version, upgrade_deps = details
+            args = install_args
+            if upgrade_deps:
+                args.append("--upgrade")
+            args.append(name + "==" + version)
+        else:
+            raise RuntimeError("Unknown action")
+        
+        proc, cmd = self._create_pip_process(args)
+        title = subprocess.list2cmdline(cmd)
+        
+        # following call blocks
+        _show_subprocess_dialog(self, proc, title)
+        if action == "uninstall":
+            self._show_instructions() # Make the old package go away as fast as possible
+        self._start_update_list(None if action == "uninstall" else name)
+        
+        
+    
+    def _handle_install_file_click(self, event):
+        if self._get_state() != "idle":
+            return 
+        
+        filename = askopenfilename (
+            filetypes = [('Package', '.whl .zip .tar.gz'), ('all files', '.*')], 
+            initialdir = get_workbench().get_option("run.working_directory")
+        )
+        if filename: # Note that missing filename may be "" or () depending on tkinter version
+            self._install_local_file(filename)
+    
+    def _install_local_file(self, filename):
+        args = ["install"]
+        if self._only_user:
+            args.append("--user")
+        args.append(filename)
+        
+        proc, cmd = self._create_pip_process(args)
+        # following call blocks
+        title = subprocess.list2cmdline(cmd)
+        
+        _, out, _ = _show_subprocess_dialog(self, proc, title)
+        
+        # Try to find out the name of the package we're installing
+        name = None
+        
+        # output should include a line like this:
+        # Installing collected packages: pytz, six, python-dateutil, numpy, pandas
+        inst_lines = re.findall("^Installing collected packages:.*?$", out,
+                                     re.MULTILINE | re.IGNORECASE)  # @UndefinedVariable
+        if len(inst_lines) == 1:
+            # take last element
+            elements = re.split(",|:", inst_lines[0])
+            name = elements[-1].strip()
+        
+        self._start_update_list(name)
+    
+    def _handle_url_click(self, event):
+        url = _extract_click_text(self.info_text, event, "url")
+        if url is not None:
+            webbrowser.open(url)
+    
+    def _on_close(self, event=None):
+        self.destroy()
+        
+    def _get_installed_version(self, name):
+        for list_name in self._installed_versions:
+            if self._normalize_name(name) == self._normalize_name(list_name):
+                return self._installed_versions[list_name]
+        
+        return None
+
+    def _prepare_env_for_pip_process(self, encoding):
+        env = {}
+        for name in os.environ:
+            if ("python" not in name.lower() and name not in ["TK_LIBRARY", "TCL_LIBRARY"]): # skip python vars
+                env[name] = os.environ[name]
+                
+        env["PYTHONIOENCODING"] = encoding
+        env["PYTHONUNBUFFERED"] = "1"
+        
+        return env
+
+    def _create_backend_process(self, args, stderr=subprocess.STDOUT):
+        encoding = "UTF-8"
+        
+                    
+        cmd = [self._get_interpreter()] + args
+        
+        startupinfo = None
+        creationflags = 0
+        if running_on_windows():
+            creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
+            startupinfo = subprocess.STARTUPINFO()
+            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+        
+        return (subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr,
+                                env=self._prepare_env_for_pip_process(encoding),
+                                universal_newlines=True,
+                                creationflags=creationflags,
+                                startupinfo=startupinfo),
+                cmd)
+
+    def _create_pip_process(self, args):
+        return self._create_backend_process(["-m", "pip"] + args)
+    
+    def _get_interpreter(self):
+        pass
+    
+    def _get_title(self):
+        return "Manage packages for " + self._get_interpreter()
+    
+    def _confirm_install(self, name):
+        return True
+    
+    def _read_only(self):
+        return False
+
+class BackendPipDialog(PipDialog):
+    def _get_interpreter(self):
+        return get_runner().get_interpreter_command()
+
+    def _confirm_install(self, name):
+        if name.lower().startswith("thonny"):
+            return messagebox.askyesno("Confirmation", 
+                                     "Looks like you are installing a Thonny-related package.\n"
+                                   + "If you meant to install a Thonny plugin, then you should\n"
+                                   + "close this dialog and choose 'Tools → Manage plugins...'\n"
+                                   + "\n"
+                                   + "Are you sure you want to install '" + name + "' here?")
+        else:
+            return True
+
+    def _handle_outdated_or_missing_pip(self):
+        if get_runner().using_venv():
+            self._install_pip()
+        else:
+            self._provide_pip_install_instructions()
+        
+    def _read_only(self):
+        return not get_runner().using_venv()
+
+class PluginsPipDialog(PipDialog):
+    def __init__(self, master):
+        PipDialog.__init__(self, master, only_user=True)
+    
+    def _get_interpreter(self):
+        return sys.executable.replace("thonny.exe", "python.exe")
+    
+    def _prepare_env_for_pip_process(self, encoding):
+        env = PipDialog._prepare_env_for_pip_process(self, encoding)
+        env["PYTHONUSERBASE"] = THONNY_USER_BASE
+        return env
+        
+        
+    def _create_widgets(self, parent):
+        bg = "#ffff99"
+        banner = tk.Label(parent, background=bg)
+        banner.grid(row=0, column=0, sticky="nsew")
+        
+        banner_text = tk.Label(banner, text="NB! This dialog is for managing Thonny plug-ins and their dependencies.\n"
+                                + "If you want to install packages for your own programs then close this and choose 'Tools → Manage packages...'\n"
+                                + "\n"
+                                + "This dialog installs packages into " + THONNY_USER_BASE + "\n"
+                                + "\n"
+                                + "NB! You need to restart Thonny after installing / upgrading / uninstalling a plug-in.",
+                                background=bg, justify="left")
+        banner_text.grid(pady=10, padx=10)
+        
+        PipDialog._create_widgets(self, parent)
+    
+    def _get_title(self):
+        return "Thonny plug-ins"
+
+    def _handle_outdated_or_missing_pip(self):
+        return self._provide_pip_install_instructions()
+    
+    def _instructions_for_command_line_install(self):
+        # System shell is not suitable without correct PYTHONUSERBASE 
+        return ""
+        
+class DetailsDialog(tk.Toplevel):
+    def __init__(self, master, package_metadata, selected_version):
+        tk.Toplevel.__init__(self, master)
+        self.result = None
+        
+        self.title("Advanced install / upgrade / downgrade")
+
+        self.rowconfigure(0, weight=1)
+        self.columnconfigure(0, weight=1)
+        main_frame = ttk.Frame(self) # To get styled background
+        main_frame.grid(sticky="nsew")
+        main_frame.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(0, weight=1)
+        
+        version_label = ttk.Label(main_frame, text="Desired version")
+        version_label.grid(row=0, column=0, columnspan=2, padx=20, pady=(15,0), sticky="w")
+        
+        def version_sort_key(s):
+            # Trying to massage understandable versions into valid StrictVersions
+            if s.replace(".", "").isnumeric(): # stable release
+                s2 = s + "b999" # make it latest beta version
+            elif "rc" in s:
+                s2 = s.replace("rc", "b8")
+            else:
+                s2 = s
+            try:
+                return StrictVersion(s2)
+            except:
+                # use only numbers
+                nums = re.findall(r"\d+", s)
+                while len(nums) < 2:
+                    nums.append("0")
+                return StrictVersion(".".join(nums[:3]))
+        
+        version_strings = list(package_metadata["releases"].keys())
+        version_strings.sort(key=version_sort_key, reverse=True)
+        self.version_combo = ttk.Combobox(main_frame, values=version_strings,
+                              exportselection=False)
+        try:
+            self.version_combo.current(version_strings.index(selected_version))
+        except:
+            pass
+        
+        self.version_combo.state(['!disabled', 'readonly'])
+        self.version_combo.grid(row=1, column=0, columnspan=2, pady=(0,15),
+                                padx=20, sticky="ew")
+        
+        
+        self.update_deps_var = tk.IntVar()
+        self.update_deps_var.set(0)
+        self.update_deps_cb = ttk.Checkbutton(main_frame, text="Upgrade dependencies",
+                                              variable=self.update_deps_var)
+        self.update_deps_cb.grid(row=2, column=0, columnspan=2, padx=20, sticky="w")
+        
+        self.ok_button = ttk.Button(main_frame, text="Install", command=self._ok)
+        self.ok_button.grid(row=3, column=0, pady=15, padx=(20, 0), sticky="se")
+        self.cancel_button = ttk.Button(main_frame, text="Cancel", command=self._cancel)
+        self.cancel_button.grid(row=3, column=1, pady=15, padx=(5,20), sticky="se")
+        
+        
+
+        if misc_utils.running_on_mac_os():
+            self.configure(background="systemSheetBackground")
+        #self.resizable(height=tk.FALSE, width=tk.FALSE)
+        self.transient(master)
+        self.grab_set() # to make it active and modal
+        self.version_combo.focus_set()
+        
+        
+        self.bind('<Escape>', self._cancel, True)  
+        self.protocol("WM_DELETE_WINDOW", self._cancel)
+        
+        ui_utils.center_window(self, master)
+        
+    
+    def _ok(self, event=None):
+        self.result = self.version_combo.get(), bool(self.update_deps_var.get())
+        self.destroy()
+    
+    def _cancel(self, event=None):
+        self.result = None
+        self.destroy()
+        
+def _fetch_url_future(url, timeout=10):
+    def load_url():
+        with urlopen(url, timeout=timeout) as conn:
+            return (conn, conn.read())
+            
+    executor = ThreadPoolExecutor(max_workers=1)
+    return executor.submit(load_url)
+
+
+
+def _get_latest_stable_version(version_strings):
+    versions = []
+    for s in version_strings:
+        if s.replace(".", "").isnumeric(): # Assuming stable versions have only dots and numbers
+            versions.append(LooseVersion(s)) # LooseVersion __str__ doesn't change the version string
+    
+    if len(versions) == 0:
+        return None
+        
+    return str(sorted(versions)[-1])
+
+
+def _show_subprocess_dialog(master, proc, title):
+    dlg = SubprocessDialog(master, proc, title)
+    dlg.wait_window()
+    return dlg.returncode, dlg.stdout, dlg.stderr
+
+
+def _ask_installation_details(master, data, selected_version):
+    dlg = DetailsDialog(master, data, selected_version)
+    dlg.wait_window()
+    return dlg.result
+
+
+def _extract_click_text(widget, event, tag):
+    # http://stackoverflow.com/a/33957256/261181
+    try:
+        index = widget.index("@%s,%s" % (event.x, event.y))
+        tag_indices = list(widget.tag_ranges(tag))
+        for start, end in zip(tag_indices[0::2], tag_indices[1::2]):
+            # check if the tag matches the mouse click index
+            if widget.compare(start, '<=', index) and widget.compare(index, '<', end):
+                return widget.get(start, end)
+    except:
+        logging.exception("extracting click text")
+        return None
+
+
+def load_plugin():
+    def open_backend_pip_gui(*args):
+        pg = BackendPipDialog(get_workbench())
+        pg.wait_window()
+    
+    def open_backend_pip_gui_enabled():
+        return "pip_gui" in get_runner().supported_features()
+
+    def open_frontend_pip_gui(*args):
+        pg = PluginsPipDialog(get_workbench())
+        pg.wait_window()
+
+    get_workbench().add_command("backendpipgui", "tools", "Manage packages...", open_backend_pip_gui,
+                                tester=open_backend_pip_gui_enabled,
+                                group=80)
+    get_workbench().add_command("pluginspipgui", "tools", "Manage plug-ins...", open_frontend_pip_gui,
+                                group=180)
+
+
+    
\ No newline at end of file
diff --git a/thonny/plugins/refactor.py b/thonny/plugins/refactor.py
new file mode 100644
index 0000000..02c0315
--- /dev/null
+++ b/thonny/plugins/refactor.py
@@ -0,0 +1,272 @@
+"""
+from rope.base.project import Project
+from rope.refactor.rename import Rename
+import rope.base.libutils
+import tkinter as tk
+from tkinter import ttk
+import os.path
+
+#arguments: 1) full path to the file, 2) new name to be applied to the identifier, 3) Rope offset (position) of the renamed identifier in the current file
+#returns a list of Rope change objects applying to this rename refactor
+#throws an exception if anything goes wrong, needs to be handled by callers!
+def get_list_of_rename_changes(full_path, new_variable_name, offset):
+    filearr = os.path.split(full_path)
+    project_path = filearr[0]
+    module_name = filearr[1]
+    project = Project(project_path, ropefolder=None)
+    module = rope.base.libutils.path_to_resource(project, full_path)
+    changes = Rename(project, module, offset).get_changes(new_variable_name)
+    return (project, changes)
+    
+#performs the changes 
+#arguments: 1) Rope project, 2) Rope changes object
+def perform_changes(project, changes):
+    project.do(changes)
+    project.close()
+
+#cancels the changes, cleans up the project
+def cancel_changes(project):
+    project.close()
+
+#utility method for convering a Text index to a Rope offset
+def calculate_offset(text):
+    contents = text.get(1.0, 'end').split('\n')
+    insert_index = text.index('insert')
+    linearr = insert_index.split('.')
+    line_no = int(linearr[0])
+    char_no = int(linearr[1])
+
+    totalchars = char_no
+    for line in range(line_no - 1):
+        totalchars += len(contents[line]) + 1
+
+    return totalchars
+
+#inspired by http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
+#creates a window asking for a new identifier name, can later be accessed via the refactor_new_variable_name variable
+class RenameWindow(tk.Toplevel):
+    def __init__(self, master, title = None):
+        tk.Toplevel.__init__(self, master)
+        self.refactor_new_variable_name = None
+
+        self.transient(master)
+
+        self.title('Rename')
+
+        self.parent = master
+        self.result = None
+    
+        ttk.Label(self, text="New name:").grid(row=0, columnspan=2)
+        self.new_name_entry = ttk.Entry(self)
+        self.new_name_entry.grid(row=1, columnspan=2)
+        self.new_name_entry.focus_force();
+
+        self.ok_button = ttk.Button(self, text="OK", command=self.ok, default=tk.ACTIVE)
+        self.cancel_button = ttk.Button(self, text="Cancel", command=self.cancel)
+        self.ok_button.grid(row=2, column=0, sticky=tk.W + tk.E, padx=5)
+        self.cancel_button.grid(row=2, column=1, sticky=tk.W + tk.E, padx=5)
+
+        self.bind("<Return>", self.ok, True)
+        self.bind("<Escape>", self.cancel, True)
+
+        self.grab_set()
+
+        self.protocol("WM_DELETE_WINDOW", self.cancel)
+        self.geometry("+%d+%d" % (master.winfo_rootx()+50,
+                                  master.winfo_rooty()+50))
+        self.resizable(width=False, height=False)
+
+        self.wait_window(self)
+    
+    #user clicked cancel - destroy the object, return the focus to master    
+    def cancel(self, event=None):
+        self.parent.focus_set()
+        self.destroy()
+
+    #user clicked ok - set the variable name, destroy the object, return the focus to parent
+    def ok(self, event=None):
+        self.withdraw()
+        self.update_idletasks()
+        self.refactor_new_variable_name = self.new_name_entry.get()
+        self.cancel()
+
+
+class RefactorRenameStartEvent(thonny.user_logging.UserEvent): #user initiated the refactoring process
+    def __init__(self, editor):
+        self.editor_id = id(editor)
+
+class RefactorRenameCancelEvent(thonny.user_logging.UserEvent): #user manually cancelled the refactoring process
+    def __init__(self, editor):
+        self.editor_id = id(editor)
+
+class RefactorRenameFailedEvent(thonny.user_logging.UserEvent): #refactoring process failed due to an error
+    def __init__(self, editor):
+        self.editor_id = id(editor)
+
+class RefactorRenameCompleteEvent(thonny.user_logging.UserEvent): #refactoring process was successfully completed
+    def __init__(self, description, offset, affected_files):
+        self.description = description
+        self.offset = offset
+        self.affected_files = affected_files
+
+def _cmd_refactor_rename(self):
+    self.log_user_event(thonny.refactor.RefactorRenameStartEvent(self.editor_notebook.get_current_editor()))
+    if not self.editor_notebook.get_current_editor():
+        self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor()))
+        errorMessage = tkMessageBox.showerror(
+                       title="Rename failed",
+                       message="Rename operation failed (no active editor tabs?).", #TODO - more informative text needed
+                       master=self)
+        return
+
+    #create a list of active but unsaved/modified editors)
+    unsaved_editors = [x for x in self.editor_notebook.winfo_children() if type(x) == Editor and x._cmd_save_file_enabled()]
+
+    if len(unsaved_editors) != 0:
+        #confirm with the user that all open editors need to be saved first
+        confirm = tkMessageBox.askyesno(
+                  title="Save Files Before Rename",
+                  message="All modified files need to be saved before refactoring. Do you want to continue?",
+                  default=tkMessageBox.YES,
+                  master=self)
+
+        if not confirm:
+            self.log_user_event(thonny.refactor.RefactorRenameCancelEvent(self.editor_notebook.get_current_editor()))
+            return #if user doesn't want it, return
+
+        for editor in unsaved_editors:                     
+            if not editor.get_filename():
+                self.editor_notebook.select(editor) #in the case of editors with no filename, show it, so user knows which one they're saving
+            editor._cmd_save_file()
+            if editor._cmd_save_file_enabled(): #just a sanity check - if after saving a file still needs saving, something is wrong
+                self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor()))
+                errorMessage = tkMessageBox.showerror(
+                               title="Rename failed",
+                               message="Rename operation failed (saving file failed).", #TODO - more informative text needed
+                               master=self)
+                return
+
+    filename = self.editor_notebook.get_current_editor().get_filename()
+
+    if filename == None: #another sanity check - the current editor should have an associated filename by this point 
+        self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor()))
+        errorMessage = tkMessageBox.showerror(
+                       title="Rename failed",
+                       message="Rename operation failed (no filename associated with current module).", #TODO - more informative text needed
+                       master=self)
+        return
+
+    identifier = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) #regex to compare valid python identifiers against
+
+    while True: #ask for new variable name until a valid one is entered
+        renameWindow = thonny.refactor.RenameWindow(self)
+        newname = renameWindow.refactor_new_variable_name
+        if newname == None:
+            self.log_user_event(thonny.refactor.RefactorRenameCancelEvent(self.editor_notebook.get_current_editor()))
+            return #user canceled, return
+
+        if re.match(identifier, newname):
+            break #valid identifier entered, continue
+
+        errorMessage = tkMessageBox.showerror(
+                       title="Incorrect identifier",
+                       message="Incorrect Python identifier, please re-enter.",
+                       master=self)
+
+    try: 
+        #calculate the offset for rope
+        offset = thonny.refactor.calculate_offset(self.editor_notebook.get_current_editor()._code_view.text)
+        #get the project handle and list of changes
+        project, changes = thonny.refactor.get_list_of_rename_changes(filename, newname, offset)
+        #if len(changes.changes == 0): raise Exception
+
+    except Exception:
+        try: #rope needs the cursor to be AFTER the first character of the variable being refactored
+             #so the reason for failure might be that the user had the cursor before the variable name
+            offset = offset + 1
+            project, changes = thonny.refactor.get_list_of_rename_changes(filename, newname, offset)
+            #if len(changes.changes == 0): raise Exception
+
+        except Exception: #couple of different reasons why this could happen, let's list them all in the error message
+            self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor()))
+            message = 'Rename operation failed. A few possible reasons: \n'
+            message += '1) Not a valid Python identifier selected \n'
+            message += '2) The current file or any other files in the same directory or in any of its subdirectores contain incorrect syntax. Make sure the current project is in its own separate folder.'
+            errorMessage = tkMessageBox.showerror(
+                           title="Rename failed",
+                           message=message, #TODO - maybe also print stack trace for more info?
+                           master=self)               
+            return
+
+    description = changes.description #needed for logging
+
+    #sanity check
+    if len(changes.changes) == 0:
+        self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor()))
+        errorMessage = tkMessageBox.showerror(
+                           title="Rename failed",
+                           message="Rename operation failed - no identifiers affected by change.", #TODO - more informative text needed
+                           master=self)               
+        return
+
+    affected_files = [] #needed for logging
+    #show the preview window to user
+    messageText = 'Confirm the changes. The following files will be modified:\n'
+    for change in changes.changes:
+        affected_files.append(change.resource._path)
+        messageText += '\n ' + change.resource._path
+
+    messageText += '\n\n NB! This action cannot be undone.'
+
+    confirm = tkMessageBox.askyesno(
+              title="Confirm changes",
+              message=messageText,
+              default=tkMessageBox.YES,
+              master=self)
+    
+    #confirm with user to finalize the changes
+    if not confirm:
+        self.log_user_event(thonny.refactor.RefactorRenameCancelEvent(self.editor_notebook.get_current_editor()))
+        thonny.refactor.cancel_changes(project)
+        return
+
+    try:
+        thonny.refactor.perform_changes(project, changes)
+    except Exception:
+            self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor()))
+            errorMessage = tkMessageBox.showerror(
+                           title="Rename failed",
+                           message="Rename operation failed (Rope error).", #TODO - more informative text needed
+                           master=self)     
+            thonny.refactor.cancel_changes(project)
+            return            
+
+    #everything went fine, let's load all the active tabs again and set their content
+    for editor in self.editor_notebook.winfo_children():
+        try: 
+            filename = editor.get_filename()
+            source, self.file_encoding = misc_utils.read_python_file(filename)
+            editor._code_view.set_content(source)
+            self.editor_notebook.tab(editor, text=self.editor_notebook._generate_editor_title(filename))
+        except Exception:
+            try: #it is possible that a file (module) itself was renamed - Rope allows it. so let's see if a file exists with the new name. 
+                filename = filename.replace(os.path.split(filename)[1], newname + '.py')
+                source, self.file_encoding = misc_utils.read_python_file(filename)
+                editor._code_view.set_content(source)
+                self.editor_notebook.tab(editor, text=self.editor_notebook._generate_editor_title(filename))
+            except Exception: #something went wrong with reloading the file, let's close this tab to avoid consistency problems
+                self.editor_notebook.forget(editor)
+                editor.destroy()
+
+    self.log_user_event(thonny.refactor.RefactorRenameCompleteEvent(description, offset, affected_files))
+    current_browser_node_path = self.file_browser.get_selected_path()
+    self.file_browser.refresh_tree()
+    if current_browser_node_path is not None:
+        self.file_browser.open_path_in_browser(current_browser_node_path)
+
+def _cmd_refactor_rename_enabled(self):
+    return self.editor_notebook.get_current_editor() is not None
+
+def _load_plugin_(workbench):
+    get_workbench().add_command("refactor_rename", "edit", "Rename identifier", ...)
+"""
\ No newline at end of file
diff --git a/thonny/plugins/replayer.py b/thonny/plugins/replayer.py
new file mode 100644
index 0000000..23b30ef
--- /dev/null
+++ b/thonny/plugins/replayer.py
@@ -0,0 +1,342 @@
+import tkinter as tk
+import tkinter.ttk as ttk
+from datetime import datetime
+from thonny import ui_utils
+from thonny.globals import get_workbench
+import json
+from thonny.base_file_browser import BaseFileBrowser
+import ast
+import os.path
+from thonny.plugins.coloring import SyntaxColorer
+
+
+class ReplayWindow(tk.Toplevel):
+    def __init__(self):
+        tk.Toplevel.__init__(self, get_workbench())
+        ui_utils.set_zoomed(self, True)
+        
+        self.main_pw   = tk.PanedWindow(self, orient=tk.HORIZONTAL, sashwidth=10)
+        self.center_pw  = tk.PanedWindow(self.main_pw, orient=tk.VERTICAL, sashwidth=10)
+        self.right_frame = ttk.Frame(self.main_pw)
+        self.right_pw  = tk.PanedWindow(self.right_frame, orient=tk.VERTICAL, sashwidth=10)
+        self.editor_notebook = ReplayerEditorNotebook(self.center_pw)
+        shell_book = ttk.Notebook(self.main_pw)
+        self.shell = ShellFrame(shell_book)
+        self.details_frame = EventDetailsFrame(self.right_pw)
+        self.log_frame = LogFrame(self.right_pw, self.editor_notebook, self.shell, self.details_frame)
+        self.browser = ReplayerFileBrowser(self.main_pw, self.log_frame)
+        self.control_frame = ControlFrame(self.right_frame)
+        
+        self.main_pw.grid(padx=10, pady=10, sticky=tk.NSEW)
+        self.main_pw.add(self.browser, width=200)
+        self.main_pw.add(self.center_pw, width=1000)
+        self.main_pw.add(self.right_frame, width=200)
+        self.center_pw.add(self.editor_notebook, height=700)
+        self.center_pw.add(shell_book, height=300)
+        shell_book.add(self.shell, text="Shell")
+        self.right_pw.grid(sticky=tk.NSEW)
+        self.control_frame.grid(sticky=tk.NSEW)
+        self.right_pw.add(self.log_frame, height=600)
+        self.right_pw.add(self.details_frame, height=200)
+        self.right_frame.columnconfigure(0, weight=1)
+        self.right_frame.rowconfigure(0, weight=1)
+        
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        
+            
+
+class ReplayerFileBrowser(BaseFileBrowser):
+    
+    def __init__(self, master, log_frame):
+        BaseFileBrowser.__init__(self, master, True, "tools.replayer_last_browser_folder")
+        self.log_frame = log_frame
+        self.configure(border=1, relief=tk.GROOVE)
+
+    def on_double_click(self, event):
+        self.save_current_folder()
+        path = self.get_selected_path()
+        if path:
+            self.log_frame.load_log(path)
+            
+class ControlFrame(ttk.Frame):
+    def __init__(self, master, **kw):
+        ttk.Frame.__init__(self, master=master, **kw)
+        
+        self.toggle_button = ttk.Button(self, text="Play")
+        self.speed_scale = ttk.Scale(self, from_=1, to=100, orient=tk.HORIZONTAL)
+        
+        self.toggle_button.grid(row=0, column=0, sticky=tk.NSEW, pady=(10,0), padx=(0,5))
+        self.speed_scale.grid(row=0, column=1, sticky=tk.NSEW, pady=(10,0), padx=(5,0))
+        
+        self.columnconfigure(1, weight=1)
+        
+        
+
+class LogFrame(ui_utils.TreeFrame):
+    def __init__(self, master, editor_book, shell, details_frame):
+        ui_utils.TreeFrame.__init__(self, master, ("desc", "pause"))
+        
+        self.tree.heading('desc', text='Event', anchor=tk.W)
+        self.tree.heading('pause', text='Pause (sec)', anchor=tk.W)
+        
+        self.configure(border=1, relief=tk.GROOVE)
+        
+        self.editor_notebook = editor_book
+        self.shell = shell
+        self.details_frame = details_frame
+        self.all_events = []
+        self.last_event_index = -1
+        self.loading = False 
+
+    def load_log(self, filename):
+        self._clear_tree()
+        self.details_frame._clear_tree()
+        self.all_events = []
+        self.last_event_index = -1
+        self.loading = True
+        self.editor_notebook.reset()
+        self.shell.reset()
+        
+        with open(filename, encoding="UTF-8") as f:
+            events = json.load(f)
+            last_event_time = None
+            for event in events:
+                node_id = self.tree.insert("", "end")
+                self.tree.set(node_id, "desc", event["sequence"])
+                event_time = datetime.strptime(event["time"], "%Y-%m-%dT%H:%M:%S.%f")
+                if last_event_time:
+                    delta = event_time - last_event_time
+                    pause = delta.seconds
+                else:
+                    pause = 0   
+                self.tree.set(node_id, "pause", str(pause if pause else ""))
+                self.all_events.append(event)
+
+                last_event_time = event_time
+                
+        self.loading = False
+        
+    def replay_event(self, event):
+        "this should be called with events in correct order"
+        #print("log replay", event)
+        
+        if "text_widget_id" in event:
+            if event.get("text_widget_context", None) == "shell":
+                self.shell.replay_event(event)
+            else:
+                self.editor_notebook.replay_event(event)
+    
+    def reset(self):
+        self.shell.reset()
+        self.editor_notebook.reset()
+        self.last_event_index = -1
+    
+    def on_select(self, event):
+        # parameter "event" is here tkinter event
+        if self.loading:
+            return 
+        iid = self.tree.focus()
+        if iid != '':
+            self.select_event(self.tree.index(iid))
+            
+        
+    def select_event(self, event_index):
+        event = self.all_events[event_index]
+        self.details_frame.load_event(event)
+        
+        # here event means logged event
+        if event_index > self.last_event_index:
+            # replay all events between last replayed event up to and including this event
+            while self.last_event_index < event_index:
+                self.replay_event(self.all_events[self.last_event_index+1])
+                self.last_event_index += 1
+                
+        elif event_index < self.last_event_index:
+            # Undo by reseting and replaying again
+            self.reset()
+            self.select_event(event_index)
+
+
+class EventDetailsFrame(ui_utils.TreeFrame):
+    def __init__(self, master):
+        ui_utils.TreeFrame.__init__(self, master, columns=("attribute", "value"))
+        self.tree.heading('attribute', text='Attribute', anchor=tk.W)
+        self.tree.heading('value', text='Value', anchor=tk.W)
+        self.configure(border=1, relief=tk.GROOVE)
+    
+    def load_event(self, event):
+        self._clear_tree()
+        for name in self.order_keys(event):
+            node_id = self.tree.insert("", "end")
+            self.tree.set(node_id, "attribute", name)
+            self.tree.set(node_id, "value", event[name])
+    
+    def order_keys(self, event):
+        return event.keys()
+
+class ReplayerCodeView(ttk.Frame):
+    def __init__(self, master):
+        ttk.Frame.__init__(self, master)
+        
+        self.vbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
+        self.vbar.grid(row=0, column=2, sticky=tk.NSEW)
+        self.hbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
+        self.hbar.grid(row=1, column=0, sticky=tk.NSEW, columnspan=2)
+        self.text = tk.Text(self,
+                yscrollcommand=self.vbar.set,
+                xscrollcommand=self.hbar.set,
+                borderwidth=0,
+                font=get_workbench().get_font("EditorFont"),
+                wrap=tk.NONE,
+                insertwidth=2,
+                #selectborderwidth=2,
+                inactiveselectbackground='gray',
+                #highlightthickness=0, # TODO: try different in Mac and Linux
+                #highlightcolor="gray",
+                padx=5,
+                pady=5,
+                undo=True,
+                autoseparators=False)
+        
+        self.text.grid(row=0, column=1, sticky=tk.NSEW)
+        self.hbar['command'] = self.text.xview
+        self.vbar['command'] = self.text.yview
+        self.columnconfigure(1, weight=1)
+        self.rowconfigure(0, weight=1)
+        
+
+class ReplayerEditor(ttk.Frame):
+    def __init__(self, master):
+        ttk.Frame.__init__(self, master)
+        self.code_view = ReplayerCodeView(self)
+        self.code_view.grid(sticky=tk.NSEW)
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+    
+    def replay_event(self, event):
+        if event["sequence"] in ["TextInsert", "TextDelete"]:
+            if event["sequence"] == "TextInsert":
+                self.code_view.text.insert(event["index"], event["text"],
+                                           ast.literal_eval(event["tags"]))
+                
+            elif event["sequence"] == "TextDelete":
+                if event["index2"] and event["index2"] != "None":
+                    self.code_view.text.delete(event["index1"], event["index2"])
+                else:
+                    self.code_view.text.delete(event["index1"])
+            
+                
+            self.see_event(event)
+            
+                
+    def see_event(self, event):
+        for key in ["index", "index1", "index2"]:
+            if key in event and event[key] and event[key] != "None":
+                self.code_view.text.see(event[key])
+
+    def reset(self):
+        self.code_view.text.delete("1.0", "end")
+        
+        
+class ReplayerEditorProper(ReplayerEditor):
+    
+    def __init__(self, master):
+        ReplayerEditor.__init__(self, master)
+        self.set_colorer()
+    
+    def set_colorer(self):
+        # TODO: some problem when doing fast rewind
+        return
+    
+        self.colorer = SyntaxColorer(self.code_view.text,
+                                     get_workbench().get_font("EditorFont"),
+                                     get_workbench().get_font("BoldEditorFont"))
+
+    def replay_event(self, event):
+        ReplayerEditor.replay_event(self, event)
+        # TODO: some problem when doing fast rewind
+        #self.colorer.notify_range("1.0", "end")
+    
+    def reset(self):
+        ReplayerEditor.reset(self)
+        self.set_colorer()
+
+
+class ReplayerEditorNotebook(ttk.Notebook):
+    def __init__(self, master):
+        ttk.Notebook.__init__(self, master, padding=0)
+        self._editors_by_text_widget_id = {}
+    
+    def clear(self):
+        
+        for child in self.winfo_children():
+            child.destroy()
+        
+        self._editors_by_text_widget_id = {}
+    
+    def get_editor_by_text_widget_id(self, text_widget_id):
+        if text_widget_id not in self._editors_by_text_widget_id:
+            editor = ReplayerEditorProper(self)
+            self.add(editor, text="<untitled>")
+            self._editors_by_text_widget_id[text_widget_id] = editor
+            
+        return self._editors_by_text_widget_id[text_widget_id]
+    
+    def replay_event(self, event):
+        if "text_widget_id" in event:
+            editor = self.get_editor_by_text_widget_id(event["text_widget_id"])
+            #print(event.editor_id, id(editor), event)
+            self.select(editor)
+            editor.replay_event(event)
+            
+            if "filename" in event:
+                self.tab(editor, text=os.path.basename(event["filename"]))
+    
+    def reset(self):
+        for editor in self.winfo_children():
+            self.forget(editor)
+            editor.destroy()
+            
+        self._editors_by_text_widget_id = {}
+
+class ShellFrame(ReplayerEditor):
+    def __init__(self, master):
+        ReplayerEditor.__init__(self, master)
+        
+        # TODO: use same source as shell
+        vert_spacing = 10
+        io_indent = 16
+        self.code_view.text.tag_configure("toplevel", font=get_workbench().get_font("EditorFont"))
+        self.code_view.text.tag_configure("prompt", foreground="purple", font=get_workbench().get_font("BoldEditorFont"))
+        self.code_view.text.tag_configure("command", foreground="black")
+        self.code_view.text.tag_configure("version", foreground="DarkGray")
+        self.code_view.text.tag_configure("automagic", foreground="DarkGray")
+        self.code_view.text.tag_configure("value", foreground="DarkBlue") # TODO: see also _text_key_press and _text_key_release
+        self.code_view.text.tag_configure("error", foreground="Red")
+        
+        self.code_view.text.tag_configure("io", lmargin1=io_indent, lmargin2=io_indent, rmargin=io_indent,
+                                font=get_workbench().get_font("IOFont"))
+        self.code_view.text.tag_configure("stdin", foreground="Blue")
+        self.code_view.text.tag_configure("stdout", foreground="Black")
+        self.code_view.text.tag_configure("stderr", foreground="Red")
+        self.code_view.text.tag_configure("hyperlink", foreground="#3A66DD", underline=True)
+        
+        self.code_view.text.tag_configure("vertically_spaced", spacing1=vert_spacing)
+        self.code_view.text.tag_configure("inactive", foreground="#aaaaaa")
+    
+
+
+def load_plugin():
+    def open_replayer():
+        win = ReplayWindow()
+        win.focus_set()
+        win.grab_set()
+        get_workbench().wait_window(win)
+    
+    get_workbench().set_default("tools.replayer_last_browser_folder", None)
+    if (get_workbench().get_option("debug_mode")
+        or get_workbench().get_option("expert_mode")):
+        get_workbench().add_command("open_replayer", "tools", "Open replayer...", 
+                                open_replayer,
+                                group=110)
diff --git a/thonny/plugins/styler.py b/thonny/plugins/styler.py
new file mode 100644
index 0000000..6c35456
--- /dev/null
+++ b/thonny/plugins/styler.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+from tkinter import ttk
+from thonny.misc_utils import running_on_linux
+from thonny.globals import get_workbench
+
+_images = set() # for keeping references to tkinter images to avoid garbace colleting them
+
+
+
+def tweak_notebooks():
+    style = ttk.Style()
+    theme = style.theme_use()
+    
+    if theme in ["xpnative", "vista"]:
+        get_workbench().get_image('gray_line.gif', "gray_line")
+        
+        style.element_create("gray_line", "image", "gray_line",
+                                   ("!selected", "gray_line"), 
+                                   height=1, width=10, border=1)
+        
+        style.layout('Tab', [
+            ('Notebook.tab', {'sticky': 'nswe', 'children': [
+                ('Notebook.padding', {'sticky': 'nswe', 'side': 'top', 'children': [
+                    ('Notebook.focus', {'sticky': 'nswe', 'side': 'top', 'children': [
+                        ('Notebook.label', {'sticky': '', 'side': 'left'}),
+                    ]})
+                ]}),
+                ('gray_line', {'sticky': 'we', 'side': 'bottom'}),
+            ]}),
+        ])
+    
+    style.configure("Tab", padding=(4,1,0,0))
+    if theme == "clam":
+        style.configure("ButtonNotebook.Tab", padding=(6,4,2,3))
+    else:
+        style.configure("ButtonNotebook.Tab", padding=(4,1,1,3))
+            
+    if theme == "aqua":
+        style.map("TNotebook.Tab", foreground=[('selected', 'white'), ('!selected', 'black')])
+
+
+def tweak_treeviews():
+    style = ttk.Style()
+    # get rid of Treeview borders
+    style.layout("Treeview", [
+        ('Treeview.treearea', {'sticky': 'nswe'})
+    ])
+    
+    # necessary for Python 2.7 TODO: doesn't help for aqua
+    style.configure("Treeview", background="white")
+    
+    #style.configure("Treeview", font='helvetica 14 bold')
+    style.configure("Treeview", font=get_workbench().get_font("TreeviewFont"))
+
+    #print(style.map("Treeview"))
+    #print(style.layout("Treeview"))
+    #style.configure("Treeview.treearea", font=TREE_FONT)
+    # NB! Some Python or Tk versions (Eg. Py 3.2.3 + Tk 8.5.11 on Raspbian)
+    # can't handle multi word color names in style.map  
+    light_blue = "#ADD8E6" 
+    light_grey = "#D3D3D3"
+    if running_on_linux():
+        style.map("Treeview",
+              background=[('selected', 'focus', light_blue),
+                          ('selected', '!focus', light_grey),
+                          ],
+              foreground=[('selected', 'black'),
+                          ],
+              )
+    else:
+        style.map("Treeview",
+              background=[('selected', 'focus', 'SystemHighlight'),
+                          ('selected', '!focus', light_grey),
+                          ],
+              foreground=[('selected', 'SystemHighlightText')],
+              )
+
+def tweak_menubuttons():
+    style = ttk.Style()
+    #print(style.layout("TMenubutton"))
+    style.layout("TMenubutton", [
+        ('Menubutton.dropdown', {'side': 'right', 'sticky': 'ns'}),
+        ('Menubutton.button', {'children': [
+            #('Menubutton.padding', {'children': [
+                ('Menubutton.label', {'sticky': ''})
+            #], 'expand': '1', 'sticky': 'we'})
+        ], 'expand': '1', 'sticky': 'nswe'})
+    ])
+    
+    style.configure("TMenubutton", padding=14)
+
+def tweak_paned_windows():
+    style = ttk.Style()
+    style.configure("Sash", sashthickness=10)
+
+
+def load_plugin():
+    tweak_notebooks()
+    tweak_treeviews()
+    tweak_paned_windows()
+    
+    
diff --git a/thonny/plugins/system_shell/__init__.py b/thonny/plugins/system_shell/__init__.py
new file mode 100644
index 0000000..88a4ca1
--- /dev/null
+++ b/thonny/plugins/system_shell/__init__.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+from subprocess import Popen, check_output
+import os.path
+import shlex
+import platform
+from tkinter.messagebox import showerror
+import shutil
+from thonny.globals import get_runner
+from thonny import THONNY_USER_DIR
+import subprocess
+from time import sleep
+
+def _create_pythonless_environment():
+    # If I want to call another python version, then 
+    # I need to remove from environment the items installed by current interpreter
+    env = {}
+    
+    for key in os.environ:
+        if ("python" not in key.lower()
+            and key not in ["TK_LIBRARY", "TCL_LIBRARY"]):
+            env[key] = os.environ[key]
+    
+    return env
+
+
+def _get_exec_prefix(python_interpreter):
+    
+    return check_output([python_interpreter, "-c", "import sys; print(sys.exec_prefix)"],
+                        universal_newlines=True,
+                        env=_create_pythonless_environment()
+                        ).strip()
+
+def _add_to_path(directory, path):
+    # Always prepending to path may seem better, but this could mess up other things.
+    # If the directory contains only one Python distribution executables, then 
+    # it probably won't be in path yet and therefore will be prepended.
+    if (directory in path.split(os.pathsep)
+        or platform.system() == "Windows" and directory.lower() in path.lower().split(os.pathsep)):
+        return path
+    else:
+        return directory + os.pathsep + path
+
+def open_system_shell():
+    """Main task is to modify path and open terminal window.
+    Bonus (and most difficult) part is executing a script in this window
+    for recommending commands for running given python and related pip"""
+    python_interpreter = get_runner().get_interpreter_command()
+    if python_interpreter is None:
+        return
+    
+    exec_prefix=_get_exec_prefix(python_interpreter)
+    if ".." in exec_prefix:
+        exec_prefix = os.path.realpath(exec_prefix)
+    env = _create_pythonless_environment()
+    
+    # TODO: take care of SSL_CERT_FILE (unset when running external python and set for builtin)
+    # Unset when we're in builtin python and target python is external
+    
+    # TODO: what if executable or explainer needs escaping?
+    # Maybe try creating a script in temp folder and execute this,
+    # passing required paths via environment variables.
+    
+    interpreter=python_interpreter.replace("pythonw","python")
+    explainer=os.path.join(os.path.dirname(__file__), "explain_environment.py")
+    cwd=get_runner().get_cwd()
+    
+    if platform.system() == "Windows":
+        return _open_shell_in_windows(cwd, env, interpreter, explainer, exec_prefix)
+        
+    elif platform.system() == "Linux":
+        return _open_shell_in_linux(cwd, env, interpreter, explainer, exec_prefix)
+        
+    elif platform.system() == "Darwin":
+        _open_shell_in_macos(cwd, env, interpreter, explainer, exec_prefix)
+    else:
+        showerror("Problem", "Don't know how to open system shell on this platform (%s)"
+                  % platform.system())
+        return
+
+def _open_shell_in_windows(cwd, env, interpreter, explainer, exec_prefix):
+    env["PATH"] = _add_to_path(exec_prefix + os.pathsep, env.get("PATH", ""))
+    env["PATH"] = _add_to_path(os.path.join(exec_prefix, "Scripts"), env.get("PATH", ""))
+    
+    # Yes, the /K argument has weird quoting. Can't explain this, but it works 
+    cmd_line = """start "Shell for {interpreter}" /D "{cwd}" /W cmd /K ""{interpreter}" "{explainer}"" """.format(
+        interpreter=interpreter, 
+        cwd=cwd,
+        explainer=explainer)
+    
+    Popen(cmd_line, env=env, shell=True)
+
+def _open_shell_in_linux(cwd, env, interpreter, explainer, exec_prefix):
+    def _shellquote(s):
+        return subprocess.list2cmdline([s])
+
+    # No escaping in PATH possible: http://stackoverflow.com/a/29213487/261181
+    # (neither necessary except for colon)
+    env["PATH"] = _add_to_path(os.path.join(exec_prefix, "bin"), env["PATH"])
+    
+    if shutil.which("x-terminal-emulator"):
+        term_cmd = "x-terminal-emulator"
+# Can't use konsole, because it doesn't pass on the environment
+#         elif shutil.which("konsole"):
+#             if (shutil.which("gnome-terminal") 
+#                 and "gnome" in os.environ.get("DESKTOP_SESSION", "").lower()):
+#                 term_cmd = "gnome-terminal"
+#             else:
+#                 term_cmd = "konsole"
+    elif shutil.which("gnome-terminal"):
+        term_cmd = "gnome-terminal"
+    elif shutil.which("terminal"): # XFCE?
+        term_cmd = "terminal"
+    elif shutil.which("xterm"):
+        term_cmd = "xterm"
+    else:
+        raise RuntimeError("Don't know how to open terminal emulator")
+    
+    # Need to prevent shell from closing after executing the command:
+    # http://stackoverflow.com/a/4466566/261181
+    core_cmd = "{interpreter} {explainer}; exec bash -i".format(interpreter=_shellquote(interpreter),
+                                                                    explainer=_shellquote(explainer))
+    in_term_cmd = "bash -c {core_cmd}".format(core_cmd=_shellquote(core_cmd))
+    whole_cmd = "{term_cmd} -e {in_term_cmd}".format(term_cmd=term_cmd,
+                                                     in_term_cmd=_shellquote(in_term_cmd))
+   
+    Popen(whole_cmd, env=env, shell=True)
+
+def _open_shell_in_macos(cwd, env, interpreter, explainer, exec_prefix):
+    _shellquote = shlex.quote
+    
+    # No quoting inside Linux PATH variable is possible: http://stackoverflow.com/a/29213487/261181
+    # (neither necessary except for colon)
+    # Assuming this applies for OS X as well
+    env["PATH"] = _add_to_path(os.path.join(exec_prefix, "bin"), env["PATH"])
+    
+    # osascript "tell application" won't change Terminal's env
+    # (at least when Terminal is already active)
+    # At the moment I just explicitly set some important variables
+    # TODO: Did I miss something?
+    cmd = "PATH={}; unset TK_LIBRARY; unset TCL_LIBRARY".format(_shellquote(env["PATH"]))
+    
+    if "SSL_CERT_FILE" in env:
+        cmd += ";export SSL_CERT_FILE=" + _shellquote(env["SSL_CERT_FILE"])
+        
+    cmd += "; {interpreter} {explainer}".format(
+        interpreter=_shellquote(interpreter),
+        explainer=_shellquote(explainer))
+    
+    # The script will be sent to Terminal with 'do script' command, which takes a string.
+    # We'll prepare an AppleScript string literal for this
+    # (http://stackoverflow.com/questions/10667800/using-quotes-in-a-applescript-string):
+    cmd_as_apple_script_string_literal = ('"' 
+                                             + cmd
+                                             .replace("\\", "\\\\")
+                                             .replace('"', '\\"') 
+                                             + '"')
+    
+    # When Terminal is not open, then do script opens two windows.
+    # do script ... in window 1 would solve this, but if Terminal is already
+    # open, this could run the script in existing terminal (in undesirable env on situation)
+    # That's why I need to prepare two variations of the 'do script' command
+    doScriptCmd1 = """        do script %s """             % cmd_as_apple_script_string_literal
+    doScriptCmd2 = """        do script %s in window 1 """ % cmd_as_apple_script_string_literal
+    
+    # The whole AppleScript will be executed with osascript by giving script
+    # lines as arguments. The lines containing our script need to be shell-quoted:
+    quotedCmd1 = subprocess.list2cmdline([doScriptCmd1])
+    quotedCmd2 = subprocess.list2cmdline([doScriptCmd2])
+    
+    # Now we can finally assemble the osascript command line
+    cmd_line = ("osascript"
+        + """ -e 'if application "Terminal" is running then ' """
+        + """ -e '    tell application "Terminal"           ' """
+        + """ -e """    + quotedCmd1
+        + """ -e '        activate                          ' """
+        + """ -e '    end tell                              ' """
+        + """ -e 'else                                      ' """
+        + """ -e '    tell application "Terminal"           ' """
+        + """ -e """    + quotedCmd2
+        + """ -e '        activate                          ' """
+        + """ -e '    end tell                              ' """
+        + """ -e 'end if                                    ' """
+        )
+    
+    Popen(cmd_line, env=env, shell=True)
+
+def load_plugin():
+    from thonny.globals import get_workbench
+    
+    def open_system_shell_for_selected_interpreter(): 
+        open_system_shell()
+    
+    get_workbench().add_command("OpenSystemShell", "tools", "Open system shell...",
+                    open_system_shell_for_selected_interpreter,
+                    tester=lambda: "system_shell" in get_runner().supported_features(),
+                    group=80)
diff --git a/thonny/plugins/system_shell/explain_environment.py b/thonny/plugins/system_shell/explain_environment.py
new file mode 100644
index 0000000..59ae197
--- /dev/null
+++ b/thonny/plugins/system_shell/explain_environment.py
@@ -0,0 +1,177 @@
+"""Prints information about how should one run python or pip so that the commands
+affect same Python installation that is used for running this script"""
+
+import os.path
+import sys
+import platform
+import subprocess
+from shutil import which
+
+
+def _find_commands(logical_command, reference_output, query_arguments,
+                   only_best=True):
+    """Returns the commands that can be used for running given conceptual command
+    (python or pip)"""
+    
+    def is_correct_command(command):
+        # Don't try to run the command itself, but first expand it to full path.
+        # The location of parent executable seems to affect command search.
+        full_path = which(command)
+        if full_path is None:
+            return False
+
+            
+        try:
+            output = subprocess.check_output([full_path] + query_arguments, 
+                                             universal_newlines=True,
+                                             shell=False)
+            
+            expected = reference_output.strip()
+            actual = output.strip()
+            if platform.system() == "Windows":
+                expected = expected.lower()
+                actual = actual.lower()
+                
+            return expected == actual
+        except:
+            return False
+    
+    correct_commands = set()
+    
+    # first look for short commands
+    for suffix in _get_version_suffixes():
+        command = logical_command + suffix
+        if is_correct_command(command):
+            if " " in command:
+                command = '"' + command + '"'
+                
+            correct_commands.add(command)
+            if only_best:
+                return list(correct_commands)
+    
+    # if no Python found, then use executable
+    if (len(correct_commands) == 0
+        and logical_command == "python" 
+        and platform.system() != "Windows"): # Unixes tend to use symlinks, not Windows
+        correct_commands.add(sys.executable)
+        if only_best:
+            return list(correct_commands)
+    
+    # if still nothing found, then add full paths
+    if len(correct_commands) == 0:
+        if platform.system() == "Windows":
+            exe_suffix = ".exe"
+        else:
+            exe_suffix = ""
+            
+        folders = [sys.exec_prefix, 
+                   os.path.join(sys.exec_prefix, "bin"),
+                   os.path.join(sys.exec_prefix, "Scripts")]
+        
+        for suffix in _get_version_suffixes():
+            command = logical_command + suffix
+            for folder in folders:
+                full_command = os.path.join(folder, command)
+                if os.path.exists(full_command + exe_suffix):
+                    if " " in full_command:
+                        full_command = '"' + full_command + '"'
+                        
+                    correct_commands.add(full_command)
+                    if only_best:
+                        return list(correct_commands)
+    
+    return sorted(correct_commands, key=lambda x: len(x))
+
+def _find_python_commands(only_best=True):
+    return _find_commands("python",
+                         sys.exec_prefix + "\n" + sys.version,
+                         ["-c", "import sys; print(sys.exec_prefix); print(sys.version)"], only_best)
+
+def _find_pip_commands(only_best=True):
+    # Asking pip version is quite slow.
+    # Trying a shortcut for common case:
+    #  if $(which <command>) lives in the same dir as current interpreter
+    #  and we're using Thonny-private venv, 
+    #  we can trust the command is the right one.
+    pref_cmd = "pip" + _get_version_suffixes()[0]
+    pref_cmd_path = which(pref_cmd)
+    if pref_cmd_path:
+        pref_cmd_dir = os.path.dirname(pref_cmd_path)
+        current_exe_dir = os.path.dirname(sys.executable)
+        if (pref_cmd_dir == current_exe_dir
+            and os.path.isfile(os.path.join(current_exe_dir, "is_private"))):
+            return [pref_cmd];
+    
+    # Fallback
+    current_ver_string = _get_pip_version_string()
+    
+    if current_ver_string is not None:
+        commands = _find_commands("pip", current_ver_string, ["--version"], only_best)
+        if len(commands) > 0:
+            return commands
+        else:
+            python_commands = _find_python_commands(True)
+            return [python_commands[0] + " -m pip"]
+    else:
+        return []
+    
+
+def _get_version_suffixes():
+    major = str(sys.version_info.major)
+    minor = "%d.%d" % (sys.version_info.major, sys.version_info.minor)
+    
+    if platform.system() == "Windows":
+        return ["", major, minor]
+    else:
+        return [major, minor, ""]
+    
+
+def _get_pip_version_string():
+    import io
+    try:
+        import pip
+    except ImportError:
+        return None
+    
+    # capture output
+    original_stdout = sys.stdout 
+    try:
+        sys.stdout = io.StringIO()
+        try:
+            pip.main(["--version"])
+        except SystemExit:
+            pass
+        return sys.stdout.getvalue().strip()
+    finally:
+        sys.stdout = original_stdout
+
+
+def _clear_screen():
+    if platform.system() == "Windows":
+        os.system("cls")
+    else:
+        os.system("clear")
+
+if __name__ == "__main__":
+    _clear_screen()
+    print("*" * 80)
+    print("This session is prepared for using Python %s installation in" % platform.python_version())
+    print(" ", os.path.realpath(sys.exec_prefix))
+    print("")
+    print("Command for running the interpreter:")
+    for command in _find_python_commands(True):
+        print(" ", command)
+        
+    print("")
+    print("Command for running pip:")
+    #print(_get_pip_version_string())
+    pip_commands = _find_pip_commands(True)
+    if len(pip_commands) == 0:
+        print(" ", "<pip is not installed>")
+    else:
+        for command in pip_commands:
+            print(" ", command)
+    
+    print("")
+    print("*" * 80)
+    
\ No newline at end of file
diff --git a/thonny/plugins/thonny_folders.py b/thonny/plugins/thonny_folders.py
new file mode 100644
index 0000000..fcb5ab0
--- /dev/null
+++ b/thonny/plugins/thonny_folders.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""Adds commands for opening certain Thonny folders"""
+
+import os.path
+from thonny.misc_utils import running_on_mac_os, running_on_linux,\
+    running_on_windows
+import subprocess
+from thonny.globals import get_workbench
+from thonny import THONNY_USER_DIR
+
+def open_path_in_system_file_manager(path):
+    if running_on_mac_os():
+        # http://stackoverflow.com/a/3520693/261181
+        # -R doesn't allow showing hidden folders
+        subprocess.Popen(["open", path])
+    elif running_on_linux():
+        subprocess.Popen(["xdg-open", path])
+    else:
+        assert running_on_windows()
+        subprocess.Popen(["explorer", path])
+
+
+
+
+def load_plugin():
+    def cmd_open_data_dir():
+        open_path_in_system_file_manager(THONNY_USER_DIR)
+        
+    def cmd_open_program_dir():
+        open_path_in_system_file_manager(get_workbench().get_package_dir())
+        
+    get_workbench().add_command("open_program_dir", "tools", "Open Thonny program folder...",
+                                cmd_open_program_dir, group=110)
+    get_workbench().add_command("open_data_dir", "tools", "Open Thonny data folder...",
+                                cmd_open_data_dir, group=110)
+    
\ No newline at end of file
diff --git a/thonny/plugins/variables.py b/thonny/plugins/variables.py
new file mode 100644
index 0000000..1747b4a
--- /dev/null
+++ b/thonny/plugins/variables.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+
+from thonny.memory import VariablesFrame
+from thonny.globals import get_workbench, get_runner
+from thonny.common import InlineCommand
+
+class GlobalsView(VariablesFrame):
+    def __init__(self, master):
+        VariablesFrame.__init__(self, master)
+        
+        get_workbench().bind("Globals", self._handle_globals_event, True)
+        get_workbench().bind("BackendRestart", lambda e=None: self._clear_tree(), True)
+        get_workbench().bind("DebuggerProgress", self._request_globals, True)
+        get_workbench().bind("ToplevelResult", self._request_globals, True)
+    
+    def before_show(self):
+        self._request_globals(even_when_hidden=True)
+    
+    def _handle_globals_event(self, event):
+        # TODO: handle other modules as well
+        self.update_variables(event.globals)
+    
+    def _request_globals(self, event=None, even_when_hidden=False):
+        if not getattr(self, "hidden", False) or even_when_hidden:
+            # TODO: module_name
+            get_runner().send_command(InlineCommand("get_globals", module_name="__main__"))
+    
+
+def load_plugin():
+    get_workbench().add_view(GlobalsView, "Variables", "ne")
\ No newline at end of file
diff --git a/thonny/res/16x16_blank.gif b/thonny/res/16x16_blank.gif
new file mode 100644
index 0000000..6391db7
Binary files /dev/null and b/thonny/res/16x16_blank.gif differ
diff --git a/thonny/res/1x1_white.gif b/thonny/res/1x1_white.gif
new file mode 100644
index 0000000..90fc402
Binary files /dev/null and b/thonny/res/1x1_white.gif differ
diff --git a/thonny/res/arrow_down2.gif b/thonny/res/arrow_down2.gif
new file mode 100644
index 0000000..b8da159
Binary files /dev/null and b/thonny/res/arrow_down2.gif differ
diff --git a/thonny/res/class.gif b/thonny/res/class.gif
new file mode 100644
index 0000000..4fa0940
Binary files /dev/null and b/thonny/res/class.gif differ
diff --git a/thonny/res/closed_folder.gif b/thonny/res/closed_folder.gif
new file mode 100644
index 0000000..12913fa
Binary files /dev/null and b/thonny/res/closed_folder.gif differ
diff --git a/thonny/res/file.new_file.gif b/thonny/res/file.new_file.gif
new file mode 100644
index 0000000..e21a547
Binary files /dev/null and b/thonny/res/file.new_file.gif differ
diff --git a/thonny/res/file.open_file.gif b/thonny/res/file.open_file.gif
new file mode 100644
index 0000000..a133b34
Binary files /dev/null and b/thonny/res/file.open_file.gif differ
diff --git a/thonny/res/file.save_file.gif b/thonny/res/file.save_file.gif
new file mode 100644
index 0000000..078b53c
Binary files /dev/null and b/thonny/res/file.save_file.gif differ
diff --git a/thonny/res/folder.gif b/thonny/res/folder.gif
new file mode 100644
index 0000000..49e233a
Binary files /dev/null and b/thonny/res/folder.gif differ
diff --git a/thonny/res/generic_file.gif b/thonny/res/generic_file.gif
new file mode 100644
index 0000000..75ff605
Binary files /dev/null and b/thonny/res/generic_file.gif differ
diff --git a/thonny/res/gray_line.gif b/thonny/res/gray_line.gif
new file mode 100644
index 0000000..cd63d79
Binary files /dev/null and b/thonny/res/gray_line.gif differ
diff --git a/thonny/res/hard_drive.gif b/thonny/res/hard_drive.gif
new file mode 100644
index 0000000..26946bf
Binary files /dev/null and b/thonny/res/hard_drive.gif differ
diff --git a/thonny/res/hard_drive2.gif b/thonny/res/hard_drive2.gif
new file mode 100644
index 0000000..4c8b183
Binary files /dev/null and b/thonny/res/hard_drive2.gif differ
diff --git a/thonny/res/method.gif b/thonny/res/method.gif
new file mode 100644
index 0000000..8d1142a
Binary files /dev/null and b/thonny/res/method.gif differ
diff --git a/thonny/res/open_folder.gif b/thonny/res/open_folder.gif
new file mode 100644
index 0000000..55d22f5
Binary files /dev/null and b/thonny/res/open_folder.gif differ
diff --git a/thonny/res/python_file.gif b/thonny/res/python_file.gif
new file mode 100644
index 0000000..0aff5a3
Binary files /dev/null and b/thonny/res/python_file.gif differ
diff --git a/thonny/res/python_icon.gif b/thonny/res/python_icon.gif
new file mode 100644
index 0000000..88f5a4c
Binary files /dev/null and b/thonny/res/python_icon.gif differ
diff --git a/thonny/res/run.debug_current_script.gif b/thonny/res/run.debug_current_script.gif
new file mode 100644
index 0000000..2c9710a
Binary files /dev/null and b/thonny/res/run.debug_current_script.gif differ
diff --git a/thonny/res/run.reset.gif b/thonny/res/run.reset.gif
new file mode 100644
index 0000000..875d61e
Binary files /dev/null and b/thonny/res/run.reset.gif differ
diff --git a/thonny/res/run.run_current_script.gif b/thonny/res/run.run_current_script.gif
new file mode 100644
index 0000000..7093fec
Binary files /dev/null and b/thonny/res/run.run_current_script.gif differ
diff --git a/thonny/res/run.run_to_cursor.gif b/thonny/res/run.run_to_cursor.gif
new file mode 100644
index 0000000..ec2c353
Binary files /dev/null and b/thonny/res/run.run_to_cursor.gif differ
diff --git a/thonny/res/run.step.gif b/thonny/res/run.step.gif
new file mode 100644
index 0000000..b231593
Binary files /dev/null and b/thonny/res/run.step.gif differ
diff --git a/thonny/res/run.step_into.gif b/thonny/res/run.step_into.gif
new file mode 100644
index 0000000..75d165b
Binary files /dev/null and b/thonny/res/run.step_into.gif differ
diff --git a/thonny/res/run.step_out.gif b/thonny/res/run.step_out.gif
new file mode 100644
index 0000000..ee6faff
Binary files /dev/null and b/thonny/res/run.step_out.gif differ
diff --git a/thonny/res/run.step_over.gif b/thonny/res/run.step_over.gif
new file mode 100644
index 0000000..1ec36ae
Binary files /dev/null and b/thonny/res/run.step_over.gif differ
diff --git a/thonny/res/run.stop.gif b/thonny/res/run.stop.gif
new file mode 100644
index 0000000..52858dd
Binary files /dev/null and b/thonny/res/run.stop.gif differ
diff --git a/thonny/res/tab_close.gif b/thonny/res/tab_close.gif
new file mode 100644
index 0000000..5b98831
Binary files /dev/null and b/thonny/res/tab_close.gif differ
diff --git a/thonny/res/tab_close_active.gif b/thonny/res/tab_close_active.gif
new file mode 100644
index 0000000..ad211bf
Binary files /dev/null and b/thonny/res/tab_close_active.gif differ
diff --git a/thonny/res/text_file.gif b/thonny/res/text_file.gif
new file mode 100644
index 0000000..2f1b267
Binary files /dev/null and b/thonny/res/text_file.gif differ
diff --git a/thonny/res/thonny.ico b/thonny/res/thonny.ico
new file mode 100644
index 0000000..760351b
Binary files /dev/null and b/thonny/res/thonny.ico differ
diff --git a/thonny/res/thonny.png b/thonny/res/thonny.png
new file mode 100644
index 0000000..76e3e7f
Binary files /dev/null and b/thonny/res/thonny.png differ
diff --git a/thonny/res/thonny_small.ico b/thonny/res/thonny_small.ico
new file mode 100644
index 0000000..8aaf432
Binary files /dev/null and b/thonny/res/thonny_small.ico differ
diff --git a/thonny/roughparse.py b/thonny/roughparse.py
new file mode 100644
index 0000000..8a60cc7
--- /dev/null
+++ b/thonny/roughparse.py
@@ -0,0 +1,940 @@
+"""Facilities for learning the structure of incomplete Python code
+Mostly copied/adapted from idlelib.HyperParser and idlelib.PyParse
+"""
+
+
+
+import re
+import sys
+from collections import Mapping
+import string
+from keyword import iskeyword
+
+NUM_CONTEXT_LINES = (50, 500, 5000000)
+
+# Reason last stmt is continued (or C_NONE if it's not).
+(C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE,
+ C_STRING_NEXT_LINES, C_BRACKET) = range(5)
+
+if 0:   # for throwaway debugging output
+    def dump(*stuff):
+        sys.__stdout__.write(" ".join(map(str, stuff)) + "\n")
+
+# Find what looks like the start of a popular stmt.
+
+_synchre = re.compile(r"""
+    ^
+    [ \t]*
+    (?: while
+    |   else
+    |   def
+    |   return
+    |   assert
+    |   break
+    |   class
+    |   continue
+    |   elif
+    |   try
+    |   except
+    |   raise
+    |   import
+    |   yield
+    )
+    \b
+""", re.VERBOSE | re.MULTILINE).search
+
+# Match blank line or non-indenting comment line.
+
+_junkre = re.compile(r"""
+    [ \t]*
+    (?: \# \S .* )?
+    \n
+""", re.VERBOSE).match
+
+# Match any flavor of string; the terminating quote is optional
+# so that we're robust in the face of incomplete program text.
+
+_match_stringre = re.compile(r"""
+    \""" [^"\\]* (?:
+                     (?: \\. | "(?!"") )
+                     [^"\\]*
+                 )*
+    (?: \""" )?
+
+|   " [^"\\\n]* (?: \\. [^"\\\n]* )* "?
+
+|   ''' [^'\\]* (?:
+                   (?: \\. | '(?!'') )
+                   [^'\\]*
+                )*
+    (?: ''' )?
+
+|   ' [^'\\\n]* (?: \\. [^'\\\n]* )* '?
+""", re.VERBOSE | re.DOTALL).match
+
+# Match a line that starts with something interesting;
+# used to find the first item of a bracket structure.
+
+_itemre = re.compile(r"""
+    [ \t]*
+    [^\s#\\]    # if we match, m.end()-1 is the interesting char
+""", re.VERBOSE).match
+
+# Match start of stmts that should be followed by a dedent.
+
+_closere = re.compile(r"""
+    \s*
+    (?: return
+    |   break
+    |   continue
+    |   raise
+    |   pass
+    )
+    \b
+""", re.VERBOSE).match
+
+# Chew up non-special chars as quickly as possible.  If match is
+# successful, m.end() less 1 is the index of the last boring char
+# matched.  If match is unsuccessful, the string starts with an
+# interesting char.
+
+_chew_ordinaryre = re.compile(r"""
+    [^[\](){}#'"\\]+
+""", re.VERBOSE).match
+
+
+class StringTranslatePseudoMapping(Mapping):
+    r"""Utility class to be used with str.translate()
+
+    This Mapping class wraps a given dict. When a value for a key is
+    requested via __getitem__() or get(), the key is looked up in the
+    given dict. If found there, the value from the dict is returned.
+    Otherwise, the default value given upon initialization is returned.
+
+    This allows using str.translate() to make some replacements, and to
+    replace all characters for which no replacement was specified with
+    a given character instead of leaving them as-is.
+
+    For example, to replace everything except whitespace with 'x':
+
+    >>> whitespace_chars = ' \t\n\r'
+    >>> preserve_dict = {ord(c): ord(c) for c in whitespace_chars}
+    >>> mapping = StringTranslatePseudoMapping(preserve_dict, ord('x'))
+    >>> text = "a + b\tc\nd"
+    >>> text.translate(mapping)
+    'x x x\tx\nx'
+    """
+    def __init__(self, non_defaults, default_value):
+        self._non_defaults = non_defaults
+        self._default_value = default_value
+
+        def _get(key, _get=non_defaults.get, _default=default_value):
+            return _get(key, _default)
+        self._get = _get
+
+    def __getitem__(self, item):
+        return self._get(item)
+
+    def __len__(self):
+        return len(self._non_defaults)
+
+    def __iter__(self):
+        return iter(self._non_defaults)
+
+    def get(self, key, default=None):
+        return self._get(key)
+
+
+class RoughParser:
+
+    def __init__(self, indentwidth, tabwidth):
+        self.indentwidth = indentwidth
+        self.tabwidth = tabwidth
+
+    def set_str(self, s):
+        assert len(s) == 0 or s[-1] == '\n'
+        self.str = s
+        self.study_level = 0
+
+    # Return index of a good place to begin parsing, as close to the
+    # end of the string as possible.  This will be the start of some
+    # popular stmt like "if" or "def".  Return None if none found:
+    # the caller should pass more prior context then, if possible, or
+    # if not (the entire program text up until the point of interest
+    # has already been tried) pass 0 to set_lo.
+    #
+    # This will be reliable iff given a reliable is_char_in_string
+    # function, meaning that when it says "no", it's absolutely
+    # guaranteed that the char is not in a string.
+
+    def find_good_parse_start(self, is_char_in_string=None,
+                              _synchre=_synchre):
+        str, pos = self.str, None  # @ReservedAssignment
+
+        if not is_char_in_string:
+            # no clue -- make the caller pass everything
+            return None
+
+        # Peek back from the end for a good place to start,
+        # but don't try too often; pos will be left None, or
+        # bumped to a legitimate synch point.
+        limit = len(str)
+        for tries in range(5):  # @UnusedVariable
+            i = str.rfind(":\n", 0, limit)
+            if i < 0:
+                break
+            i = str.rfind('\n', 0, i) + 1  # start of colon line
+            m = _synchre(str, i, limit)
+            if m and not is_char_in_string(m.start()):
+                pos = m.start()
+                break
+            limit = i
+        if pos is None:
+            # Nothing looks like a block-opener, or stuff does
+            # but is_char_in_string keeps returning true; most likely
+            # we're in or near a giant string, the colorizer hasn't
+            # caught up enough to be helpful, or there simply *aren't*
+            # any interesting stmts.  In any of these cases we're
+            # going to have to parse the whole thing to be sure, so
+            # give it one last try from the start, but stop wasting
+            # time here regardless of the outcome.
+            m = _synchre(str)
+            if m and not is_char_in_string(m.start()):
+                pos = m.start()
+            return pos
+
+        # Peeking back worked; look forward until _synchre no longer
+        # matches.
+        i = pos + 1
+        while 1:
+            m = _synchre(str, i)
+            if m:
+                s, i = m.span()
+                if not is_char_in_string(s):
+                    pos = s
+            else:
+                break
+        return pos
+
+    # Throw away the start of the string.  Intended to be called with
+    # find_good_parse_start's result.
+
+    def set_lo(self, lo):
+        assert lo == 0 or self.str[lo-1] == '\n'
+        if lo > 0:
+            self.str = self.str[lo:]
+
+    # Build a translation table to map uninteresting chars to 'x', open
+    # brackets to '(', close brackets to ')' while preserving quotes,
+    # backslashes, newlines and hashes. This is to be passed to
+    # str.translate() in _study1().
+    _tran = {}
+    _tran.update((ord(c), ord('(')) for c in "({[")
+    _tran.update((ord(c), ord(')')) for c in ")}]")
+    _tran.update((ord(c), ord(c)) for c in "\"'\\\n#")
+    _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x'))
+
+    # As quickly as humanly possible <wink>, find the line numbers (0-
+    # based) of the non-continuation lines.
+    # Creates self.{goodlines, continuation}.
+
+    def _study1(self):
+        if self.study_level >= 1:
+            return
+        self.study_level = 1
+
+        # Map all uninteresting characters to "x", all open brackets
+        # to "(", all close brackets to ")", then collapse runs of
+        # uninteresting characters.  This can cut the number of chars
+        # by a factor of 10-40, and so greatly speed the following loop.
+        str = (self.str  # @ReservedAssignment
+                .translate(self._tran)
+                .replace('xxxxxxxx', 'x')
+                .replace('xxxx', 'x')
+                .replace('xx', 'x')
+                .replace('xx', 'x')
+                .replace('\nx', '\n'))
+        # note that replacing x\n with \n would be incorrect, because
+        # x may be preceded by a backslash
+
+        # March over the squashed version of the program, accumulating
+        # the line numbers of non-continued stmts, and determining
+        # whether & why the last stmt is a continuation.
+        continuation = C_NONE
+        level = lno = 0     # level is nesting level; lno is line number
+        self.goodlines = goodlines = [0]
+        push_good = goodlines.append
+        i, n = 0, len(str)
+        while i < n:
+            ch = str[i]
+            i = i+1
+
+            # cases are checked in decreasing order of frequency
+            if ch == 'x':
+                continue
+
+            if ch == '\n':
+                lno = lno + 1
+                if level == 0:
+                    push_good(lno)
+                    # else we're in an unclosed bracket structure
+                continue
+
+            if ch == '(':
+                level = level + 1
+                continue
+
+            if ch == ')':
+                if level:
+                    level = level - 1
+                    # else the program is invalid, but we can't complain
+                continue
+
+            if ch == '"' or ch == "'":
+                # consume the string
+                quote = ch
+                if str[i-1:i+2] == quote * 3:
+                    quote = quote * 3
+                firstlno = lno
+                w = len(quote) - 1
+                i = i+w
+                while i < n:
+                    ch = str[i]
+                    i = i+1
+
+                    if ch == 'x':
+                        continue
+
+                    if str[i-1:i+w] == quote:
+                        i = i+w
+                        break
+
+                    if ch == '\n':
+                        lno = lno + 1
+                        if w == 0:
+                            # unterminated single-quoted string
+                            if level == 0:
+                                push_good(lno)
+                            break
+                        continue
+
+                    if ch == '\\':
+                        assert i < n
+                        if str[i] == '\n':
+                            lno = lno + 1
+                        i = i+1
+                        continue
+
+                    # else comment char or paren inside string
+
+                else:
+                    # didn't break out of the loop, so we're still
+                    # inside a string
+                    if (lno - 1) == firstlno:
+                        # before the previous \n in str, we were in the first
+                        # line of the string
+                        continuation = C_STRING_FIRST_LINE
+                    else:
+                        continuation = C_STRING_NEXT_LINES
+                continue    # with outer loop
+
+            if ch == '#':
+                # consume the comment
+                i = str.find('\n', i)
+                assert i >= 0
+                continue
+
+            assert ch == '\\'
+            assert i < n
+            if str[i] == '\n':
+                lno = lno + 1
+                if i+1 == n:
+                    continuation = C_BACKSLASH
+            i = i+1
+
+        # The last stmt may be continued for all 3 reasons.
+        # String continuation takes precedence over bracket
+        # continuation, which beats backslash continuation.
+        if (continuation != C_STRING_FIRST_LINE
+            and continuation != C_STRING_NEXT_LINES and level > 0):
+            continuation = C_BRACKET
+        self.continuation = continuation
+
+        # Push the final line number as a sentinel value, regardless of
+        # whether it's continued.
+        assert (continuation == C_NONE) == (goodlines[-1] == lno)
+        if goodlines[-1] != lno:
+            push_good(lno)
+
+    def get_continuation_type(self):
+        self._study1()
+        return self.continuation
+
+    # study1 was sufficient to determine the continuation status,
+    # but doing more requires looking at every character.  study2
+    # does this for the last interesting statement in the block.
+    # Creates:
+    #     self.stmt_start, stmt_end
+    #         slice indices of last interesting stmt
+    #     self.stmt_bracketing
+    #         the bracketing structure of the last interesting stmt;
+    #         for example, for the statement "say(boo) or die", stmt_bracketing
+    #         will be [(0, 0), (3, 1), (8, 0)]. Strings and comments are
+    #         treated as brackets, for the matter.
+    #     self.lastch
+    #         last non-whitespace character before optional trailing
+    #         comment
+    #     self.lastopenbracketpos
+    #         if continuation is C_BRACKET, index of last open bracket
+
+    def _study2(self):
+        if self.study_level >= 2:
+            return
+        self._study1()
+        self.study_level = 2
+
+        # Set p and q to slice indices of last interesting stmt.
+        str, goodlines = self.str, self.goodlines  # @ReservedAssignment
+        i = len(goodlines) - 1
+        p = len(str)    # index of newest line
+        while i:
+            assert p
+            # p is the index of the stmt at line number goodlines[i].
+            # Move p back to the stmt at line number goodlines[i-1].
+            q = p
+            for nothing in range(goodlines[i-1], goodlines[i]):  # @UnusedVariable
+                # tricky: sets p to 0 if no preceding newline
+                p = str.rfind('\n', 0, p-1) + 1
+            # The stmt str[p:q] isn't a continuation, but may be blank
+            # or a non-indenting comment line.
+            if  _junkre(str, p):
+                i = i-1
+            else:
+                break
+        if i == 0:
+            # nothing but junk!
+            assert p == 0
+            q = p
+        self.stmt_start, self.stmt_end = p, q
+
+        # Analyze this stmt, to find the last open bracket (if any)
+        # and last interesting character (if any).
+        lastch = ""
+        stack = []  # stack of open bracket indices
+        push_stack = stack.append
+        bracketing = [(p, 0)]
+        while p < q:
+            # suck up all except ()[]{}'"#\\
+            m = _chew_ordinaryre(str, p, q)
+            if m:
+                # we skipped at least one boring char
+                newp = m.end()
+                # back up over totally boring whitespace
+                i = newp - 1    # index of last boring char
+                while i >= p and str[i] in " \t\n":
+                    i = i-1
+                if i >= p:
+                    lastch = str[i]
+                p = newp
+                if p >= q:
+                    break
+
+            ch = str[p]
+
+            if ch in "([{":
+                push_stack(p)
+                bracketing.append((p, len(stack)))
+                lastch = ch
+                p = p+1
+                continue
+
+            if ch in ")]}":
+                if stack:
+                    del stack[-1]
+                lastch = ch
+                p = p+1
+                bracketing.append((p, len(stack)))
+                continue
+
+            if ch == '"' or ch == "'":
+                # consume string
+                # Note that study1 did this with a Python loop, but
+                # we use a regexp here; the reason is speed in both
+                # cases; the string may be huge, but study1 pre-squashed
+                # strings to a couple of characters per line.  study1
+                # also needed to keep track of newlines, and we don't
+                # have to.
+                bracketing.append((p, len(stack)+1))
+                lastch = ch
+                p = _match_stringre(str, p, q).end()
+                bracketing.append((p, len(stack)))
+                continue
+
+            if ch == '#':
+                # consume comment and trailing newline
+                bracketing.append((p, len(stack)+1))
+                p = str.find('\n', p, q) + 1
+                assert p > 0
+                bracketing.append((p, len(stack)))
+                continue
+
+            assert ch == '\\'
+            p = p+1     # beyond backslash
+            assert p < q
+            if str[p] != '\n':
+                # the program is invalid, but can't complain
+                lastch = ch + str[p]
+            p = p+1     # beyond escaped char
+
+        # end while p < q:
+
+        self.lastch = lastch
+        if stack:
+            self.lastopenbracketpos = stack[-1]
+        self.stmt_bracketing = tuple(bracketing)
+
+    # Assuming continuation is C_BRACKET, return the number
+    # of spaces the next line should be indented.
+
+    def compute_bracket_indent(self):
+        self._study2()
+        assert self.continuation == C_BRACKET
+        j = self.lastopenbracketpos
+        str = self.str  # @ReservedAssignment
+        n = len(str)
+        origi = i = str.rfind('\n', 0, j) + 1
+        j = j+1     # one beyond open bracket
+        # find first list item; set i to start of its line
+        while j < n:
+            m = _itemre(str, j)
+            if m:
+                j = m.end() - 1     # index of first interesting char
+                extra = 0
+                break
+            else:
+                # this line is junk; advance to next line
+                i = j = str.find('\n', j) + 1
+        else:
+            # nothing interesting follows the bracket;
+            # reproduce the bracket line's indentation + a level
+            j = i = origi
+            while str[j] in " \t":
+                j = j+1
+            extra = self.indentwidth
+        return len(str[i:j].expandtabs(self.tabwidth)) + extra
+
+    # Return number of physical lines in last stmt (whether or not
+    # it's an interesting stmt!  this is intended to be called when
+    # continuation is C_BACKSLASH).
+
+    def get_num_lines_in_stmt(self):
+        self._study1()
+        goodlines = self.goodlines
+        return goodlines[-1] - goodlines[-2]
+
+    # Assuming continuation is C_BACKSLASH, return the number of spaces
+    # the next line should be indented.  Also assuming the new line is
+    # the first one following the initial line of the stmt.
+
+    def compute_backslash_indent(self):
+        self._study2()
+        assert self.continuation == C_BACKSLASH
+        str = self.str  # @ReservedAssignment
+        i = self.stmt_start
+        while str[i] in " \t":
+            i = i+1
+        startpos = i
+
+        # See whether the initial line starts an assignment stmt; i.e.,
+        # look for an = operator
+        endpos = str.find('\n', startpos) + 1
+        found = level = 0
+        while i < endpos:
+            ch = str[i]
+            if ch in "([{":
+                level = level + 1
+                i = i+1
+            elif ch in ")]}":
+                if level:
+                    level = level - 1
+                i = i+1
+            elif ch == '"' or ch == "'":
+                i = _match_stringre(str, i, endpos).end()
+            elif ch == '#':
+                break
+            elif level == 0 and ch == '=' and \
+                   (i == 0 or str[i-1] not in "=<>!") and \
+                   str[i+1] != '=':
+                found = 1
+                break
+            else:
+                i = i+1
+
+        if found:
+            # found a legit =, but it may be the last interesting
+            # thing on the line
+            i = i+1     # move beyond the =
+            found = re.match(r"\s*\\", str[i:endpos]) is None
+
+        if not found:
+            # oh well ... settle for moving beyond the first chunk
+            # of non-whitespace chars
+            i = startpos
+            while str[i] not in " \t\n":
+                i = i+1
+
+        return len(str[self.stmt_start:i].expandtabs(\
+                                     self.tabwidth)) + 1
+
+    # Return the leading whitespace on the initial line of the last
+    # interesting stmt.
+
+    def get_base_indent_string(self):
+        self._study2()
+        i, n = self.stmt_start, self.stmt_end
+        j = i
+        str_ = self.str
+        while j < n and str_[j] in " \t":
+            j = j + 1
+        return str_[i:j]
+
+    # Did the last interesting stmt open a block?
+
+    def is_block_opener(self):
+        self._study2()
+        return self.lastch == ':'
+
+    # Did the last interesting stmt close a block?
+
+    def is_block_closer(self):
+        self._study2()
+        return _closere(self.str, self.stmt_start) is not None
+
+    # index of last open bracket ({[, or None if none
+    lastopenbracketpos = None
+
+    def get_last_open_bracket_pos(self):
+        self._study2()
+        return self.lastopenbracketpos
+
+    # the structure of the bracketing of the last interesting statement,
+    # in the format defined in _study2, or None if the text didn't contain
+    # anything
+    stmt_bracketing = None
+
+    def get_last_stmt_bracketing(self):
+        self._study2()
+        return self.stmt_bracketing
+
+
+
+
+# all ASCII chars that may be in an identifier
+_ASCII_ID_CHARS = frozenset(string.ascii_letters + string.digits + "_")
+# all ASCII chars that may be the first char of an identifier
+_ASCII_ID_FIRST_CHARS = frozenset(string.ascii_letters + "_")
+
+# lookup table for whether 7-bit ASCII chars are valid in a Python identifier
+_IS_ASCII_ID_CHAR = [(chr(x) in _ASCII_ID_CHARS) for x in range(128)]
+# lookup table for whether 7-bit ASCII chars are valid as the first
+# char in a Python identifier
+_IS_ASCII_ID_FIRST_CHAR = \
+    [(chr(x) in _ASCII_ID_FIRST_CHARS) for x in range(128)]
+
+
+class HyperParser:
+    """Provide advanced parsing abilities for ParenMatch and other extensions.
+    
+    HyperParser uses PyParser.  PyParser mostly gives information on the
+    proper indentation of code.  HyperParser gives additional information on
+    the structure of code.
+    """
+    def __init__(self, text, index):
+        "To initialize, analyze the surroundings of the given index."
+
+        self.text = text
+
+        parser = RoughParser(text.indentwidth, text.tabwidth)
+
+        def index2line(index):
+            return int(float(index))
+        lno = index2line(text.index(index))
+
+        for context in NUM_CONTEXT_LINES:
+            startat = max(lno - context, 1)
+            startatindex = repr(startat) + ".0"
+            stopatindex = "%d.end" % lno
+            # We add the newline because PyParse requires a newline
+            # at end. We add a space so that index won't be at end
+            # of line, so that its status will be the same as the
+            # char before it, if should.
+            parser.set_str(text.get(startatindex, stopatindex)+' \n')
+            bod = parser.find_good_parse_start(
+                      _build_char_in_string_func(startatindex))
+            if bod is not None or startat == 1:
+                break
+        parser.set_lo(bod or 0)
+
+        # We want what the parser has, minus the last newline and space.
+        self.rawtext = parser.str[:-2]
+        # Parser.str apparently preserves the statement we are in, so
+        # that stopatindex can be used to synchronize the string with
+        # the text box indices.
+        self.stopatindex = stopatindex
+        self.bracketing = parser.get_last_stmt_bracketing()
+        # find which pairs of bracketing are openers. These always
+        # correspond to a character of rawtext.
+        self.isopener = [i>0 and self.bracketing[i][1] >
+                         self.bracketing[i-1][1]
+                         for i in range(len(self.bracketing))]
+
+        self.set_index(index)
+
+    def set_index(self, index):
+        """Set the index to which the functions relate.
+
+        The index must be in the same statement.
+        """
+        indexinrawtext = (len(self.rawtext) -
+                          len(self.text.get(index, self.stopatindex)))
+        if indexinrawtext < 0:
+            raise ValueError("Index %s precedes the analyzed statement"
+                             % index)
+        self.indexinrawtext = indexinrawtext
+        # find the rightmost bracket to which index belongs
+        self.indexbracket = 0
+        while (self.indexbracket < len(self.bracketing)-1 and
+               self.bracketing[self.indexbracket+1][0] < self.indexinrawtext):
+            self.indexbracket += 1
+        if (self.indexbracket < len(self.bracketing)-1 and
+            self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and
+           not self.isopener[self.indexbracket+1]):
+            self.indexbracket += 1
+
+    def is_in_string(self):
+        """Is the index given to the HyperParser in a string?"""
+        # The bracket to which we belong should be an opener.
+        # If it's an opener, it has to have a character.
+        return (self.isopener[self.indexbracket] and
+                self.rawtext[self.bracketing[self.indexbracket][0]]
+                in ('"', "'"))
+
+    def is_in_code(self):
+        """Is the index given to the HyperParser in normal code?"""
+        return (not self.isopener[self.indexbracket] or
+                self.rawtext[self.bracketing[self.indexbracket][0]]
+                not in ('#', '"', "'"))
+
+    def get_surrounding_brackets(self, openers='([{', mustclose=False):
+        """Return bracket indexes or None.
+
+        If the index given to the HyperParser is surrounded by a
+        bracket defined in openers (or at least has one before it),
+        return the indices of the opening bracket and the closing
+        bracket (or the end of line, whichever comes first).
+
+        If it is not surrounded by brackets, or the end of line comes
+        before the closing bracket and mustclose is True, returns None.
+        """
+
+        bracketinglevel = self.bracketing[self.indexbracket][1]
+        before = self.indexbracket
+        while (not self.isopener[before] or
+              self.rawtext[self.bracketing[before][0]] not in openers or
+              self.bracketing[before][1] > bracketinglevel):
+            before -= 1
+            if before < 0:
+                return None
+            bracketinglevel = min(bracketinglevel, self.bracketing[before][1])
+        after = self.indexbracket + 1
+        while (after < len(self.bracketing) and
+              self.bracketing[after][1] >= bracketinglevel):
+            after += 1
+
+        beforeindex = self.text.index("%s-%dc" %
+            (self.stopatindex, len(self.rawtext)-self.bracketing[before][0]))
+        if (after >= len(self.bracketing) or
+           self.bracketing[after][0] > len(self.rawtext)):
+            if mustclose:
+                return None
+            afterindex = self.stopatindex
+        else:
+            # We are after a real char, so it is a ')' and we give the
+            # index before it.
+            afterindex = self.text.index(
+                "%s-%dc" % (self.stopatindex,
+                 len(self.rawtext)-(self.bracketing[after][0]-1)))
+
+        return beforeindex, afterindex
+
+    # the set of built-in identifiers which are also keywords,
+    # i.e. keyword.iskeyword() returns True for them
+    _ID_KEYWORDS = frozenset({"True", "False", "None"})
+
+    @classmethod
+    def _eat_identifier(cls, str, limit, pos):
+        """Given a string and pos, return the number of chars in the
+        identifier which ends at pos, or 0 if there is no such one.
+
+        This ignores non-identifier eywords are not identifiers.
+        """
+        is_ascii_id_char = _IS_ASCII_ID_CHAR
+
+        # Start at the end (pos) and work backwards.
+        i = pos
+
+        # Go backwards as long as the characters are valid ASCII
+        # identifier characters. This is an optimization, since it
+        # is faster in the common case where most of the characters
+        # are ASCII.
+        while i > limit and (
+                ord(str[i - 1]) < 128 and
+                is_ascii_id_char[ord(str[i - 1])]
+        ):
+            i -= 1
+
+        # If the above loop ended due to reaching a non-ASCII
+        # character, continue going backwards using the most generic
+        # test for whether a string contains only valid identifier
+        # characters.
+        if i > limit and ord(str[i - 1]) >= 128:
+            while i - 4 >= limit and ('a' + str[i - 4:pos]).isidentifier():
+                i -= 4
+            if i - 2 >= limit and ('a' + str[i - 2:pos]).isidentifier():
+                i -= 2
+            if i - 1 >= limit and ('a' + str[i - 1:pos]).isidentifier():
+                i -= 1
+
+            # The identifier candidate starts here. If it isn't a valid
+            # identifier, don't eat anything. At this point that is only
+            # possible if the first character isn't a valid first
+            # character for an identifier.
+            if not str[i:pos].isidentifier():
+                return 0
+        elif i < pos:
+            # All characters in str[i:pos] are valid ASCII identifier
+            # characters, so it is enough to check that the first is
+            # valid as the first character of an identifier.
+            if not _IS_ASCII_ID_FIRST_CHAR[ord(str[i])]:
+                return 0
+
+        # All keywords are valid identifiers, but should not be
+        # considered identifiers here, except for True, False and None.
+        if i < pos and (
+                iskeyword(str[i:pos]) and
+                str[i:pos] not in cls._ID_KEYWORDS
+        ):
+            return 0
+
+        return pos - i
+
+    # This string includes all chars that may be in a white space
+    _whitespace_chars = " \t\n\\"
+
+    def get_expression(self):
+        """Return a string with the Python expression which ends at the
+        given index, which is empty if there is no real one.
+        """
+        if not self.is_in_code():
+            raise ValueError("get_expression should only be called"
+                             "if index is inside a code.")
+
+        rawtext = self.rawtext
+        bracketing = self.bracketing
+
+        brck_index = self.indexbracket
+        brck_limit = bracketing[brck_index][0]
+        pos = self.indexinrawtext
+
+        last_identifier_pos = pos
+        postdot_phase = True
+
+        while 1:
+            # Eat whitespaces, comments, and if postdot_phase is False - a dot
+            while 1:
+                if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars:
+                    # Eat a whitespace
+                    pos -= 1
+                elif (not postdot_phase and
+                      pos > brck_limit and rawtext[pos-1] == '.'):
+                    # Eat a dot
+                    pos -= 1
+                    postdot_phase = True
+                # The next line will fail if we are *inside* a comment,
+                # but we shouldn't be.
+                elif (pos == brck_limit and brck_index > 0 and
+                      rawtext[bracketing[brck_index-1][0]] == '#'):
+                    # Eat a comment
+                    brck_index -= 2
+                    brck_limit = bracketing[brck_index][0]
+                    pos = bracketing[brck_index+1][0]
+                else:
+                    # If we didn't eat anything, quit.
+                    break
+
+            if not postdot_phase:
+                # We didn't find a dot, so the expression end at the
+                # last identifier pos.
+                break
+
+            ret = self._eat_identifier(rawtext, brck_limit, pos)
+            if ret:
+                # There is an identifier to eat
+                pos = pos - ret
+                last_identifier_pos = pos
+                # Now, to continue the search, we must find a dot.
+                postdot_phase = False
+                # (the loop continues now)
+
+            elif pos == brck_limit:
+                # We are at a bracketing limit. If it is a closing
+                # bracket, eat the bracket, otherwise, stop the search.
+                level = bracketing[brck_index][1]
+                while brck_index > 0 and bracketing[brck_index-1][1] > level:
+                    brck_index -= 1
+                if bracketing[brck_index][0] == brck_limit:
+                    # We were not at the end of a closing bracket
+                    break
+                pos = bracketing[brck_index][0]
+                brck_index -= 1
+                brck_limit = bracketing[brck_index][0]
+                last_identifier_pos = pos
+                if rawtext[pos] in "([":
+                    # [] and () may be used after an identifier, so we
+                    # continue. postdot_phase is True, so we don't allow a dot.
+                    pass
+                else:
+                    # We can't continue after other types of brackets
+                    if rawtext[pos] in "'\"":
+                        # Scan a string prefix
+                        while pos > 0 and rawtext[pos - 1] in "rRbBuU":
+                            pos -= 1
+                        last_identifier_pos = pos
+                    break
+
+            else:
+                # We've found an operator or something.
+                break
+
+        return rawtext[last_identifier_pos:self.indexinrawtext]
+
+
+
+def _is_char_in_string(text_index):
+    # in idlelib.EditorWindow this used info from colorer
+    # to speed up things, but I dont want to rely on this
+    return 1
+    
+def _build_char_in_string_func(startindex):
+    # copied from idlelib.EditorWindow (Python 3.4.2)
+    
+    # Our editwin provides a _is_char_in_string function that works
+    # with a Tk text index, but PyParse only knows about offsets into
+    # a string. This builds a function for PyParse that accepts an
+    # offset.
+
+    def inner(offset, _startindex=startindex,
+              _icis=_is_char_in_string):
+        return _icis(_startindex + "+%dc" % offset)
+    return inner
+
diff --git a/thonny/running.py b/thonny/running.py
new file mode 100644
index 0000000..759d3e8
--- /dev/null
+++ b/thonny/running.py
@@ -0,0 +1,1106 @@
+# -*- coding: utf-8 -*-
+
+"""Code for maintaining the background process and for running
+user programs
+
+Commands get executed via shell, this way the command line in the 
+shell becomes kind of title for the execution.
+
+""" 
+
+
+from _thread import start_new_thread
+from logging import debug
+import os.path
+import subprocess
+import sys
+
+from thonny.common import serialize_message, ToplevelCommand, \
+    InlineCommand, parse_shell_command, \
+    CommandSyntaxError, parse_message, DebuggerCommand, InputSubmission,\
+    UserError
+from thonny.globals import get_workbench, get_runner
+import shlex
+from thonny import THONNY_USER_DIR
+from thonny.misc_utils import running_on_windows, running_on_mac_os, eqfn
+from shutil import which
+import shutil
+import tokenize
+import collections
+import signal
+import logging
+from time import sleep
+
+
+DEFAULT_CPYTHON_INTERPRETER = "default"
+SAME_AS_FRONTEND_INTERPRETER = "same as front-end"
+WINDOWS_EXE = "python.exe"
+
+class Runner:
+    def __init__(self):
+        get_workbench().set_default("run.working_directory", os.path.expanduser("~"))
+        get_workbench().set_default("run.auto_cd", True)
+        get_workbench().set_default("run.backend_configuration", "Python (%s)" % DEFAULT_CPYTHON_INTERPRETER)
+        get_workbench().set_default("run.used_interpreters", [])
+        get_workbench().add_backend("Python", CPythonProxy)
+        
+        from thonny.shell import ShellView
+        get_workbench().add_view(ShellView, "Shell", "s",
+            visible_by_default=True,
+            default_position_key='A')
+        
+        self._init_commands()
+        
+        self._state = None
+        self._proxy = None
+        self._postponed_commands = []
+        self._current_toplevel_command = None
+        self._current_command = None
+        
+        self._check_alloc_console()
+    
+    def start(self):
+        try:
+            self.reset_backend()
+        finally:
+            self._poll_vm_messages()
+    
+    def _init_commands(self):
+        shell = get_workbench().get_view("ShellView")
+        shell.add_command("Run", self.handle_execute_from_shell)
+        shell.add_command("Reset", self._handle_reset_from_shell)
+        shell.add_command("cd", self._handle_cd_from_shell)
+        
+        get_workbench().add_command('run_current_script', "run", 'Run current script',
+            handler=self._cmd_run_current_script,
+            default_sequence="<F5>",
+            tester=self._cmd_run_current_script_enabled,
+            group=10,
+            image_filename="run.run_current_script.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command('reset', "run", 'Interrupt/Reset',
+            handler=self.cmd_interrupt_reset,
+            default_sequence="<Control-F2>",
+            tester=self._cmd_interrupt_reset_enabled,
+            group=70,
+            image_filename="run.stop.gif",
+            include_in_toolbar=True)
+        
+        get_workbench().add_command('interrupt', "run", "Interrupt execution",
+            handler=self._cmd_interrupt,
+            tester=self._cmd_interrupt_enabled,
+            default_sequence="<Control-c>",
+            bell_when_denied=False)
+    
+    def get_cwd(self):
+        # TODO: make it nicer
+        if hasattr(self._proxy, "cwd"):
+            return self._proxy.cwd
+        else:
+            return ""
+    
+    def get_state(self):
+        """State is one of "running", "waiting_input", "waiting_debugger_command",
+            "waiting_toplevel_command"
+        """
+        return self._state
+    
+    def _set_state(self, state):
+        if self._state != state:
+            logging.debug("Runner state changed: %s ==> %s" % (self._state, state))
+            self._state = state
+            if self._state == "waiting_toplevel_command":
+                self._current_toplevel_command = None
+            
+            if self._state != "running":
+                self._current_command = None
+    
+    def get_current_toplevel_command(self):
+        return self._current_toplevel_command
+            
+    def get_current_command(self):
+        return self._current_command
+            
+    def get_sys_path(self):
+        return self._proxy.get_sys_path()
+    
+    def send_command(self, cmd):
+        if self._proxy is None:
+            return
+        
+        if not self._state_is_suitable(cmd):
+            if isinstance(cmd, DebuggerCommand) and self.get_state() == "running":
+                # probably waiting behind some InlineCommand
+                self._postpone_command(cmd)
+                return
+            elif isinstance(cmd, InlineCommand):
+                self._postpone_command(cmd)
+                return
+            else:
+                raise AssertionError("Trying to send " + str(cmd) + " in state " + self.get_state())
+        
+        if cmd.command in ("Run", "Debug", "Reset"):
+            get_workbench().event_generate("BackendRestart")
+        
+        accepted = self._proxy.send_command(cmd)
+        
+        if (accepted and isinstance(cmd, (ToplevelCommand, DebuggerCommand, InlineCommand))):
+            self._set_state("running")
+            self._current_command = cmd
+            if isinstance(cmd, ToplevelCommand):
+                self._current_toplevel_command = cmd
+        
+    
+    def send_program_input(self, data):
+        assert self.get_state() == "waiting_input"
+        self._proxy.send_program_input(data)
+        self._set_state("running")
+        
+    def execute_script(self, script_path, args, working_directory=None, command_name="Run"):
+        if (working_directory is not None and self._proxy.cwd != working_directory):
+            # create compound command
+            # start with %cd
+            cmd_line = "%cd " + shlex.quote(working_directory) + "\n"
+            next_cwd = working_directory
+        else:
+            # create simple command
+            cmd_line = ""
+            next_cwd = self._proxy.cwd
+        
+        # append main command (Run, run, Debug or debug)
+        rel_filename = os.path.relpath(script_path, next_cwd)
+        cmd_line += "%" + command_name + " " + shlex.quote(rel_filename)
+        
+        # append args
+        for arg in args:
+            cmd_line += " " + shlex.quote(arg) 
+        
+        cmd_line += "\n"
+        
+        # submit to shell (shell will execute it)
+        get_workbench().get_view("ShellView").submit_command(cmd_line)
+        
+    def execute_current(self, command_name, always_change_to_script_dir=False):
+        """
+        This method's job is to create a command for running/debugging
+        current file/script and submit it to shell
+        """
+        
+        editor = get_workbench().get_current_editor()
+        if not editor:
+            return
+
+        filename = editor.get_filename(True)
+        if not filename:
+            return
+        
+        if editor.is_modified():
+            filename = editor.save_file()
+            if not filename:
+                return 
+            
+        
+        # changing dir may be required
+        script_dir = os.path.realpath(os.path.dirname(filename))
+        
+        if (get_workbench().get_option("run.auto_cd") 
+            and command_name[0].isupper() or always_change_to_script_dir):
+            working_directory = script_dir
+        else:
+            working_directory = None
+        
+        self.execute_script(filename, [], working_directory, command_name)
+        
+    def handle_execute_from_shell(self, cmd_line):
+        """
+        Handles all commands that take a filename and 0 or more extra arguments.
+        Passes the command to backend.
+        
+        (Debugger plugin may also use this method)
+        """
+        command, args = parse_shell_command(cmd_line)
+        
+        if len(args) >= 1:
+            get_workbench().get_editor_notebook().save_all_named_editors()
+            cmd = ToplevelCommand(command=command,
+                               filename=args[0],
+                               args=args[1:])
+            
+            if os.path.isabs(cmd.filename):
+                cmd.full_filename = cmd.filename
+            else:
+                cmd.full_filename = os.path.join(self.get_cwd(), cmd.filename)
+                
+            if command in ["Run", "run", "Debug", "debug"]:
+                with tokenize.open(cmd.full_filename) as fp:
+                    cmd.source = fp.read()
+                
+            self.send_command(cmd)
+        else:
+            raise CommandSyntaxError("Command '%s' takes at least one argument", command)
+
+    def _handle_reset_from_shell(self, cmd_line):
+        command, args = parse_shell_command(cmd_line)
+        assert command == "Reset"
+        
+        if len(args) == 0:
+            self.send_command(ToplevelCommand(command="Reset"))
+        else:
+            raise CommandSyntaxError("Command 'Reset' doesn't take arguments")
+        
+
+    def _handle_cd_from_shell(self, cmd_line):
+        command, args = parse_shell_command(cmd_line)
+        assert command == "cd"
+        
+        if len(args) == 1:
+            self.send_command(ToplevelCommand(command="cd", path=args[0]))
+        else:
+            raise CommandSyntaxError("Command 'cd' takes one argument")
+
+    def _cmd_run_current_script_enabled(self):
+        return (get_workbench().get_editor_notebook().get_current_editor() is not None
+                and get_runner().get_state() == "waiting_toplevel_command"
+                and "run" in get_runner().supported_features())
+    
+    def _cmd_run_current_script(self):
+        self.execute_current("Run")
+    
+    def _cmd_interrupt(self):
+        self.interrupt_backend()
+        
+    def _cmd_interrupt_enabled(self):
+        widget = get_workbench().focus_get()
+        if not running_on_mac_os(): # on Mac Ctrl+C is not used for Copy
+            if hasattr(widget, "selection_get"):
+                try:
+                    if widget.selection_get() != "":
+                        # assuming user meant to copy, not interrupt
+                        # (IDLE seems to follow same logic)
+                        return False
+                except:
+                    # selection_get() gives error when calling without selection on Ubuntu
+                    pass
+
+        return get_runner().get_state() != "waiting_toplevel_command"
+    
+    def cmd_interrupt_reset(self):
+        if self.get_state() == "waiting_toplevel_command":
+            get_workbench().get_view("ShellView").submit_command("%Reset\n")
+        else:
+            get_runner().interrupt_backend()
+    
+            
+    def _cmd_interrupt_reset_enabled(self):
+        return True
+    
+    def _postpone_command(self, cmd):
+        # in case of InlineCommands, discard older same type command
+        if isinstance(cmd, InlineCommand):
+            for older_cmd in self._postponed_commands:
+                if older_cmd.command == cmd.command:
+                    self._postponed_commands.remove(older_cmd)
+        
+        if len(self._postponed_commands) > 10: 
+            "Can't pile up too many commands. This command will be just ignored"
+        else:
+            self._postponed_commands.append(cmd)
+    
+    def _state_is_suitable(self, cmd):
+        if isinstance(cmd, ToplevelCommand):
+            return (self.get_state() == "waiting_toplevel_command"
+                    or cmd.command in ["Reset", "Run", "Debug"])
+            
+        elif isinstance(cmd, DebuggerCommand):
+            return self.get_state() == "waiting_debugger_command"
+        
+        elif isinstance(cmd, InlineCommand):
+            # UI may send inline commands in any state,
+            # but some backends don't accept them in some states
+            return self.get_state() in self._proxy.allowed_states_for_inline_commands()
+        
+        else:
+            raise RuntimeError("Unknown command class: " + str(type(cmd)))
+    
+    def _send_postponed_commands(self):
+        remaining = []
+        
+        for cmd in self._postponed_commands:
+            if self._state_is_suitable(cmd):
+                logging.debug("Sending postponed command", cmd)
+                self.send_command(cmd)
+            else:
+                remaining.append(cmd)
+        
+        self._postponed_commands = remaining
+        
+    
+    def _poll_vm_messages(self):
+        """I chose polling instead of event_generate in listener thread,
+        because event_generate across threads is not reliable
+        http://www.thecodingforums.com/threads/more-on-tk-event_generate-and-threads.359615/
+        """
+        try:
+            initial_state = self.get_state()
+            
+            while self._proxy is not None:
+                try:
+                    msg = self._proxy.fetch_next_message()
+                    if not msg:
+                        break
+                except BackendTerminatedError as exc:
+                    self._report_backend_crash(exc)
+                    self.reset_backend()
+                    return
+                
+                if msg.get("SystemExit", False):
+                    self.reset_backend()
+                    return
+                
+                # change state
+                if "command_context" in msg:
+                    # message_context shows the state where corresponding command was handled in the backend
+                    # Now we got the response and we're return to that state
+                    self._set_state(msg["command_context"])
+                elif msg["message_type"] == "ToplevelResult":
+                    # some ToplevelResult-s don't have command_context
+                    self._set_state("waiting_toplevel_command")
+                elif msg["message_type"] == "InputRequest":
+                    self._set_state("waiting_input")
+                else:
+                    "other messages don't affect the state"
+                
+                if msg["message_type"] == "ToplevelResult":
+                    self._current_toplevel_command = None
+    
+                #logging.debug("Runner: State: %s, Fetched msg: %s" % (self.get_state(), msg))
+                get_workbench().event_generate(msg["message_type"], **msg)
+                
+                # TODO: maybe distinguish between workbench cwd and backend cwd ??
+                get_workbench().set_option("run.working_directory", self.get_cwd())
+                
+                # TODO: is it necessary???
+                # https://stackoverflow.com/a/13520271/261181
+                #get_workbench().update() 
+                
+            if self.get_state() != initial_state:
+                self._send_postponed_commands()
+                
+        finally:
+            get_workbench().after(50, self._poll_vm_messages)
+    
+    def _report_backend_crash(self, exc):
+        err = "Backend terminated (returncode: %s)\n" % exc.returncode
+        
+        try:
+            faults_file = os.path.join(THONNY_USER_DIR, "backend_faults.log")
+            if os.path.exists(faults_file):
+                with open(faults_file, encoding="ASCII") as fp:
+                    err += fp.read()
+        except:
+            logging.exception("Failed retrieving backend faults")
+                
+        err = err.strip() + "\nResetting ...\n"
+        
+        get_workbench().event_generate("ProgramOutput",
+                                       stream_name="stderr",
+                                       data=err)
+        
+        get_workbench().become_topmost_window()
+        
+    
+    def reset_backend(self):
+        self.kill_backend()
+        configuration = get_workbench().get_option("run.backend_configuration")
+        backend_name, configuration_option = parse_configuration(configuration)
+        if backend_name not in get_workbench().get_backends():
+            raise UserError("Can't find backend '{}'. Please select another backend from options"
+                            .format(backend_name))
+        
+        backend_class = get_workbench().get_backends()[backend_name]
+        self._set_state("running")
+        self._proxy = None
+        self._proxy = backend_class(configuration_option)
+    
+    def interrupt_backend(self):
+        if self._proxy is not None:
+            self._proxy.interrupt()
+        else:
+            logging.warning("Interrupting without proxy")
+    
+    def kill_backend(self):
+        self._current_toplevel_command = None
+        self._current_command = None
+        self._postponed_commands = []
+        if self._proxy:
+            self._proxy.kill_current_process()
+            self._proxy = None
+
+    def get_interpreter_command(self):
+        return self._proxy.get_interpreter_command()
+    
+    def get_backend_description(self):
+        return self._proxy.get_description()
+
+    def _check_alloc_console(self):
+        if (sys.executable.endswith("thonny.exe")
+            or sys.executable.endswith("pythonw.exe")):
+            # These don't have console allocated.
+            # Console is required for sending interrupts.
+            
+            # AllocConsole would be easier but flashes console window
+            
+            import ctypes
+            kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
+            
+            exe = (sys.executable
+                      .replace("thonny.exe", "python.exe") 
+                      .replace("pythonw.exe", "python.exe"))
+            
+            cmd = [exe, "-c", "print('Hi!'); input()"]
+            child = subprocess.Popen(cmd,
+                                     stdin=subprocess.PIPE,
+                                     stdout=subprocess.PIPE,
+                                     stderr=subprocess.PIPE,
+                                     shell=True)
+            child.stdout.readline()
+            result = kernel32.AttachConsole(child.pid)
+            if not result:
+                err = ctypes.get_last_error()
+                logging.info("Could not allocate console. Error code: " +str(err))
+            child.stdin.write(b"\n")
+            child.stdin.flush()
+
+    def supported_features(self):
+        if self._proxy is None:
+            return []
+        else:
+            return self._proxy.supported_features()
+            
+            
+    def get_frontend_python(self):
+        return sys.executable.replace("thonny.exe", "python.exe")
+    
+    def using_venv(self):
+        return isinstance(self._proxy,  CPythonProxy) and self._proxy.in_venv
+
+class BackendProxy:
+    """Communicates with backend process.
+    
+    All communication methods must be non-blocking, 
+    ie. suitable for calling from GUI thread."""
+    
+    def __init__(self, configuration_option):
+        """Initializes (or starts the initialization of) the backend process.
+        
+        Backend is considered ready when the runner gets a ToplevelResult
+        with attribute "welcome_text" from fetch_next_message.
+        
+        param configuration_option:
+            If configuration is "Foo (bar)", then "Foo" is backend descriptor
+            and "bar" is the configuration option"""
+    
+    @classmethod
+    def get_configuration_options(cls):
+        """Returns a list strings for populating interpreter selection dialog.
+        The strings are without backend descriptor"""
+        raise NotImplementedError()
+    
+    def get_description(self):
+        """Returns a string that describes the backend"""
+        raise NotImplementedError()        
+
+    def send_command(self, cmd):
+        """Send the command to backend"""
+        raise NotImplementedError()
+    
+    def allowed_states_for_inline_commands(self):
+        return ["waiting_toplevel_command"]
+
+    def send_program_input(self, data):
+        """Send input data to backend"""
+        raise NotImplementedError()
+    
+    def fetch_next_message(self):
+        """Read next message from the queue or None if queue is empty"""
+        raise NotImplementedError()
+
+    def get_sys_path(self):
+        "backend's sys.path"
+        return []
+    
+    def interrupt(self):
+        """Tries to interrupt current command without reseting the backend"""
+        self.kill_current_process()
+    
+    def kill_current_process(self):
+        """Kill the backend.
+        
+        Is called when Thonny no longer needs this backend 
+        (Thonny gets closed or new backend gets selected)
+        """
+        pass
+    
+    def get_interpreter_command(self):
+        """Return system command for invoking current interpreter"""
+        raise NotImplementedError()
+    
+    def supported_features(self):
+        return ["run"]
+
+    
+
+class CPythonProxy(BackendProxy):
+    @classmethod
+    def get_configuration_options(cls):
+        return ([DEFAULT_CPYTHON_INTERPRETER, SAME_AS_FRONTEND_INTERPRETER] 
+                + CPythonProxy._get_interpreters())
+        
+    def __init__(self, configuration_option):
+        if configuration_option == DEFAULT_CPYTHON_INTERPRETER:
+            self._prepare_private_venv()
+            self._executable = get_private_venv_executable()
+        elif configuration_option == SAME_AS_FRONTEND_INTERPRETER:
+            self._executable = get_runner().get_frontend_python()
+        elif configuration_option.startswith("."):
+            # Relative paths are relative to front-end interpretator directory
+            # (This must be written directly to conf file, as it can't be selected from Options dialog)  
+            self._executable = os.path.normpath(os.path.join(os.path.dirname(sys.executable), 
+                                                             configuration_option)) 
+        else:
+            self._executable = configuration_option
+            
+            # Rembember the usage of this non-default interpreter
+            used_interpreters = get_workbench().get_option("run.used_interpreters")
+            if self._executable not in used_interpreters:
+                used_interpreters.append(self._executable)
+            get_workbench().set_option("run.used_interpreters", used_interpreters)
+            
+        
+        cwd = get_workbench().get_option("run.working_directory")
+        if os.path.exists(cwd):
+            self.cwd = cwd
+        else:
+            self.cwd = os.path.expanduser("~")
+            
+        self._proc = None
+        self._message_queue = None
+        self._sys_path = []
+        self._tkupdate_loop_id = None
+        self.in_venv = None
+        
+        self._start_new_process()
+    
+    def fetch_next_message(self):
+        if not self._message_queue or len(self._message_queue) == 0:
+            if self._proc is not None:
+                retcode = self._proc.poll()
+                if retcode is not None:
+                    raise BackendTerminatedError(retcode)
+            return None
+        
+        msg = self._message_queue.popleft()
+        if "tkinter_is_active" in msg:
+            self._update_tkupdating(msg)
+        
+        if "in_venv" in msg:
+            self.in_venv = msg["in_venv"]
+        
+        if msg["message_type"] == "ProgramOutput":
+            # combine available output messages to one single message, 
+            # in order to put less pressure on UI code
+            
+            while True:
+                if len(self._message_queue) == 0:
+                    return msg
+                else:
+                    next_msg = self._message_queue.popleft()
+                    if (next_msg["message_type"] == "ProgramOutput" 
+                        and next_msg["stream_name"] == msg["stream_name"]):
+                        msg["data"] += next_msg["data"]
+                    else:
+                        # not same type of message, put it back
+                        self._message_queue.appendleft(next_msg)
+                        return msg
+            
+        else: 
+            return msg
+    
+    def get_description(self):
+        # TODO: show backend version and interpreter path
+        return "Python (current dir: {})".format(self.cwd)
+        
+        
+    def send_command(self, cmd):
+        if isinstance(cmd, ToplevelCommand) and cmd.command in ("Run", "Debug", "Reset"):
+            self.kill_current_process()
+            self._start_new_process(cmd)
+             
+        self._proc.stdin.write(serialize_message(cmd) + "\n")
+        self._proc.stdin.flush()
+        return True 
+    
+    def send_program_input(self, data):
+        self.send_command(InputSubmission(data=data))
+        
+    def allowed_states_for_inline_commands(self):
+        return ["waiting_toplevel_command", "waiting_debugger_command", 
+                "waiting_input"]
+
+    def get_sys_path(self):
+        return self._sys_path
+    
+    def interrupt(self):
+        
+        def do_kill():
+            self._proc.kill()
+            get_workbench().event_generate("ProgramOutput",
+                                           stream_name="stderr",
+                                           data="KeyboardInterrupt: Forced reset")
+            get_runner().reset_backend()
+        
+        if self._proc is not None:
+            if self._proc.poll() is None:
+                command_to_interrupt = get_runner().get_current_toplevel_command()
+                if running_on_windows():
+                    try:
+                        os.kill(self._proc.pid, signal.CTRL_BREAK_EVENT)  # @UndefinedVariable
+                    except:
+                        logging.exception("Could not interrupt backend process")
+                else:
+                    self._proc.send_signal(signal.SIGINT)
+            
+                # Tkinter programs can't be interrupted so easily:
+                # http://stackoverflow.com/questions/13784232/keyboardinterrupt-taking-a-while
+                # so let's chedule a hard kill in case the program refuses to be interrupted
+                def go_hard():
+                    if (get_runner().get_state() != "waiting_toplevel_command"
+                        and get_runner().get_current_toplevel_command() == command_to_interrupt): # still running same command
+                        do_kill()
+                
+                # 100 ms was too little for Mac
+                # 250 ms was too little for one of the Windows machines
+                get_workbench().after(500, go_hard)
+            else:
+                do_kill()
+            
+                    
+    
+    def kill_current_process(self):
+        if self._proc is not None and self._proc.poll() is None: 
+            self._proc.kill()
+            
+        self._proc = None
+        self._message_queue = None
+    
+    def _prepare_jedi(self):
+        """Make jedi available for the backend"""
+        
+        # Copy jedi
+        import jedi
+        dirname = os.path.join(THONNY_USER_DIR, "jedi_" + str(jedi.__version__))
+        if not os.path.exists(dirname):
+            shutil.copytree(jedi.__path__[0], os.path.join(dirname, "jedi"))
+        return dirname
+    
+        # TODO: clean up old versions
+    
+    def _start_new_process(self, cmd=None):
+        this_python = get_runner().get_frontend_python()
+        # deque, because in one occasion I need to put messages back
+        self._message_queue = collections.deque()
+    
+        # prepare the environment
+        my_env = os.environ.copy()
+        
+        # Delete some environment variables if the backend is (based on) a different Python instance
+        if self._executable not in [
+            this_python,
+            this_python.replace("python.exe", "pythonw.exe"),
+            this_python.replace("pythonw.exe", "python.exe"),
+            get_private_venv_executable()]:
+            
+            # Keep only the variables, that are not related to Python
+            my_env = {name : my_env[name] for name in my_env 
+                      if "python" not in name.lower() and name not in ["TK_LIBRARY", "TCL_LIBRARY"]}
+            
+            # Remove variables used to tweak bundled Thonny-private Python
+            if using_bundled_python():
+                my_env = {name : my_env[name] for name in my_env
+                          if name not in ["SSL_CERT_FILE", "SSL_CERT_DIR", "LD_LIBRARY_PATH"]}
+        
+        # variables controlling communication with the back-end process
+        my_env["PYTHONIOENCODING"] = "ASCII" 
+        my_env["PYTHONUNBUFFERED"] = "1" 
+        
+        
+        my_env["THONNY_USER_DIR"] = THONNY_USER_DIR
+        
+        # venv may not find (correct) Tk without assistance (eg. in Ubuntu)
+        if self._executable == get_private_venv_executable():
+            try:
+                my_env["TCL_LIBRARY"] = get_workbench().tk.exprstring('$tcl_library')
+                my_env["TK_LIBRARY"] = get_workbench().tk.exprstring('$tk_library')
+            except:
+                logging.exception("Can't find Tcl/Tk library")
+        
+        # If the back-end interpreter is something else than front-end's one,
+        # then it may not have jedi installed. 
+        # In this case fffer front-end's jedi for the back-end
+        if self._executable != get_runner().get_frontend_python(): 
+            # I don't want to use PYTHONPATH for making jedi available
+            # because that would add it to the front of sys.path
+            my_env["JEDI_LOCATION"] = self._prepare_jedi()
+        
+        if not os.path.exists(self._executable):
+            raise UserError("Interpreter (%s) not found. Please recheck corresponding option!"
+                            % self._executable)
+        
+        
+        import thonny.shared.backend_launcher
+        cmd_line = [
+            self._executable, 
+            '-u', # unbuffered IO (neccessary in Python 3.1)
+            '-B', # don't write pyo/pyc files 
+                  # (to avoid problems when using different Python versions without write permissions)
+            thonny.shared.backend_launcher.__file__
+        ]
+        
+
+        if hasattr(cmd, "filename"):
+            cmd_line.append(cmd.filename)
+            if hasattr(cmd, "args"):
+                cmd_line.extend(cmd.args)
+        
+        if hasattr(cmd, "environment"):
+            my_env.update(cmd.environment)            
+        
+        creationflags = 0
+        if running_on_windows():
+            creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
+        
+        debug("Starting the backend: %s %s", cmd_line, self.cwd)
+        self._proc = subprocess.Popen (
+            cmd_line,
+            #bufsize=0,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            cwd=self.cwd,
+            env=my_env,
+            universal_newlines=True,
+            creationflags=creationflags
+        )
+        
+        if cmd:
+            # Consume the ready message, cmd will get its own result message
+            ready_line = self._proc.stdout.readline()
+            if ready_line == "": # There was some problem
+                error_msg = self._proc.stderr.read()
+                raise Exception("Error starting backend process: " + error_msg)
+            
+            #ready_msg = parse_message(ready_line)
+            #self._sys_path = ready_msg["path"]
+            #debug("Backend ready: %s", ready_msg)
+        
+        
+        
+        # setup asynchronous output listeners
+        start_new_thread(self._listen_stdout, ())
+        start_new_thread(self._listen_stderr, ())
+    
+    def _listen_stdout(self):
+        #debug("... started listening to stdout")
+        # will be called from separate thread
+        while True:
+            data = self._proc.stdout.readline()
+            #debug("... read some stdout data", repr(data))
+            if data == '':
+                break
+            else:
+                msg = parse_message(data)
+                if "cwd" in msg:
+                    self.cwd = msg["cwd"]
+                    
+                # TODO: it was "with self._state_lock:". Is it necessary?
+                self._message_queue.append(msg)
+                
+                if len(self._message_queue) > 100:
+                    # Probably backend runs an infinite/long print loop.
+                    # Throttle message thougput in order to keep GUI thread responsive.
+                    sleep(0.1)
+
+    def _listen_stderr(self):
+        # stderr is used only for debugger debugging
+        while True:
+            data = self._proc.stderr.readline()
+            if data == '':
+                break
+            else:
+                debug("### BACKEND ###: %s", data.strip())
+        
+
+
+    @classmethod
+    def _get_interpreters(cls):
+        result = set()
+        
+        if running_on_windows():
+            # registry
+            result.update(CPythonProxy._get_interpreters_from_windows_registry())
+            
+            # Common locations
+            for dir_ in ["C:\\Python34",
+                         "C:\\Python35",
+                         "C:\\Program Files\\Python 3.5",
+                         "C:\\Program Files (x86)\\Python 3.5",
+                         "C:\\Python36",
+                         "C:\\Program Files\\Python 3.6",
+                         "C:\\Program Files (x86)\\Python 3.6",
+                         ]:
+                path = os.path.join(dir_, WINDOWS_EXE)
+                if os.path.exists(path):
+                    result.add(os.path.realpath(path))  
+        
+        else:
+            # Common unix locations
+            for dir_ in ["/bin", "/usr/bin", "/usr/local/bin",
+                         os.path.expanduser("~/.local/bin")]:
+                for name in ["python3", "python3.4", "python3.5", "python3.6"]:
+                    path = os.path.join(dir_, name)
+                    if os.path.exists(path):
+                        result.add(path)  
+        
+        if running_on_mac_os():
+            for version in ["3.4", "3.5", "3.6"]:
+                dir_ = os.path.join("/Library/Frameworks/Python.framework/Versions",
+                                    version, "bin")
+                path = os.path.join(dir_, "python3")
+                
+                if os.path.exists(path):
+                    result.add(path)
+        
+        for command in ["pythonw", "python3", "python3.4", "python3.5", "python3.6"]:
+            path = which(command)
+            if path is not None and os.path.isabs(path):
+                result.add(path)
+        
+        current_configuration = get_workbench().get_option("run.backend_configuration")
+        backend, configuration_option = parse_configuration(current_configuration)
+        if backend == "Python" and configuration_option and os.path.exists(configuration_option):
+            result.add(os.path.realpath(configuration_option))
+        
+        for path in get_workbench().get_option("run.used_interpreters"):
+            if os.path.exists(path):
+                result.add(os.path.realpath(path))
+        
+        return sorted(result)
+    
+    
+    @classmethod
+    def _get_interpreters_from_windows_registry(cls):
+        import winreg
+        result = set()
+        for key in [winreg.HKEY_LOCAL_MACHINE,
+                    winreg.HKEY_CURRENT_USER]:
+            for version in ["3.4",
+                            "3.5", "3.5-32", "3.5-64",
+                            "3.6", "3.6-32", "3.6-64"]:
+                try:
+                    for subkey in [
+                        'SOFTWARE\\Python\\PythonCore\\' + version + '\\InstallPath',
+                        'SOFTWARE\\Python\\PythonCore\\Wow6432Node\\' + version + '\\InstallPath'
+                                 ]:
+                        dir_ = winreg.QueryValue(key, subkey)
+                        if dir_:
+                            path = os.path.join(dir_, WINDOWS_EXE)
+                            if os.path.exists(path):
+                                result.add(path)
+                except:
+                    pass
+        
+        return result
+    
+    def get_interpreter_command(self):
+        return self._executable
+    
+    
+    def _update_tkupdating(self, msg):
+        """Enables running Tkinter programs which doesn't call mainloop. 
+        
+        When mainloop is omitted, then program can be interacted with
+        from the shell after it runs to the end.
+        
+        Each ToplevelResponse is supposed to tell, whether tkinter window
+        is open and needs updating.
+        """
+        if not "tkinter_is_active" in msg:
+            return
+        
+        if msg["tkinter_is_active"] and self._tkupdate_loop_id is None:
+            # Start updating
+            self._tkupdate_loop_id = self._loop_tkupdate(True)
+        elif not msg["tkinter_is_active"] and self._tkupdate_loop_id is not None:
+            # Cancel updating
+            try:
+                get_workbench().after_cancel(self._tkupdate_loop_id)
+            finally:
+                self._tkupdate_loop_id = None
+    
+    def _loop_tkupdate(self, force=False):
+        if force or get_runner().get_state() == "waiting_toplevel_command":
+            self.send_command(InlineCommand("tkupdate"))
+            self._tkupdate_loop_id = get_workbench().after(50, self._loop_tkupdate)
+        else:
+            self._tkupdate_loop_id = None
+        
+
+    def _prepare_private_venv(self):
+        path = _get_private_venv_path()
+        if os.path.isdir(path) and os.path.isfile(os.path.join(path, "pyvenv.cfg")):
+            self._check_upgrade_private_venv(path)
+        else:
+            self._create_private_venv(path, "Please wait!\nThonny prepares its virtual environment.")
+        
+    def _check_upgrade_private_venv(self, path):
+        # If home is wrong then regenerate
+        # If only micro version is different, then upgrade
+        info = _get_venv_info(path)
+        
+        if not eqfn(info["home"], os.path.dirname(sys.executable)):
+            self._create_private_venv(path, 
+                                 "Thonny's virtual environment was created for another interpreter.\n"
+                                 + "Regenerating the virtual environment for current interpreter.\n"
+                                 + "(You may need to reinstall your 3rd party packages)\n"
+                                 + "Please wait!.",
+                                 clear=True)
+        else:
+            venv_version = tuple(map(int, info["version"].split(".")))
+            sys_version = sys.version_info[:3]
+            assert venv_version[0] == sys_version[0]
+            assert venv_version[1] == sys_version[1]
+            
+            if venv_version[2] != sys_version[2]:
+                self._create_private_venv(path, "Please wait!\nUpgrading Thonny's virtual environment.",
+                                     upgrade=True)
+                
+    
+    def _create_private_venv(self, path, description, clear=False, upgrade=False):
+        base_exe = sys.executable
+        if sys.executable.endswith("thonny.exe"):
+            # assuming that thonny.exe is in the same dir as "python.exe"
+            base_exe = sys.executable.replace("thonny.exe", "python.exe")
+        
+        
+        # Don't include system site packages
+        # This way all students will have similar configuration
+        # independently of system Python (if Thonny is used with system Python)
+        
+        # NB! Cant run venv.create directly, because in Windows 
+        # it tries to link venv to thonny.exe.
+        # Need to run it via proper python
+        cmd = [base_exe, "-m", "venv"]
+        if clear:
+            cmd.append("--clear")
+        if upgrade:
+            cmd.append("--upgrade")
+        
+        try:
+            import ensurepip  # @UnusedImport
+        except ImportError:
+            cmd.append("--without-pip")
+            
+        cmd.append(path)
+        startupinfo = None
+        if running_on_windows():
+            startupinfo = subprocess.STARTUPINFO()
+            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+        proc = subprocess.Popen(cmd, startupinfo=startupinfo,
+                                stdout=subprocess.PIPE,
+                                stderr=subprocess.STDOUT,
+                                universal_newlines=True)
+             
+        from thonny.ui_utils import SubprocessDialog
+        dlg = SubprocessDialog(get_workbench(), proc, "Preparing the backend", long_description=description)
+        try:
+            get_workbench().wait_window(dlg)
+        except:
+            # if using --without-pip the dialog may close very quickly 
+            # and for some reason wait_window would give error then
+            logging.exception("Problem with waiting for venv creation dialog")
+        get_workbench().become_topmost_window() # Otherwise focus may get stuck somewhere
+        
+        bindir = os.path.dirname(get_private_venv_executable())
+        # create private env marker
+        marker_path = os.path.join(bindir, "is_private")
+        with open(marker_path, mode="w") as fp:
+            fp.write("# This file marks Thonny-private venv")
+        
+        # Create recommended pip conf to get rid of list deprecation warning
+        # https://github.com/pypa/pip/issues/4058
+        pip_conf = "pip.ini" if running_on_windows() else "pip.conf"
+        with open(os.path.join(path, pip_conf), mode="w") as fp:
+            fp.write("[list]\nformat = columns")
+        
+        assert os.path.isdir(path)
+
+    def supported_features(self):
+        return ["run", "debug", "pip_gui", "system_shell"]
+
+
+def parse_configuration(configuration):
+    """
+    "Python (C:\Python34\python.exe)" becomes ("Python", "C:\Python34\python.exe")
+    "BBC micro:bit" becomes ("BBC micro:bit", "")
+    """
+    
+    parts = configuration.split("(", maxsplit=1)
+    if len(parts) == 1:
+        return configuration, ""
+    else:
+        return parts[0].strip(), parts[1].strip(" )")
+
+
+    
+    
+def _get_private_venv_path():
+    if "thonny" in sys.executable.lower():
+        prefix = "BundledPython"
+    else:
+        prefix = "Python" 
+    return os.path.join(THONNY_USER_DIR, prefix + "%d%d" % (sys.version_info[0], sys.version_info[1]))
+
+def get_private_venv_executable():
+    venv_path = _get_private_venv_path()
+    
+    if running_on_windows():
+        exe = os.path.join(venv_path, "Scripts", WINDOWS_EXE)
+    else:
+        exe = os.path.join(venv_path, "bin", "python3")
+    
+    return exe
+
+def _get_venv_info(venv_path):
+    cfg_path = os.path.join(venv_path, "pyvenv.cfg")
+    result = {}
+    
+    with open(cfg_path, encoding="UTF-8") as fp:
+        for line in fp:
+            if "=" in line:
+                key, val = line.split("=", maxsplit=1)
+                result[key.strip()] = val.strip()
+    
+    return result;
+
+
+def using_bundled_python():
+    return os.path.exists(os.path.join(
+        os.path.dirname(sys.executable),
+        "thonny_python.ini"
+    ))
+
+class BackendTerminatedError(Exception):
+    def __init__(self, returncode):
+        Exception.__init__(self)
+        self.returncode = returncode    
diff --git a/thonny/shared/__init__.py b/thonny/shared/__init__.py
new file mode 100644
index 0000000..a2ba1fd
--- /dev/null
+++ b/thonny/shared/__init__.py
@@ -0,0 +1,2 @@
+# Frontend will interpret this folder as thonny.shared
+# For backend this folder will be in path
\ No newline at end of file
diff --git a/thonny/shared/backend_launcher.py b/thonny/shared/backend_launcher.py
new file mode 100644
index 0000000..c23edc5
--- /dev/null
+++ b/thonny/shared/backend_launcher.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+"""
+This file is run by VMProxy
+
+(Why separate file for launching? I want to have clean global scope 
+in toplevel __main__ module (because that's where user scripts run), but backend's global scope 
+is far from clean. 
+I could also do python -c "from backend import VM: VM().mainloop()", but looks like this 
+gives relative __file__-s on imported modules.) 
+"""
+
+if __name__ == "__main__":
+    # imports required by backend itself
+    import sys
+    import logging
+    import os.path
+    THONNY_USER_DIR = os.environ["THONNY_USER_DIR"]
+    # set up logging
+    logger = logging.getLogger()
+    logFormatter = logging.Formatter('%(levelname)s: %(message)s')
+    file_handler = logging.FileHandler(os.path.join(THONNY_USER_DIR,"backend.log"), 
+                                       encoding="UTF-8",
+                                       mode="w");
+    file_handler.setFormatter(logFormatter)
+    file_handler.setLevel(logging.INFO);
+    logger.addHandler(file_handler)
+    
+    # TODO: sending log records to original stdout could be better (reading from stderr may introduce sync problems)
+    stream_handler = logging.StreamHandler(stream=sys.stderr)
+    stream_handler.setLevel(logging.INFO);
+    stream_handler.setFormatter(logFormatter)
+    logger.addHandler(stream_handler)
+    
+    logger.setLevel(logging.INFO)
+    
+    import faulthandler
+    fault_out = open(os.path.join(THONNY_USER_DIR, "backend_faults.log"), mode="w")
+    faulthandler.enable(fault_out)    
+    
+    from thonny.backend import VM  # @UnresolvedImport
+    VM().mainloop()
+    
diff --git a/thonny/shared/thonny/__init__.py b/thonny/shared/thonny/__init__.py
new file mode 100644
index 0000000..44f8ed5
--- /dev/null
+++ b/thonny/shared/thonny/__init__.py
@@ -0,0 +1,2 @@
+# Frontend will interpret this folder as thonny.shared.thonny
+# For backend parent folder will be in path, so this will be package thonny
\ No newline at end of file
diff --git a/thonny/shared/thonny/ast_utils.py b/thonny/shared/thonny/ast_utils.py
new file mode 100644
index 0000000..4982a44
--- /dev/null
+++ b/thonny/shared/thonny/ast_utils.py
@@ -0,0 +1,513 @@
+# -*- coding: utf-8 -*-
+
+import ast
+import _ast
+import io
+import sys
+import token
+import tokenize
+import traceback
+
+def extract_text_range(source, text_range):
+    lines = source.splitlines(True)
+    # get relevant lines
+    lines = lines[text_range.lineno-1:text_range.end_lineno]
+
+    # trim last and first lines
+    lines[-1] = lines[-1][:text_range.end_col_offset]
+    lines[0] = lines[0][text_range.col_offset:]
+    return "".join(lines)
+
+
+
+def find_expression(node, text_range):
+    if (hasattr(node, "lineno")
+        and node.lineno == text_range.lineno and node.col_offset == text_range.col_offset
+        and node.end_lineno == text_range.end_lineno and node.end_col_offset == text_range.end_col_offset
+        # expression and Expr statement can have same range
+        and isinstance(node, _ast.expr)):
+        return node
+    else:
+        for child in ast.iter_child_nodes(node):
+            result = find_expression(child, text_range)
+            if result is not None:
+                return result
+        return None
+
+
+def contains_node(parent_node, child_node):
+    for child in ast.iter_child_nodes(parent_node):
+        if child == child_node or contains_node(child, child_node):
+            return True
+
+    return False
+
+def has_parent_with_class(target_node, parent_class, tree):
+    for node in ast.walk(tree):
+        if isinstance(node, parent_class) and contains_node(node, target_node):
+            return True
+
+    return False
+
+
+def parse_source(source, filename='<unknown>', mode="exec"):
+    root = ast.parse(source, filename, mode)
+    mark_text_ranges(root, source)
+    return root
+
+
+def get_last_child(node):
+    if isinstance(node, ast.Call):
+        # TODO: take care of Python 3.5 updates (Starred etc.)
+        if hasattr(node, "kwargs") and node.kwargs is not None:
+            return node.kwargs
+        elif hasattr(node, "starargs") and node.starargs is not None:
+            return node.starargs
+        elif len(node.keywords) > 0:
+            return node.keywords[-1]
+        elif len(node.args) > 0:
+            # TODO: ast.Starred doesn't exist in Python 3.4  ?? 
+            if isinstance(node.args[-1], ast.Starred):
+                # return the thing under Starred
+                return node.args[-1].value
+            else:
+                return node.args[-1]
+        else:
+            return node.func
+
+    elif isinstance(node, ast.BoolOp):
+        return node.values[-1]
+
+    elif isinstance(node, ast.BinOp):
+        return node.right
+
+    elif isinstance(node, ast.Compare):
+        return node.comparators[-1]
+
+    elif isinstance(node, ast.UnaryOp):
+        return node.operand
+
+    elif (isinstance(node, (ast.Tuple, ast.List, ast.Set))
+          and len(node.elts)) > 0:
+        return node.elts[-1]
+
+    elif (isinstance(node, ast.Dict)
+          and len(node.values)) > 0:
+        return node.values[-1]
+
+    elif (isinstance(node, (ast.Return, ast.Assign, ast.AugAssign, ast.Yield, ast.YieldFrom))
+          and node.value is not None):
+        return node.value
+
+    elif isinstance(node, ast.Delete):
+        return node.targets[-1]
+
+    elif isinstance(node, ast.Expr):
+        return node.value
+
+    elif isinstance(node, ast.Assert):
+        if node.msg is not None:
+            return node.msg
+        else:
+            return node.test
+
+    elif isinstance(node, ast.Subscript):
+        if hasattr(node.slice, "value"):
+            return node.slice.value
+        else:
+            assert (hasattr(node.slice, "lower")
+                    and hasattr(node.slice, "upper")
+                    and hasattr(node.slice, "step"))
+
+            if node.slice.step is not None:
+                return node.slice.step
+            elif node.slice.upper is not None:
+                return node.slice.upper
+            else:
+                return node.slice.lower
+
+
+    elif isinstance(node, (ast.For, ast.While, ast.If, ast.With)):
+        return True # There is last child, but I don't know which it will be
+
+    else:
+        return None
+
+    # TODO: pick more cases from here:
+    """
+    (isinstance(node, (ast.IfExp, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp))
+            or isinstance(node, ast.Raise) and (node.exc is not None or node.cause is not None)
+            # or isinstance(node, ast.FunctionDef, ast.Lambda) and len(node.args.defaults) > 0
+                and (node.dest is not None or len(node.values) > 0))
+
+            #"TODO: Import ja ImportFrom"
+            # TODO: what about ClassDef ???
+    """
+
+
+
+
+def mark_text_ranges(node, source):
+    """
+    Node is an AST, source is corresponding source as string.
+    Function adds recursively attributes end_lineno and end_col_offset to each node
+    which has attributes lineno and col_offset.
+    """
+
+
+    def _extract_tokens(tokens, lineno, col_offset, end_lineno, end_col_offset):
+        return list(filter((lambda tok: tok.start[0] >= lineno
+                                   and (tok.start[1] >= col_offset or tok.start[0] > lineno)
+                                   and tok.end[0] <= end_lineno
+                                   and (tok.end[1] <= end_col_offset or tok.end[0] < end_lineno)
+                                   and tok.string != ''),
+                           tokens))
+
+
+
+    def _mark_text_ranges_rec(node, tokens, prelim_end_lineno, prelim_end_col_offset):
+        """
+        Returns the earliest starting position found in given tree,
+        this is convenient for internal handling of the siblings
+        """
+
+        # set end markers to this node
+        if "lineno" in node._attributes and "col_offset" in node._attributes:
+            tokens = _extract_tokens(tokens, node.lineno, node.col_offset, prelim_end_lineno, prelim_end_col_offset)
+            try:
+                tokens = _mark_end_and_return_child_tokens(node, tokens, prelim_end_lineno, prelim_end_col_offset)
+            except:
+                traceback.print_exc() # TODO: log it somewhere
+                # fallback to incorrect marking instead of exception
+                node.end_lineno = node.lineno
+                node.end_col_offset = node.col_offset + 1
+
+
+        # mark its children, starting from last one
+        # NB! need to sort children because eg. in dict literal all keys come first and then all values
+        children = list(_get_ordered_child_nodes(node))
+        for child in reversed(children):
+            (prelim_end_lineno, prelim_end_col_offset) = \
+                _mark_text_ranges_rec(child, tokens, prelim_end_lineno, prelim_end_col_offset)
+
+        if "lineno" in node._attributes and "col_offset" in node._attributes:
+            # new "front" is beginning of this node
+            prelim_end_lineno = node.lineno
+            prelim_end_col_offset = node.col_offset
+
+        return (prelim_end_lineno, prelim_end_col_offset)
+
+
+    def _strip_trailing_junk_from_expressions(tokens):
+        while (tokens[-1].type not in (token.RBRACE, token.RPAR, token.RSQB,
+                                      token.NAME, token.NUMBER, token.STRING)
+                    and not (hasattr(token, "ELLIPSIS") and tokens[-1].type == token.ELLIPSIS)
+                    and tokens[-1].string not in ")}]"
+                    or tokens[-1].string in ['and', 'as', 'assert', 'class', 'def', 'del',
+                                              'elif', 'else', 'except', 'exec', 'finally',
+                                              'for', 'from', 'global', 'if', 'import', 'in',
+                                              'is', 'lambda', 'not', 'or', 'try',
+                                              'while', 'with', 'yield']):
+            del tokens[-1]
+
+    def _strip_trailing_extra_closers(tokens, remove_naked_comma):
+        level = 0
+        for i in range(len(tokens)):
+            if tokens[i].string in "({[":
+                level += 1
+            elif tokens[i].string in ")}]":
+                level -= 1
+
+            if level == 0 and tokens[i].string == "," and remove_naked_comma:
+                tokens[:] = tokens[0:i]
+                return
+
+            if level < 0:
+                tokens[:] = tokens[0:i]
+                return
+
+    def _strip_unclosed_brackets(tokens):
+        level = 0
+        for i in range(len(tokens)-1, -1, -1):
+            if tokens[i].string in "({[":
+                level -= 1
+            elif tokens[i].string in ")}]":
+                level += 1
+
+            if level < 0:
+                tokens[:] = tokens[0:i]
+                level = 0  # keep going, there may be more unclosed brackets
+
+    def _mark_end_and_return_child_tokens(node, tokens, prelim_end_lineno, prelim_end_col_offset):
+        """
+        # shortcut
+        node.end_lineno = prelim_end_lineno
+        node.end_col_offset = prelim_end_col_offset
+        return tokens
+        """
+        # prelim_end_lineno and prelim_end_col_offset are the start of
+        # next positioned node or end of source, ie. the suffix of given
+        # range may contain keywords, commas and other stuff not belonging to current node
+
+        # Function returns the list of tokens which cover all its children
+
+
+        if isinstance(node, _ast.stmt):
+            # remove empty trailing lines
+            while (tokens[-1].type in (tokenize.NL, tokenize.COMMENT, token.NEWLINE, token.INDENT)
+                   or tokens[-1].string in (":", "else", "elif", "finally", "except")):
+                del tokens[-1]
+
+        else:
+            _strip_trailing_extra_closers(tokens, not (isinstance(node, ast.Tuple) or isinstance(node, ast.Lambda)))
+            _strip_trailing_junk_from_expressions(tokens)
+            _strip_unclosed_brackets(tokens)
+
+        # set the end markers of this node
+        node.end_lineno = tokens[-1].end[0]
+        node.end_col_offset = tokens[-1].end[1]
+
+        # Peel off some trailing tokens which can't be part any
+        # positioned child node.
+        # TODO: maybe cleaning from parent side is better than
+        # _strip_trailing_junk_from_expressions
+
+        # Remove trailing empty parens from no-arg call
+        if (isinstance(node, ast.Call)
+            and _tokens_text(tokens[-2:]) == "()"):
+            del tokens[-2:]
+
+        # Remove trailing full slice
+        elif isinstance(node, ast.Subscript):
+            if  _tokens_text(tokens[-3:]) == "[:]":
+                del tokens[-3:]
+
+            elif _tokens_text(tokens[-4:]) == "[::]":
+                del tokens[-4:]
+
+        # Attribute name would confuse the "value" of Attribute
+        elif isinstance(node, ast.Attribute):
+            assert tokens[-1].type == token.NAME
+            del tokens[-1]
+            _strip_trailing_junk_from_expressions(tokens)
+
+        return tokens
+
+    all_tokens = list(tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline))
+    source_lines = source.splitlines(True)
+    fix_ast_problems(node, source_lines, all_tokens)
+    prelim_end_lineno = len(source_lines)
+    prelim_end_col_offset = len(source_lines[len(source_lines)-1])
+    _mark_text_ranges_rec(node, all_tokens, prelim_end_lineno, prelim_end_col_offset)
+
+
+
+def value_to_literal(value):
+    if value is None:
+        return ast.Name(id="None", ctx=ast.Load())
+    elif isinstance(value, bool):
+        if value:
+            return ast.Name(id="True", ctx=ast.Load())
+        else:
+            return ast.Name(id="False", ctx=ast.Load())
+    elif isinstance(value, str):
+        return ast.Str(s=value)
+    else:
+        raise NotImplementedError("only None, bool and str supported at the moment, not " + str(type(value)))
+
+
+
+def fix_ast_problems(tree, source_lines, tokens):
+    # Problem 1:
+    # Python parser gives col_offset as offset to its internal UTF-8 byte array
+    # I need offsets to chars
+    utf8_byte_lines = list(map(lambda line: line.encode("UTF-8"), source_lines))
+
+    # Problem 2:
+    # triple-quoted strings have just plain wrong positions: http://bugs.python.org/issue18370
+    # Fortunately lexer gives them correct positions
+    string_tokens = list(filter(lambda tok: tok.type == token.STRING, tokens))
+
+    # Problem 3:
+    # Binary operations have wrong positions: http://bugs.python.org/issue18374
+
+    # Problem 4:
+    # Function calls have wrong positions in Python 3.4: http://bugs.python.org/issue21295
+    # similar problem is with Attributes and Subscripts
+
+    def fix_node(node):
+        for child in _get_ordered_child_nodes(node):
+        #for child in ast.iter_child_nodes(node):
+            fix_node(child)
+
+        if isinstance(node, ast.Str):
+            # fix triple-quote problem
+            # get position from tokens
+            token = string_tokens.pop(0)
+            node.lineno, node.col_offset = token.start
+
+        elif ((isinstance(node, ast.Expr) or isinstance(node, ast.Attribute))
+            and isinstance(node.value, ast.Str)):
+            # they share the wrong offset of their triple-quoted child
+            # get position from already fixed child
+            # TODO: try whether this works when child is in parentheses
+            node.lineno = node.value.lineno
+            node.col_offset = node.value.col_offset
+
+        elif (isinstance(node, ast.BinOp)
+            and compare_node_positions(node, node.left) > 0):
+            # fix binop problem
+            # get position from an already fixed child
+            node.lineno = node.left.lineno
+            node.col_offset = node.left.col_offset
+
+        elif (isinstance(node, ast.Call)
+            and compare_node_positions(node, node.func) > 0):
+            # Python 3.4 call problem
+            # get position from an already fixed child
+            node.lineno = node.func.lineno
+            node.col_offset = node.func.col_offset
+
+        elif (isinstance(node, ast.Attribute)
+            and compare_node_positions(node, node.value) > 0):
+            # Python 3.4 attribute problem ...
+            node.lineno = node.value.lineno
+            node.col_offset = node.value.col_offset
+
+        elif (isinstance(node, ast.Subscript)
+            and compare_node_positions(node, node.value) > 0):
+            # Python 3.4 Subscript problem ...
+            node.lineno = node.value.lineno
+            node.col_offset = node.value.col_offset
+
+        else:
+            # Let's hope this node has correct lineno, and byte-based col_offset
+            # Now compute char-based col_offset
+            if hasattr(node, "lineno"):
+                byte_line = utf8_byte_lines[node.lineno-1]
+                char_col_offset = len(byte_line[:node.col_offset].decode("UTF-8"))
+                node.col_offset = char_col_offset
+
+
+    fix_node(tree)
+
+def compare_node_positions(n1, n2):
+    if n1.lineno > n2.lineno:
+        return 1
+    elif n1.lineno < n2.lineno:
+        return -1
+    elif n1.col_offset > n2.col_offset:
+        return 1
+    elif n2.col_offset < n2.col_offset:
+        return -1
+    else:
+        return 0
+
+
+def pretty(node, key="/", level=0):
+    """Used for testing and new test generation via AstView.
+    Don't change the format without updating tests"""
+    if isinstance(node, ast.AST):
+        fields = [(key, val) for key, val in ast.iter_fields(node)]
+        value_label = node.__class__.__name__
+        if isinstance(node, ast.Call):
+            # Try to make 3.4 AST-s more similar to 3.5
+            if sys.version_info[:2] == (3,4):
+                if ("kwargs", None) in fields:
+                    fields.remove(("kwargs", None))
+                if ("starargs", None) in fields:
+                    fields.remove(("starargs", None))
+
+            # TODO: translate also non-None kwargs and starargs
+
+    elif isinstance(node, list):
+        fields = list(enumerate(node))
+        if len(node) == 0:
+            value_label = "[]"
+        else:
+            value_label = "[...]"
+    else:
+        fields = []
+        value_label = repr(node)
+
+    item_text = level * '    ' + str(key) + "=" + value_label
+
+    if hasattr(node, "lineno"):
+        item_text += " @ " + str(getattr(node, "lineno"))
+        if hasattr(node, "col_offset"):
+            item_text += "." + str(getattr(node, "col_offset"))
+
+        if hasattr(node, "end_lineno"):
+            item_text += "  -  " + str(getattr(node, "end_lineno"))
+            if hasattr(node, "end_col_offset"):
+                item_text += "." + str(getattr(node, "end_col_offset"))
+
+    lines = [item_text] + [pretty(field_value, field_key, level+1)
+                           for field_key, field_value in fields]
+
+    return "\n".join(lines)
+
+
+def _get_ordered_child_nodes(node):
+    if isinstance(node, ast.Dict):
+        children = []
+        for i in range(len(node.keys)):
+            children.append(node.keys[i])
+            children.append(node.values[i])
+        return children
+    elif isinstance(node, ast.Call):
+        children = [node.func] + node.args
+
+        for kw in node.keywords:
+            children.append(kw.value)
+
+        # TODO: take care of Python 3.5 updates (eg. args=[Starred] and keywords)
+        if hasattr(node, "starargs") and node.starargs is not None:
+            children.append(node.starargs)
+        if hasattr(node, "kwargs") and node.kwargs is not None:
+            children.append(node.kwargs)
+
+        children.sort(key=lambda x: (x.lineno, x.col_offset))
+        return children
+
+    # arguments and their defaults are detached in the AST
+    elif isinstance(node, ast.arguments):
+        children = node.args + node.kwonlyargs + node.kw_defaults + node.defaults
+
+        if node.vararg is not None:
+            children.append(node.vararg)
+        if node.kwarg is not None:
+            children.append(node.kwarg)
+
+        children.sort(key=lambda x: (x.lineno, x.col_offset))
+        return children
+
+    else:
+        return ast.iter_child_nodes(node)
+
+def _tokens_text(tokens):
+    return "".join([t.string for t in tokens])
+
+def _range_contains_smaller(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, 
+                      other_lineno, other_col_offset, other_end_lineno, other_end_col_offset):
+    this_start = (self_lineno, self_col_offset)
+    this_end = (self_end_lineno, self_end_col_offset)
+    other_start = (other_lineno, other_col_offset)
+    other_end = (other_end_lineno, other_end_col_offset)
+    
+    return (this_start < other_start and this_end > other_end
+            or this_start == other_start and this_end > other_end
+            or this_start < other_start and this_end == other_end)
+
+def _range_contains_smaller_eq(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, 
+                      other_lineno, other_col_offset, other_end_lineno, other_end_col_offset):
+    return (_range_eq(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset)
+            or _range_contains_smaller(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset))
+
+def _range_eq(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, 
+                      other_lineno, other_col_offset, other_end_lineno, other_end_col_offset):
+    return (self_lineno == other_lineno
+            and self_col_offset == other_col_offset
+            and self_end_lineno == other_end_lineno
+            and self_end_col_offset == other_end_col_offset)
+            
\ No newline at end of file
diff --git a/thonny/shared/thonny/backend.py b/thonny/shared/thonny/backend.py
new file mode 100644
index 0000000..ecf9e2e
--- /dev/null
+++ b/thonny/shared/thonny/backend.py
@@ -0,0 +1,1298 @@
+# -*- coding: utf-8 -*-
+
+import sys 
+import os.path
+import inspect
+import ast
+import _ast
+import _io
+import traceback
+import types
+import logging
+import pydoc
+import builtins
+import site
+
+import __main__  # @UnresolvedImport
+
+from thonny import ast_utils
+from thonny.common import TextRange,\
+    parse_message, serialize_message, DebuggerCommand,\
+    ToplevelCommand, FrameInfo, InlineCommand, InputSubmission
+import signal
+import warnings
+
+BEFORE_STATEMENT_MARKER = "_thonny_hidden_before_stmt"
+BEFORE_EXPRESSION_MARKER = "_thonny_hidden_before_expr"
+AFTER_STATEMENT_MARKER = "_thonny_hidden_after_stmt"
+AFTER_EXPRESSION_MARKER = "_thonny_hidden_after_expr"
+
+EXCEPTION_TRACEBACK_LIMIT = 100
+DEBUG = True    
+
+logger = logging.getLogger()
+info = logger.info
+
+class VM:
+    def __init__(self):
+        self._main_dir = os.path.dirname(sys.modules["thonny"].__file__)
+        self._heap = {} # WeakValueDictionary would be better, but can't store reference to None
+        site.sethelper() # otherwise help function is not available
+        pydoc.pager = pydoc.plainpager # otherwise help command plays tricks
+        self._install_fake_streams()
+        self._current_executor = None
+        self._io_level = 0
+        
+        original_argv = sys.argv.copy()
+        original_path = sys.path.copy()
+        
+        # clean up path
+        sys.path = [d for d in sys.path if d != ""]
+        
+        # script mode
+        if len(sys.argv) > 1:
+            special_names_to_remove = set()
+            sys.argv[:] = sys.argv[1:] # shift argv[1] to position of script name
+            sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0]))) # add program's dir
+            __main__.__dict__["__file__"] = sys.argv[0]
+            # TODO: inspect.getdoc
+        
+        # shell mode
+        else:
+            special_names_to_remove = {"__file__", "__cached__"}
+            sys.argv[:] = [""] # empty "script name"
+            sys.path.insert(0, "")   # current dir
+        
+        # add jedi
+        if "JEDI_LOCATION" in os.environ:
+            sys.path.append(os.environ["JEDI_LOCATION"])
+    
+        # clean __main__ global scope
+        for key in list(__main__.__dict__.keys()):
+            if not key.startswith("__") or key in special_names_to_remove:
+                del __main__.__dict__[key] 
+        
+        # unset __doc__, then exec dares to write doc of the script there
+        __main__.__doc__ = None
+        
+        self.send_message(self.create_message("ToplevelResult",
+                          main_dir=self._main_dir,
+                          original_argv=original_argv,
+                          original_path=original_path,
+                          argv=sys.argv,
+                          path=sys.path,
+                          welcome_text="Python " + _get_python_version_string(),
+                          executable=sys.executable,
+                          in_venv=hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix,
+                          python_version=_get_python_version_string(),
+                          cwd=os.getcwd()))
+        
+        self._install_signal_handler()
+        
+    def mainloop(self):
+        try:
+            while True: 
+                try:
+                    cmd = self._fetch_command()
+                    self.handle_command(cmd, "waiting_toplevel_command")
+                except KeyboardInterrupt:
+                    logger.exception("Interrupt in mainloop")
+                    # Interrupt must always result in waiting_toplevel_command state
+                    # Don't show error messages, as the interrupted command may have been InlineCommand
+                    # (handlers of ToplevelCommands in normal cases catch the interrupt and provide
+                    # relevant message)  
+                    self.send_message(self.create_message("ToplevelResult"))
+        except:
+            logger.exception("Crash in mainloop")
+            
+            
+    def handle_command(self, cmd, command_context):
+        assert isinstance(cmd, ToplevelCommand) or isinstance(cmd, InlineCommand)
+        
+        error_response_type = "ToplevelResult" if isinstance(cmd, ToplevelCommand) else "InlineError"
+        try:
+            handler = getattr(self, "_cmd_" + cmd.command)
+        except AttributeError:
+            response = self.create_message(error_response_type, error="Unknown command: " + cmd.command)
+        else:
+            try:
+                response = handler(cmd)
+            except:
+                response = self.create_message(error_response_type,
+                    error="Thonny internal error: {0}".format(traceback.format_exc(EXCEPTION_TRACEBACK_LIMIT)))
+        
+        if response is not None:
+            response["command_context"] = command_context
+            response["command"] = cmd.command
+            if response["message_type"] == "ToplevelResult":
+                self._add_tkinter_info(response)
+            self.send_message(response)
+    
+    def _install_signal_handler(self):
+        def signal_handler(signal, frame):
+            raise KeyboardInterrupt("Execution interrupted")
+        
+        if os.name == 'nt':
+            signal.signal(signal.SIGBREAK, signal_handler)
+        else:
+            signal.signal(signal.SIGINT, signal_handler)        
+    
+    def _cmd_cd(self, cmd):
+        try:
+            os.chdir(cmd.path)
+            return self.create_message("ToplevelResult")
+        except Exception as e:
+            # TODO: should output user error
+            return self.create_message("ToplevelResult", error=str(e))
+    
+    def _cmd_Reset(self, cmd):
+        # nothing to do, because Reset always happens in fresh process
+        return self.create_message("ToplevelResult",
+                                   welcome_text="Python " + _get_python_version_string(),
+                                   executable=sys.executable)
+    
+    def _cmd_Run(self, cmd):
+        return self._execute_file(cmd, False)
+    
+    def _cmd_run(self, cmd):
+        return self._execute_file(cmd, False)
+    
+    def _cmd_Debug(self, cmd):
+        return self._execute_file(cmd, True)
+    
+    def _cmd_debug(self, cmd):
+        return self._execute_file(cmd, True)
+    
+    def _cmd_execute_source(self, cmd):
+        return self._execute_source(cmd, "ToplevelResult")
+    
+    def _cmd_execute_source_inline(self, cmd):
+        return self._execute_source(cmd, "InlineResult")
+    
+    def _cmd_tkupdate(self, cmd):
+        # advance the event loop
+        # http://bugs.python.org/issue989712
+        # http://bugs.python.org/file6090/run.py.diff
+        try:
+            root = self._get_tkinter_default_root()
+            if root is None:
+                return
+            
+            import tkinter
+            while root.dooneevent(tkinter._tkinter.DONT_WAIT):
+                pass
+                 
+        except:
+            pass
+            
+        return None
+    
+    
+    def _cmd_get_globals(self, cmd):
+        if not cmd.module_name in sys.modules:
+            raise ThonnyClientError("Module '{0}' is not loaded".format(cmd.module_name))
+        
+        return self.create_message("Globals", module_name=cmd.module_name,
+                              globals=self.export_variables(sys.modules[cmd.module_name].__dict__))
+    
+    def _cmd_get_locals(self, cmd):
+        for frame in inspect.stack():
+            if id(frame) == cmd.frame_id:
+                return self.create_message("Locals", locals=self.export_variables(frame.f_locals))
+        else:
+            raise ThonnyClientError("Frame '{0}' not found".format(cmd.frame_id))
+            
+    
+    def _cmd_get_heap(self, cmd):
+        result = {}
+        for key in self._heap:
+            result[key] = self.export_value(self._heap[key])
+            
+        return self.create_message("Heap", heap=result)
+    
+    def _cmd_shell_autocomplete(self, cmd):
+        error = None
+        try:
+            import jedi
+        except ImportError:
+            completions = []
+            error = "Could not import jedi"
+        else:
+            try:
+                #with warnings.catch_warnings():
+                interpreter = jedi.Interpreter(cmd.source, [__main__.__dict__])
+                completions = self._export_completions(interpreter.completions())
+            except Exception as e:
+                completions = []
+                error = "Autocomplete error: " + str(e)
+            except:
+                completions = []
+                error = "Autocomplete error"
+        
+        return self.create_message("ShellCompletions", 
+            source=cmd.source,
+            completions=completions,
+            error=error
+        )
+    
+    def _cmd_editor_autocomplete(self, cmd):
+        error = None
+        try:
+            import jedi
+            with warnings.catch_warnings():
+                script = jedi.Script(cmd.source, cmd.row, cmd.column, cmd.filename)
+                completions = self._export_completions(script.completions())
+                
+        except ImportError:
+            completions = []
+            error = "Could not import jedi"
+        except Exception as e:
+            completions = []
+            error = "Autocomplete error: " + str(e)
+        except:
+            completions = []
+            error = "Autocomplete error"
+        
+        return self.create_message("EditorCompletions", 
+                          source=cmd.source,
+                          row=cmd.row,
+                          column=cmd.column,
+                          filename=cmd.filename,
+                          completions=completions,
+                          error=error)
+    
+    def _export_completions(self, jedi_completions):
+        result = []
+        for c in jedi_completions:
+            if not c.name.startswith("__"):
+                record = {"name":c.name, "complete":c.complete, 
+                          "type":c.type, "description":c.description}
+                try:
+                    """ TODO: 
+                    if c.type in ["class", "module", "function"]:
+                        if c.type == "function":
+                            record["docstring"] = c.docstring()
+                        else:
+                            record["docstring"] = c.description + "\n" + c.docstring()
+                    """
+                except:
+                    pass
+                result.append(record)
+        return result
+        
+    
+    def _cmd_get_object_info(self, cmd):
+        if cmd.object_id in self._heap:
+            value = self._heap[cmd.object_id]
+            attributes = {}
+            for name in dir(value):
+                if not name.startswith("__") or cmd.all_attributes:
+                    #attributes[name] = inspect.getattr_static(value, name)
+                    try: 
+                        attributes[name] = getattr(value, name)
+                    except:
+                        pass 
+            
+            self._heap[id(type(value))] = type(value)
+            
+            info = {'id' : cmd.object_id,
+                    'repr' : repr(value),
+                    'type' : str(type(value)),
+                    'type_id' : id(type(value)),
+                    'attributes': self.export_variables(attributes)}
+            
+            if isinstance(value, _io.TextIOWrapper):
+                self._add_file_handler_info(value, info)
+            elif (type(value) in (types.BuiltinFunctionType, types.BuiltinMethodType,
+                                 types.FunctionType, types.LambdaType, types.MethodType)):
+                self._add_function_info(value, info)
+            elif (isinstance(value, list) 
+                  or isinstance(value, tuple)
+                  or isinstance(value, set)):
+                self._add_elements_info(value, info)
+            elif (isinstance(value, dict)):
+                self._add_entries_info(value, info)
+            
+        else:
+            info = {'id' : cmd.object_id,
+                    "repr": "<object info not found>",
+                    "type" : "object",
+                    "type_id" : id(object),
+                    "attributes" : {}}
+        
+        return self.create_message("ObjectInfo", id=cmd.object_id, info=info)
+    
+    def _get_tkinter_default_root(self):
+        tkinter = sys.modules.get("tkinter")
+        if tkinter is not None:
+            return getattr(tkinter, "_default_root", None)
+        else:
+            return None
+
+    
+    def _add_file_handler_info(self, value, info):
+        try:
+            assert isinstance(value.name, str)
+            assert value.mode in ("r", "rt", "tr", "br", "rb")
+            assert value.errors in ("strict", None)
+            assert value.newlines is None or value.tell() > 0
+            # TODO: cache the content
+            # TODO: don't read too big files
+            with open(value.name, encoding=value.encoding) as f:
+                info["file_encoding"] = f.encoding
+                info["file_content"] = f.read()
+                info["file_tell"] = value.tell()
+        except Exception as e:
+            info["file_error"] = "Could not get file content, error:" + str(e)
+            pass
+    
+    def _add_tkinter_info(self, msg):
+        # tkinter._default_root is not None,
+        # when window has been created and mainloop isn't called or hasn't ended yet
+        msg["tkinter_is_active"] = self._get_tkinter_default_root() is not None
+    
+    def _add_function_info(self, value, info):
+        try:
+            info["source"] = inspect.getsource(value)
+        except:
+            pass
+        
+    def _add_elements_info(self, value, info):
+        info["elements"] = []
+        for element in value:
+            info["elements"].append(self.export_value(element))
+        
+    def _add_entries_info(self, value, info):
+        info["entries"] = []
+        for key in value:
+            info["entries"].append((self.export_value(key),
+                                     self.export_value(value[key])))
+        
+    def _execute_file(self, cmd, debug_mode):
+        # args are accepted only in Run and Debug,
+        # and were stored in sys.argv already in VM.__init__
+        result_attributes = self._execute_source_ex(cmd.source, cmd.full_filename, "exec", debug_mode) 
+        return self.create_message("ToplevelResult", **result_attributes)
+    
+    def _execute_source(self, cmd, result_type):
+        filename = "<pyshell>"
+        
+        if hasattr(cmd, "global_vars"):
+            global_vars = cmd.global_vars
+        elif hasattr(cmd, "extra_vars"):
+            global_vars = __main__.__dict__.copy() # Don't want to mess with main namespace
+            global_vars.update(cmd.extra_vars)
+        else:
+            global_vars = __main__.__dict__
+
+        # let's see if it's single expression or something more complex
+        try:
+            root = ast.parse(cmd.source, filename=filename, mode="exec")
+        except SyntaxError as e:
+            return self.create_message(result_type,
+                error="".join(traceback.format_exception_only(SyntaxError, e)))
+            
+        assert isinstance(root, ast.Module)
+            
+        if len(root.body) == 1 and isinstance(root.body[0], ast.Expr):
+            mode = "eval"
+        else:
+            mode = "exec"
+            
+        result_attributes = self._execute_source_ex(cmd.source, filename, mode,
+            hasattr(cmd, "debug_mode") and cmd.debug_mode,
+            global_vars)
+        
+        if "__result__" in global_vars:
+            result_attributes["__result__"] = global_vars["__result__"]
+        
+        if hasattr(cmd, "request_id"):
+            result_attributes["request_id"] = cmd.request_id
+        else:
+            result_attributes["request_id"] = None
+        
+        return self.create_message(result_type, **result_attributes)
+        
+    def _execute_source_ex(self, source, filename, execution_mode, debug_mode,
+                        global_vars=None):
+        if debug_mode:
+            self._current_executor = FancyTracer(self)
+        else:
+            self._current_executor = Executor(self)
+        
+        try:
+            return self._current_executor.execute_source(source, 
+                                                         filename,
+                                                         execution_mode,
+                                                         global_vars)
+        finally:
+            self._current_executor = None
+    
+        
+    def _install_fake_streams(self):
+        self._original_stdin = sys.stdin
+        self._original_stdout = sys.stdout
+        self._original_stderr = sys.stderr        
+        
+        # yes, both out and err will be directed to out (but with different tags)
+        # this allows client to see the order of interleaving writes to stdout/stderr
+        sys.stdin = VM.FakeInputStream(self, sys.stdin)
+        sys.stdout = VM.FakeOutputStream(self, sys.stdout, "stdout")
+        sys.stderr = VM.FakeOutputStream(self, sys.stdout, "stderr") 
+             
+        # fake it properly: replace also "backup" streams
+        sys.__stdin__ = sys.stdin
+        sys.__stdout__ = sys.stdout
+        sys.__stderr__ = sys.stderr
+        
+    def _fetch_command(self):
+        line = self._original_stdin.readline()
+        if line == "":
+            logger.info("Read stdin EOF")
+            sys.exit()
+        cmd = parse_message(line)
+        return cmd
+
+    def create_message(self, message_type, **kwargs):
+        kwargs["message_type"] = message_type
+        if "cwd" not in kwargs:
+            kwargs["cwd"] = os.getcwd()
+            
+        return kwargs
+
+    def send_message(self, msg):
+        self._original_stdout.write(serialize_message(msg) + "\n")
+        self._original_stdout.flush()
+        
+    def export_value(self, value, skip_None=False):
+        if value is None and skip_None:
+            return None
+        
+        self._heap[id(value)] = value
+        try:
+            type_name = value.__class__.__name__
+        except:
+            type_name = type(value).__name__ 
+            
+        result = {'id' : id(value),
+                  'repr' : repr(value), 
+                  'type_name'  : type_name}
+        
+        return result
+    
+    def export_variables(self, variables):
+        result = {}
+        for name in variables:
+            if not name.startswith("_thonny_hidden_"):
+                result[name] = self.export_value(variables[name])
+            
+        return result
+           
+    def _debug(self, *args):
+        print("VM:", *args, file=self._original_stderr)
+    
+    
+    def _enter_io_function(self):
+        self._io_level += 1
+    
+    def _exit_io_function(self):
+        self._io_level -= 1
+    
+    def is_doing_io(self):
+        return self._io_level > 0
+    
+
+    class FakeStream:
+        def __init__(self, vm, target_stream):
+            self._vm = vm
+            self._target_stream = target_stream
+            
+        def isatty(self):
+            return True
+        
+        def __getattr__(self, name):
+            # TODO: is it safe to perform those other functions without notifying vm
+            # via _enter_io_function?
+            return getattr(self._target_stream, name)
+        
+    class FakeOutputStream(FakeStream):
+        def __init__(self, vm, target_stream, stream_name):
+            VM.FakeStream.__init__(self, vm, target_stream)
+            self._stream_name = stream_name
+            
+        def write(self, data):
+            try:
+                self._vm._enter_io_function()
+                if data != "":
+                    self._vm.send_message(self._vm.create_message("ProgramOutput", stream_name=self._stream_name, data=data))
+            finally:
+                self._vm._exit_io_function()
+        
+        def writelines(self, lines):
+            try:
+                self._vm._enter_io_function()
+                self.write(''.join(lines))
+            finally:
+                self._vm._exit_io_function()
+    
+    class FakeInputStream(FakeStream):
+        
+        def _generic_read(self, method, limit=-1):
+            try:
+                self._vm._enter_io_function()
+                self._vm.send_message(self._vm.create_message("InputRequest", method=method, limit=limit))
+                
+                while True:
+                    cmd = self._vm._fetch_command()
+                    if isinstance(cmd, InputSubmission):
+                        return cmd.data
+                    elif isinstance(cmd, InlineCommand):
+                        self._vm.handle_command(cmd, "waiting_input")
+                    else:
+                        raise ThonnyClientError("Wrong type of command when waiting for input")
+            finally:
+                self._vm._exit_io_function()
+        
+        def read(self, limit=-1):
+            return self._generic_read("read", limit)
+        
+        def readline(self, limit=-1):
+            return self._generic_read("readline", limit)
+        
+        def readlines(self, limit=-1):
+            return self._generic_read("readlines", limit)
+            
+    
+
+
+class Executor:
+    def __init__(self, vm):
+        self._vm = vm
+    
+    def execute_source(self, source, filename, mode, global_vars=None):
+        
+        if global_vars is None:
+            global_vars = __main__.__dict__
+        
+        try:
+            bytecode = self._compile_source(source, filename, mode)
+            if hasattr(self, "_trace"):
+                sys.settrace(self._trace)    
+            if mode == "eval":
+                value = eval(bytecode, global_vars)
+                if value is not None:
+                    builtins._ = value 
+                return {"value_info" : self._vm.export_value(value)}
+            else:
+                assert mode == "exec"
+                exec(bytecode, global_vars) # <Marker: remove this line from stacktrace>
+                return {"context_info" : "after normal execution", "source" : source, "filename" : filename, "mode" : mode}
+        except SyntaxError as e:
+            return {"error" : "".join(traceback.format_exception_only(SyntaxError, e))}
+        except ThonnyClientError as e:
+            return {"error" : str(e)}
+        except SystemExit:
+            e_type, e_value, e_traceback = sys.exc_info()
+            self._print_user_exception(e_type, e_value, e_traceback)
+            return {"SystemExit" : True}
+        except:
+            # other unhandled exceptions (supposedly client program errors) are printed to stderr, as usual
+            # for VM mainloop they are not exceptions
+            e_type, e_value, e_traceback = sys.exc_info()
+            self._print_user_exception(e_type, e_value, e_traceback)
+            return {"context_info" : "other unhandled exception"}
+        finally:
+            sys.settrace(None)
+    
+    def _print_user_exception(self, e_type, e_value, e_traceback):
+        lines = traceback.format_exception(e_type, e_value, e_traceback)
+
+        for line in lines:
+            # skip lines denoting thonny execution frame
+            if ("thonny/backend" in line 
+                or "thonny\\backend" in line
+                or "remove this line from stacktrace" in line):
+                continue
+            else:
+                sys.stderr.write(line)
+
+    def _compile_source(self, source, filename, mode):
+        return compile(source, filename, mode)
+
+
+class FancyTracer(Executor):
+    
+    def __init__(self, vm):
+        self._vm = vm
+        self._normcase_thonny_src_dir = os.path.normcase(os.path.dirname(sys.modules["thonny"].__file__)) 
+        self._instrumented_files = _PathSet()
+        self._interesting_files = _PathSet() # only events happening in these files are reported
+        self._current_command = None
+        self._unhandled_exception = None
+        self._install_marker_functions()
+        self._custom_stack = []
+    
+    def execute_source(self, source, filename, mode, global_vars=None):
+        self._current_command = DebuggerCommand(command="step", state=None, focus=None, frame_id=None, exception=None)
+        
+        return Executor.execute_source(self, source, filename, mode, global_vars)
+        #assert len(self._custom_stack) == 0
+        
+    def _install_marker_functions(self):
+        # Make dummy marker functions universally available by putting them
+        # into builtin scope        
+        self.marker_function_names = {
+            BEFORE_STATEMENT_MARKER,
+            AFTER_STATEMENT_MARKER, 
+            BEFORE_EXPRESSION_MARKER,
+            AFTER_EXPRESSION_MARKER,
+        }
+        
+        for name in self.marker_function_names:
+            if not hasattr(builtins, name):
+                setattr(builtins, name, getattr(self, name))
+        
+    def _is_interesting_exception(self, frame):
+        # interested only in exceptions in command frame or it's parent frames
+        cmd = self._current_command
+        return (id(frame) == cmd.frame_id
+                or not self._frame_is_alive(cmd.frame_id))
+
+    def _compile_source(self, source, filename, mode):
+        root = ast.parse(source, filename, mode)
+        
+        ast_utils.mark_text_ranges(root, source)
+        self._tag_nodes(root)        
+        self._insert_expression_markers(root)
+        self._insert_statement_markers(root)
+        self._instrumented_files.add(filename)
+        
+        return compile(root, filename, mode)
+    
+    def _may_step_in(self, code):
+            
+        return not (
+            code is None 
+            or code.co_filename is None
+            or code.co_flags & inspect.CO_GENERATOR  # @UndefinedVariable
+            or sys.version_info >= (3,5) and code.co_flags & inspect.CO_COROUTINE  # @UndefinedVariable
+            or sys.version_info >= (3,5) and code.co_flags & inspect.CO_ITERABLE_COROUTINE  # @UndefinedVariable
+            or sys.version_info >= (3,6) and code.co_flags & inspect.CO_ASYNC_GENERATOR  # @UndefinedVariable
+            or "importlib._bootstrap" in code.co_filename
+            or os.path.normcase(code.co_filename) not in self._instrumented_files 
+                and code.co_name not in self.marker_function_names
+            or os.path.normcase(code.co_filename).startswith(self._normcase_thonny_src_dir)
+                and code.co_name not in self.marker_function_names
+            or self._vm.is_doing_io() 
+        )
+        
+    
+    def _trace(self, frame, event, arg):
+        """
+        1) Detects marker calls and responds to client queries in these spots
+        2) Maintains a customized view of stack
+        """
+        if not self._may_step_in(frame.f_code):
+            return
+        
+        code_name = frame.f_code.co_name
+        
+        if event == "call":
+            self._unhandled_exception = None # some code is running, therefore exception is not propagating anymore
+            
+            if code_name in self.marker_function_names:
+                # the main thing
+                if code_name == BEFORE_STATEMENT_MARKER:
+                    event = "before_statement"
+                elif code_name == AFTER_STATEMENT_MARKER:
+                    event = "after_statement"
+                elif code_name == BEFORE_EXPRESSION_MARKER:
+                    event = "before_expression"
+                elif code_name == AFTER_EXPRESSION_MARKER:
+                    event = "after_expression"
+                else:
+                    raise AssertionError("Unknown marker function")
+                
+                marker_function_args = frame.f_locals.copy()
+                del marker_function_args["self"]
+                
+                self._handle_progress_event(frame.f_back, event, marker_function_args)
+                self._try_interpret_as_again_event(frame.f_back, event, marker_function_args)
+                
+                
+            else:
+                # Calls to proper functions.
+                # Client doesn't care about these events,
+                # it cares about "before_statement" events in the first statement of the body
+                self._custom_stack.append(CustomStackFrame(frame, "call"))
+        
+        elif event == "return":
+            if code_name not in self.marker_function_names:
+                self._custom_stack.pop()
+                if len(self._custom_stack) == 0:
+                    # We popped last frame, this means our program has ended.
+                    # There may be more events coming from upper (system) frames
+                    # but we're not interested in those
+                    sys.settrace(None)
+            else:
+                pass
+                
+        elif event == "exception":
+            exc = arg[1]
+            if self._unhandled_exception is None:
+                # this means it's the first time we see this exception
+                exc.causing_frame = frame
+            else:
+                # this means the exception is propagating to older frames
+                # get the causing_frame from previous occurrence
+                exc.causing_frame = self._unhandled_exception.causing_frame 
+            
+            self._unhandled_exception = exc
+            if self._is_interesting_exception(frame):
+                self._report_state_and_fetch_next_message(frame)
+
+        # TODO: support line event in non-instrumented files
+        elif event == "line":
+            self._unhandled_exception = None  
+                
+        return self._trace
+        
+            
+    def _handle_progress_event(self, frame, event, args):
+        """
+        Tries to respond to current command in this state. 
+        If it can't, then it returns, program resumes
+        and _trace will call it again in another state.
+        Otherwise sends response and fetches next command.  
+        """
+        self._debug("Progress event:", event, self._current_command)
+        focus = TextRange(*args["text_range"])
+        
+        self._custom_stack[-1].last_event = event
+        self._custom_stack[-1].last_event_focus = focus
+        self._custom_stack[-1].last_event_args = args
+        
+        # Select the correct method according to the command
+        tester = getattr(self, "_cmd_" + self._current_command.command + "_completed")
+             
+        # If method decides we're in the right place to respond to the command ...
+        if tester(frame, event, args, focus, self._current_command):
+            if event == "after_expression":
+                value = self._vm.export_value(args["value"])
+            else:
+                value = None
+            self._report_state_and_fetch_next_message(frame, value)
+    
+    def _report_state_and_fetch_next_message(self, frame, value=None):
+            #self._debug("Completed command: ", self._current_command)
+            
+            if self._unhandled_exception is not None:
+                frame_infos = traceback.format_stack(self._unhandled_exception.causing_frame)
+                # I want to show frames from current frame to causing_frame
+                if frame == self._unhandled_exception.causing_frame:
+                    interesting_frame_infos = []
+                else:
+                    # c how far is current frame from causing_frame?
+                    _distance = 0
+                    _f = self._unhandled_exception.causing_frame 
+                    while _f != frame:
+                        _distance += 1
+                        _f = _f.f_back
+                        if _f == None:
+                            break
+                    interesting_frame_infos = frame_infos[-_distance:]
+                exception_lower_stack_description = "".join(interesting_frame_infos)
+                exception_msg = str(self._unhandled_exception)
+            else:
+                exception_lower_stack_description = None 
+                exception_msg = None
+            
+            self._vm.send_message(self._vm.create_message("DebuggerProgress",
+                command=self._current_command.command,
+                stack=self._export_stack(),
+                exception=self._vm.export_value(self._unhandled_exception, True),
+                exception_msg=exception_msg,
+                exception_lower_stack_description=exception_lower_stack_description,
+                value=value,
+                command_context="waiting_debugger_command"
+            ))
+            
+            # Fetch next debugger command
+            self._current_command = self._vm._fetch_command()
+            self._debug("got command:", self._current_command)
+            # get non-progress commands out our way
+            self._respond_to_inline_commands()  
+            assert isinstance(self._current_command, DebuggerCommand)
+            
+        # Return and let Python run to next progress event
+        
+    
+    def _try_interpret_as_again_event(self, frame, original_event, original_args):
+        """
+        Some after_* events can be interpreted also as 
+        "before_*_again" events (eg. when last argument of a call was 
+        evaluated, then we are just before executing the final stage of the call)
+        """
+
+        if original_event == "after_expression":
+            node_tags = original_args.get("node_tags")
+            value = original_args.get("value")
+            
+            if (node_tags is not None 
+                and ("last_child" in node_tags
+                     or "or_arg" in node_tags and value
+                     or "and_arg" in node_tags and not value)):
+                
+                # next step will be finalizing evaluation of parent of current expr
+                # so let's say we're before that parent expression
+                again_args = {"text_range" : original_args.get("parent_range"),
+                              "node_tags" : ""}
+                again_event = ("before_expression_again" 
+                               if "child_of_expression" in node_tags
+                               else "before_statement_again")
+                
+                self._handle_progress_event(frame, again_event, again_args)
+                
+    
+    def _respond_to_inline_commands(self):
+        while isinstance(self._current_command, InlineCommand): 
+            self._vm.handle_command(self._current_command, "waiting_debugger_command")
+            self._current_command = self._vm._fetch_command()
+    
+    def _get_frame_source_info(self, frame):
+        if frame.f_code.co_name == "<module>":
+            obj = inspect.getmodule(frame)
+            lineno = 1
+        else:
+            obj = frame.f_code
+            lineno = obj.co_firstlineno
+        
+        # lineno returned by getsourcelines is not consistent between modules vs functions
+        lines, _ = inspect.getsourcelines(obj) 
+        return "".join(lines), lineno
+        
+    
+    
+    def _cmd_exec_completed(self, frame, event, args, focus, cmd):
+        """
+        Identifies the moment when piece of code indicated by cmd.frame_id and cmd.focus
+        has completed execution (either successfully or not).
+        """
+        
+        # it's meant to be executed in before* state, but if we're not there
+        # we'll step there
+        
+        if cmd.state not in ("before_expression", "before_expression_again",
+                             "before_statement", "before_statement_again"):
+            return self._cmd_step_completed(frame, event, args, focus, cmd)
+        
+        
+        if id(frame) == cmd.frame_id:
+            
+            if focus.is_smaller_in(cmd.focus):
+                # we're executing a child of command focus,
+                # keep running
+                return False 
+            
+            elif focus == cmd.focus:
+                
+                if event.startswith("before_"):
+                    # we're just starting
+                    return False
+                
+                elif (event == "after_expression"
+                      and cmd.state in ("before_expression", "before_expression_again")
+                      or 
+                      event == "after_statement"
+                      and cmd.state in ("before_statement", "before_statement_again")):
+                    # Normal completion
+                    # Maybe there was an exception, but this is forgotten now
+                    cmd._unhandled_exception = False
+                    self._debug("Exec normal")
+                    return True
+                
+                
+                elif (cmd.state in ("before_statement", "before_statement_again")
+                      and event == "after_expression"):
+                    # Same code range can contain expression statement and expression.
+                    # Here we need to run just a bit more
+                    return False
+                
+                else:
+                    # shouldn't be here
+                    raise AssertionError("Unexpected state in responding to " + str(cmd))
+                    
+            else:
+                # We're outside of starting focus, assumedly because of an exception
+                self._debug("Exec outside", cmd.focus, focus)
+                return True
+        
+        else:
+            # We're in another frame
+            if self._frame_is_alive(cmd.frame_id):
+                # We're in a successor frame, keep running
+                return False
+            else:
+                # Original frame has completed, assumedly because of an exception
+                # We're done
+                self._debug("Exec wrong frame")
+                return True
+            
+
+    
+    def _cmd_step_completed(self, frame, event, args, focus, cmd):
+        return True
+    
+    def _cmd_run_to_before_completed(self, frame, event, args, focus, cmd):
+        return event.startswith("before")
+    
+    def _cmd_out_completed(self, frame, event, args, focus, cmd):
+        """Complete current frame"""
+        return (
+            # the frame has completed
+            not self._frame_is_alive(cmd.frame_id)
+            # we're in the same frame but on higher level 
+            or id(frame) == cmd.frame_id and focus.contains_smaller(cmd.focus)
+        )
+    
+    
+    def _cmd_line_completed(self, frame, event, args, focus, cmd):
+        return (event == "before_statement" 
+            and os.path.normcase(frame.f_code.co_filename) == os.path.normcase(cmd.target_filename)
+            and focus.lineno == cmd.target_lineno
+            and (focus != cmd.focus or id(frame) != cmd.frame_id))
+
+    
+    def _frame_is_alive(self, frame_id):
+        for frame in self._custom_stack:
+            if frame.id == frame_id:
+                return True
+        else:
+            return False 
+    
+    def _export_stack(self):
+        result = []
+        
+        for custom_frame in self._custom_stack:
+            
+            last_event_args = custom_frame.last_event_args.copy()
+            if "value" in last_event_args:
+                last_event_args["value"] = self._vm.export_value(last_event_args["value"]) 
+            
+            system_frame = custom_frame.system_frame
+            source, firstlineno = self._get_frame_source_info(system_frame)
+            
+            result.append(FrameInfo(
+                id=id(system_frame),
+                filename=system_frame.f_code.co_filename,
+                module_name=system_frame.f_globals["__name__"],
+                code_name=system_frame.f_code.co_name,
+                locals=self._vm.export_variables(system_frame.f_locals),
+                source=source,
+                firstlineno=firstlineno,
+                last_event=custom_frame.last_event,
+                last_event_args=last_event_args,
+                last_event_focus=custom_frame.last_event_focus,
+            ))
+        
+        return result
+
+    def _thonny_hidden_before_stmt(self, text_range, node_tags):
+        """
+        The code to be debugged will be instrumented with this function
+        inserted before each statement. 
+        Entry into this function indicates that statement as given
+        by the code range is about to be evaluated next.
+        """
+        return None
+    
+    def _thonny_hidden_after_stmt(self, text_range, node_tags):
+        """
+        The code to be debugged will be instrumented with this function
+        inserted after each statement. 
+        Entry into this function indicates that statement as given
+        by the code range was just executed successfully.
+        """
+        return None
+    
+    def _thonny_hidden_before_expr(self, text_range, node_tags):
+        """
+        Entry into this function indicates that expression as given
+        by the code range is about to be evaluated next
+        """ 
+        return text_range
+    
+    def _thonny_hidden_after_expr(self, text_range, node_tags, value, parent_range):
+        """
+        The code to be debugged will be instrumented with this function
+        wrapped around each expression (given as 2nd argument). 
+        Entry into this function indicates that expression as given
+        by the code range was just evaluated to given value
+        """ 
+        return value
+    
+
+    def _tag_nodes(self, root):
+        """Marks interesting properties of AST nodes"""
+        
+        def add_tag(node, tag):
+            if not hasattr(node, "tags"):
+                node.tags = set()
+                node.tags.add("class=" + node.__class__.__name__)
+            node.tags.add(tag)
+        
+        for node in ast.walk(root):
+            
+            # tag last children 
+            last_child = ast_utils.get_last_child(node)
+            if last_child is not None and last_child:
+                add_tag(node, "has_children")
+                
+                if isinstance(last_child, ast.AST):
+                    last_child.parent_node = node
+                    add_tag(last_child, "last_child")
+                    if isinstance(node, _ast.expr):
+                        add_tag(last_child, "child_of_expression")
+                    else:
+                        add_tag(last_child, "child_of_statement")
+                    
+                    if isinstance(node, ast.Call):
+                        add_tag(last_child, "last_call_arg")
+                    
+            # other cases
+            if isinstance(node, ast.Call):
+                add_tag(node.func, "call_function")
+                node.func.parent_node = node
+                
+            if isinstance(node, ast.BoolOp) and node.op == ast.Or():
+                for child in node.values:
+                    add_tag(child, "or_arg")
+                    child.parent_node = node
+                    
+            if isinstance(node, ast.BoolOp) and node.op == ast.And():
+                for child in node.values:
+                    add_tag(child, "and_arg")
+                    child.parent_node = node
+            
+            # TODO: assert (it doesn't evaluate msg when test == True)
+            
+            if isinstance(node, ast.Str):
+                add_tag(node, "StringLiteral")
+                
+            if isinstance(node, ast.Num):
+                add_tag(node, "NumberLiteral")
+            
+            if isinstance(node, ast.ListComp):
+                add_tag(node.elt, "ListComp.elt")
+                
+            if isinstance(node, ast.SetComp):
+                add_tag(node.elt, "SetComp.elt")
+                
+            if isinstance(node, ast.DictComp):
+                add_tag(node.key, "DictComp.key")
+                add_tag(node.value, "DictComp.value")
+            
+            if isinstance(node, ast.comprehension):
+                for expr in node.ifs:
+                    add_tag(expr, "comprehension.if")
+                
+                
+            # make sure every node has this field
+            if not hasattr(node, "tags"):
+                node.tags = set()
+            
+    
+    def _should_instrument_as_expression(self, node):
+        return (isinstance(node, _ast.expr)
+                and (not hasattr(node, "ctx") or isinstance(node.ctx, ast.Load))
+                # TODO: repeatedly evaluated subexpressions of comprehensions
+                # can be supported (but it requires some redesign both in backend and GUI)
+                and "ListComp.elt" not in node.tags 
+                and "SetComp.elt" not in node.tags
+                and "DictComp.key" not in node.tags
+                and "DictComp.value" not in node.tags
+                and "comprehension.if" not in node.tags
+                )         
+        return 
+    
+    def _should_instrument_as_statement(self, node):
+        return (isinstance(node, _ast.stmt)
+                # Shouldn't insert anything before from __future__ import
+                # as this is not a normal statement
+                # https://bitbucket.org/plas/thonny/issues/183/thonny-throws-false-positive-syntaxerror
+                and (not isinstance(node, _ast.ImportFrom)
+                     or node.module != "__future__"))
+    
+    def _insert_statement_markers(self, root):
+        # find lists of statements and insert before/after markers for each statement
+        for name, value in ast.iter_fields(root):
+            if isinstance(value, ast.AST):
+                self._insert_statement_markers(value)
+            elif isinstance(value, list):
+                if len(value) > 0:
+                    new_list = []
+                    for node in value:
+                        if self._should_instrument_as_statement(node):
+                            # self._debug("EBFOMA", node)
+                            # add before marker
+                            new_list.append(self._create_statement_marker(node, 
+                                                                          BEFORE_STATEMENT_MARKER))
+                        
+                        # original statement
+                        if self._should_instrument_as_statement(node):
+                            self._insert_statement_markers(node)
+                        new_list.append(node)
+                        
+                        if isinstance(node, _ast.stmt):
+                            # add after marker
+                            new_list.append(self._create_statement_marker(node,
+                                                                          AFTER_STATEMENT_MARKER))
+                    setattr(root, name, new_list)
+    
+    
+    def _create_statement_marker(self, node, function_name):
+        call = self._create_simple_marker_call(node, function_name)
+        stmt = ast.Expr(value=call)
+        ast.copy_location(stmt, node)
+        ast.fix_missing_locations(stmt)
+        return stmt
+        
+    
+    def _insert_expression_markers(self, node):
+        """
+        each expression e gets wrapped like this:
+            _after(_before(_loc, _node_is_zoomable), e, _node_role, _parent_range)
+        where
+            _after is function that gives the resulting value
+            _before is function that signals the beginning of evaluation of e
+            _loc gives the code range of e
+            _node_is_zoomable indicates whether this node has subexpressions
+            _node_role is either 'last_call_arg', 'last_op_arg', 'first_or_arg', 
+                                 'first_and_arg', 'function' or None
+        """
+        tracer = self
+        
+        class ExpressionVisitor(ast.NodeTransformer):
+            def generic_visit(self, node):
+                if isinstance(node, _ast.expr):
+                    if isinstance(node, ast.Starred):
+                        # keep this node as is, but instrument its children
+                        return ast.NodeTransformer.generic_visit(self, node)
+                    elif tracer._should_instrument_as_expression(node):
+                        # before marker 
+                        before_marker = tracer._create_simple_marker_call(node, BEFORE_EXPRESSION_MARKER)
+                        ast.copy_location(before_marker, node)
+                        
+                        # after marker
+                        after_marker = ast.Call (
+                            func=ast.Name(id=AFTER_EXPRESSION_MARKER, ctx=ast.Load()),
+                            args=[
+                                before_marker,
+                                tracer._create_tags_literal(node),
+                                ast.NodeTransformer.generic_visit(self, node),
+                                tracer._create_location_literal(node.parent_node if hasattr(node, "parent_node") else None)
+                            ],
+                            keywords=[]
+                        )
+                        ast.copy_location(after_marker, node)
+                        ast.fix_missing_locations(after_marker)
+                        
+                        return after_marker
+                    else:
+                        # This expression (and its children) should be ignored
+                        return node
+                else:
+                    # Descend into statements
+                    return ast.NodeTransformer.generic_visit(self, node)
+        
+        return ExpressionVisitor().visit(node)   
+            
+    
+    def _create_location_literal(self, node):
+        if node is None:
+            return ast_utils.value_to_literal(None)
+        
+        assert hasattr(node, "end_lineno")
+        assert hasattr(node, "end_col_offset")
+        
+        nums = []
+        for value in node.lineno, node.col_offset, node.end_lineno, node.end_col_offset:
+            nums.append(ast.Num(n=value))
+        return ast.Tuple(elts=nums, ctx=ast.Load())
+    
+    def _create_tags_literal(self, node):
+        if hasattr(node, "tags"):
+            # maybe set would perform as well, but I think string is faster
+            return ast_utils.value_to_literal(",".join(node.tags))
+            #self._debug("YESTAGS")
+        else:
+            #self._debug("NOTAGS " + str(node))
+            return ast_utils.value_to_literal("")
+    
+    def _create_simple_marker_call(self, node, fun_name):
+        assert hasattr(node, "end_lineno")
+        assert hasattr(node, "end_col_offset")
+        
+        args = [
+            self._create_location_literal(node),
+            self._create_tags_literal(node),
+        ]
+        
+        return ast.Call (
+            func=ast.Name(id=fun_name, ctx=ast.Load()),
+            args=args,
+            keywords=[]
+        )
+    
+    def _debug(self, *args):
+        print("TRACER:", *args, file=self._vm._original_stderr)
+
+class CustomStackFrame:
+    def __init__(self, frame, last_event, focus=None):
+        self.id = id(frame)
+        self.system_frame = frame
+        self.last_event = last_event
+        self.focus = None
+        
+class ThonnyClientError(Exception):
+    pass
+    
+
+def fdebug(frame, msg, *args):
+    if logger.isEnabledFor(logging.DEBUG):
+        logger.debug(_get_frame_prefix(frame) + msg, *args)
+    
+    
+def _get_frame_prefix(frame):
+    return str(id(frame)) + " " + ">" * len(inspect.getouterframes(frame, 0)) + " "
+    
+def _get_python_version_string(add_word_size=False):
+    result = ".".join(map(str, sys.version_info[:3]))
+    if sys.version_info[3] != "final":
+        result += "-" + sys.version_info[3]
+    
+    if add_word_size:
+        result += " (" + ("64" if sys.maxsize > 2**32 else "32")+ " bit)"
+    
+    return result    
+
+class _PathSet:
+    "implementation of set whose in operator works well for filenames"
+    def __init__(self):
+        self._normcase_set = set()
+        
+    def add(self, name):
+        self._normcase_set.add(os.path.normcase(name))
+    
+    def remove(self, name):
+        self._normcase_set.remove(os.path.normcase(name))
+    
+    def clear(self):
+        self._normcase_set.clear()
+    
+    def __contains__(self, name):
+        return os.path.normcase(name) in self._normcase_set
+
+    def __iter__(self):
+        for item in self._normcase_set:
+            yield item
diff --git a/thonny/shared/thonny/common.py b/thonny/shared/thonny/common.py
new file mode 100644
index 0000000..b60914f
--- /dev/null
+++ b/thonny/shared/thonny/common.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+ 
+"""
+Classes used both by front-end and back-end
+"""
+import shlex
+
+class Record:
+    def __init__(self, **kw):
+        self.__dict__.update(kw)
+    
+    def update(self, **kw):
+        self.__dict__.update(kw)
+    
+    def setdefault(self, **kw):
+        "updates those fields that are not yet present (similar to dict.setdefault)"
+        for key in kw:
+            if not hasattr(self, key):
+                setattr(self, key, kw[key])
+    
+    def __repr__(self):
+        keys = self.__dict__.keys()
+        items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
+        return "{}({})".format(self.__class__.__name__, ", ".join(items))
+    
+    def __str__(self):
+        keys = sorted(self.__dict__.keys())
+        items = ("{}={!r}".format(k, str(self.__dict__[k])) for k in keys)
+        return "{}({})".format(self.__class__.__name__, ", ".join(items))
+    
+    def __eq__(self, other):
+        if type(self) != type(other):
+            return False
+        
+        if len(self.__dict__) != len(other.__dict__):
+            return False 
+        
+        for key in self.__dict__:
+            if not hasattr(other, key):
+                return False
+            self_value = getattr(self, key)
+            other_value = getattr(other, key)
+            
+            if type(self_value) != type(other_value) or self_value != other_value:
+                return False
+        
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+        
+    def __hash__(self):
+        return hash(repr(self))
+
+
+class TextRange(Record):
+    def __init__(self, lineno, col_offset, end_lineno, end_col_offset):
+        self.lineno = lineno
+        self.col_offset = col_offset
+        self.end_lineno = end_lineno
+        self.end_col_offset = end_col_offset
+    
+    def contains_smaller(self, other):
+        this_start = (self.lineno, self.col_offset)
+        this_end = (self.end_lineno, self.end_col_offset)
+        other_start = (other.lineno, other.col_offset)
+        other_end = (other.end_lineno, other.end_col_offset)
+        
+        return (this_start < other_start and this_end > other_end
+                or this_start == other_start and this_end > other_end
+                or this_start < other_start and this_end == other_end)
+    
+    def contains_smaller_eq(self, other):
+        return self.contains_smaller(other) or self == other
+    
+    def not_smaller_in(self, other):
+        return not other.contains_smaller(self)
+
+    def is_smaller_in(self, other):
+        return other.contains_smaller(self)
+    
+    def not_smaller_eq_in(self, other):
+        return not other.contains_smaller_eq(self)
+
+    def is_smaller_eq_in(self, other):
+        return other.contains_smaller_eq(self)
+    
+    def get_start_index(self):
+        return str(self.lineno) + "." + str(self.col_offset)
+    
+    def get_end_index(self):
+        return str(self.end_lineno) + "." + str(self.end_col_offset)
+    
+    def __str__(self):
+        return "TR(" + str(self.lineno) + "." + str(self.col_offset) + ", " \
+                     + str(self.end_lineno) + "." + str(self.end_col_offset) + ")"
+    
+    
+                 
+class FrameInfo(Record):
+    def get_description(self):
+        return (
+            "[" + str(self.id) + "] "
+            + self.code_name + " in " + self.filename
+            + ", focus=" + str(self.focus)
+        )
+
+
+class ToplevelCommand(Record):
+    pass
+
+class DebuggerCommand(Record):
+    def __init__(self, command, **kw):
+        Record.__init__(self, **kw)
+        self.command = command
+
+class InputSubmission(Record):
+    pass
+
+class InlineCommand(Record):
+    """
+    Can be used both during debugging and between debugging.
+    Initially meant for sending variable and heap info requests
+    """
+    def __init__(self, command, **kw):
+        Record.__init__(self, **kw)
+        self.command = command
+
+
+class CommandSyntaxError(Exception):
+    pass
+
+def parse_shell_command(cmd_line, split_arguments=True):
+    assert cmd_line.startswith("%")
+    
+    parts = cmd_line.strip().split(maxsplit=1)
+    command = parts[0][1:] # remove %
+    
+    if len(parts) == 1:
+        arg_str = ""
+    else:
+        arg_str = parts[1]
+        
+    if split_arguments:
+        args = shlex.split(arg_str.strip(), posix=True) 
+    else:
+        args = [arg_str]
+        
+    return (command, args)
+
+
+def serialize_message(msg):
+    # I want to transfer only ASCII chars because 
+    # encodings are not reliable 
+    # (eg. can't find a way to specify PYTHONIOENCODING for cx_freeze'd program) 
+    return repr(msg).encode("UTF-7").decode("ASCII") 
+
+def parse_message(msg_string):
+    return eval(msg_string.encode("ASCII").decode("UTF-7"))
+
+
+
+def quote_path_for_shell(path):
+    for c in path:
+        if (not c.isalpha() 
+            and not c.isnumeric()
+            and c not in "-_./\\"):
+            return '"' + path.replace('"', '\\"') + '"'
+    else:
+        return path
+
+
+def print_structure(o):
+    print(o.__class__.__name__)
+    for attr in dir(o):
+        print(attr, "=", getattr(o, attr))
+
+class UserError(RuntimeError):
+    pass
+
+if __name__ == "__main__":
+    tr1 = TextRange(1,0,1,10)
+    tr2 = TextRange(1,0,1,10)
+    print(tr2.contains_smaller(tr1))
diff --git a/thonny/shell.py b/thonny/shell.py
new file mode 100644
index 0000000..d11d05a
--- /dev/null
+++ b/thonny/shell.py
@@ -0,0 +1,640 @@
+# -*- coding: utf-8 -*-
+
+import os.path
+import re
+from tkinter import ttk
+import traceback
+
+import thonny
+from thonny import memory, roughparse
+from thonny.common import ToplevelCommand, parse_shell_command
+from thonny.misc_utils import running_on_mac_os, shorten_repr
+from thonny.ui_utils import EnhancedTextWithLogging
+import tkinter as tk
+from thonny.globals import get_workbench, get_runner
+from thonny.codeview import EDIT_BACKGROUND, PythonText
+from thonny.tktextext import index2line
+
+
+class ShellView (ttk.Frame):
+    def __init__(self, master, **kw):
+        ttk.Frame.__init__(self, master, **kw)
+        
+        self.vert_scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
+        self.vert_scrollbar.grid(row=0, column=2, sticky=tk.NSEW)
+        self.text = ShellText(self,
+                            font=get_workbench().get_font("EditorFont"),
+                            #foreground="white",
+                            #background="#666666",
+                            highlightthickness=0,
+                            #highlightcolor="LightBlue",
+                            borderwidth=0,
+                            yscrollcommand=self.vert_scrollbar.set,
+                            padx=4,
+                            insertwidth=2,
+                            height=10,
+                            undo=True)
+        
+        get_workbench().event_generate("ShellTextCreated", text_widget=self.text)
+        get_workbench().add_command("clear_shell", "edit", "Clear shell",
+                                    self.clear_shell,
+                                    group=200)
+        
+        self.text.grid(row=0, column=1, sticky=tk.NSEW)
+        self.vert_scrollbar['command'] = self.text.yview
+        self.columnconfigure(1, weight=1)
+        self.rowconfigure(0, weight=1)
+
+    def focus_set(self):
+        self.text.focus_set()
+    
+    def add_command(self, command, handler):
+        self.text.add_command(command, handler)
+
+    def submit_command(self, cmd_line):
+        self.text.submit_command(cmd_line)
+    
+    def clear_shell(self):
+        self.text._clear_shell()
+        
+    def report_exception(self, prelude=None, conclusion=None):
+        if prelude is not None:
+            self.text.direct_insert("end", prelude + "\n", ("stderr",))
+        
+        self.text.direct_insert("end", traceback.format_exc() + "\n", ("stderr",))
+        
+        if conclusion is not None:
+            self.text.direct_insert("end", conclusion + "\n", ("stderr",))
+        
+
+
+class ShellText(EnhancedTextWithLogging, PythonText):
+    
+    def __init__(self, master, cnf={}, **kw):
+        if not "background" in kw:
+            kw["background"] = EDIT_BACKGROUND
+            
+        EnhancedTextWithLogging.__init__(self, master, cnf, **kw)
+        self.bindtags(self.bindtags() + ('ShellText',))
+        
+        self._before_io = True
+        self._command_history = [] # actually not really history, because each command occurs only once
+        self._command_history_current_index = None
+        
+        
+        """
+        self.margin = tk.Text(self,
+                width = 4,
+                padx = 4,
+                highlightthickness = 0,
+                takefocus = 0,
+                bd = 0,
+                #font = self.font,
+                cursor = "dotbox",
+                background = '#e0e0e0',
+                foreground = '#999999',
+                #state='disabled'
+                )
+        self.margin.grid(row=0, column=0)
+        """
+        
+        self.bind("<Up>", self._arrow_up, True)
+        self.bind("<Down>", self._arrow_down, True)
+        self.bind("<KeyPress>", self._text_key_press, True)
+        self.bind("<KeyRelease>", self._text_key_release, True)
+        
+        prompt_font = get_workbench().get_font("BoldEditorFont")
+        vert_spacing = 10
+        io_indent = 16
+        code_indent = prompt_font.measure(">>> ")
+        
+        
+        self.tag_configure("toplevel", font=get_workbench().get_font("EditorFont"))
+        self.tag_configure("prompt", foreground="purple", font=prompt_font)
+        self.tag_configure("command", foreground="black",
+                           lmargin1=code_indent, lmargin2=code_indent)
+        self.tag_configure("welcome", foreground="DarkGray", font=get_workbench().get_font("EditorFont"))
+        self.tag_configure("automagic", foreground="DarkGray", font=get_workbench().get_font("EditorFont"))
+        self.tag_configure("value", foreground="DarkBlue") 
+        self.tag_configure("error", foreground="Red")
+        
+        self.tag_configure("io", lmargin1=io_indent, lmargin2=io_indent, rmargin=io_indent,
+                                font=get_workbench().get_font("IOFont"))
+        self.tag_configure("stdin", foreground="Blue")
+        self.tag_configure("stdout", foreground="Black")
+        self.tag_configure("stderr", foreground="Red")
+        self.tag_configure("hyperlink", foreground="#3A66DD", underline=True)
+        self.tag_bind("hyperlink", "<ButtonRelease-1>", self._handle_hyperlink)
+        self.tag_bind("hyperlink", "<Enter>", self._hyperlink_enter)
+        self.tag_bind("hyperlink", "<Leave>", self._hyperlink_leave)
+        
+        self.tag_configure("vertically_spaced", spacing1=vert_spacing)
+        self.tag_configure("inactive", foreground="#aaaaaa")
+        
+        # create 3 marks: input_start shows the place where user entered but not-yet-submitted
+        # input starts, output_end shows the end of last output,
+        # output_insert shows where next incoming program output should be inserted
+        self.mark_set("input_start", "end-1c")
+        self.mark_gravity("input_start", tk.LEFT)
+        
+        self.mark_set("output_end", "end-1c")
+        self.mark_gravity("output_end", tk.LEFT)
+        
+        self.mark_set("output_insert", "end-1c")
+        self.mark_gravity("output_insert", tk.RIGHT)
+        
+        
+        self.active_object_tags = set()
+        
+        self._command_handlers = {}
+        
+        self._last_configuration = None
+    
+        get_workbench().bind("InputRequest", self._handle_input_request, True)
+        get_workbench().bind("ProgramOutput", self._handle_program_output, True)
+        get_workbench().bind("ToplevelResult", self._handle_toplevel_result, True)
+        
+        self._init_menu()
+    
+    def _init_menu(self):
+        self._menu = tk.Menu(self, tearoff=False)
+        self._menu.add_command(label="Clear shell", command=self._clear_shell)
+    
+    def add_command(self, command, handler):
+        self._command_handlers[command] = handler
+        
+    def submit_command(self, cmd_line):
+        assert get_runner().get_state() == "waiting_toplevel_command"
+        self.delete("input_start", "end")
+        self.insert("input_start", cmd_line, ("automagic",))
+        self.see("end")
+        self.mark_set("insert", "end")
+        self._try_submit_input()
+    
+    def _handle_input_request(self, msg):
+        self["font"] = get_workbench().get_font("IOFont") # otherwise the cursor is of toplevel size
+        self.focus_set()
+        self.mark_set("insert", "end")
+        self.tag_remove("sel", "1.0", tk.END)
+        self._current_input_request = msg
+        self._try_submit_input() # try to use leftovers from previous request
+        self.see("end")
+
+    def _handle_program_output(self, msg):
+        self["font"] = get_workbench().get_font("IOFont")
+        
+        # mark first line of io
+        if self._before_io:
+            self._insert_text_directly(msg.data[0], ("io", msg.stream_name, "vertically_spaced"))
+            self._before_io = False
+            self._insert_text_directly(msg.data[1:], ("io", msg.stream_name))
+        else:
+            self._insert_text_directly(msg.data, ("io", msg.stream_name))
+        
+        self.mark_set("output_end", self.index("end-1c"))
+        self.see("end")
+            
+    def _handle_toplevel_result(self, msg):
+        self["font"] = get_workbench().get_font("EditorFont")
+        self._before_io = True
+        if hasattr(msg, "error"):
+            self._insert_text_directly(msg.error + "\n", ("toplevel", "error"))
+        
+        if hasattr(msg, "welcome_text"):
+            configuration = get_workbench().get_option("run.backend_configuration") 
+            welcome_text = msg.welcome_text
+            if hasattr(msg, "executable") and msg.executable != thonny.running.get_private_venv_executable():
+                welcome_text += " (" + msg.executable + ")"
+            if (configuration != self._last_configuration
+                and not (self._last_configuration is None and not configuration)):
+                    self._insert_text_directly(welcome_text, ("welcome",))
+                    
+            self._last_configuration = get_workbench().get_option("run.backend_configuration")
+            
+        
+        if hasattr(msg, "value_info"):
+            value_repr = shorten_repr(msg.value_info["repr"], 10000)
+            if value_repr != "None":
+                if get_workbench().in_heap_mode():
+                    value_repr = memory.format_object_id(msg.value_info["id"])
+                object_tag = "object_" + str(msg.value_info["id"])
+                self._insert_text_directly(value_repr + "\n", ("toplevel",
+                                                               "value",
+                                                               object_tag))
+                if running_on_mac_os():
+                    sequence = "<Command-Button-1>"
+                else:
+                    sequence = "<Control-Button-1>"
+                self.tag_bind(object_tag, sequence,
+                                   lambda _: get_workbench().event_generate(
+                                        "ObjectSelect", object_id=msg.value_info["id"]))
+                
+                self.active_object_tags.add(object_tag)
+        
+        self.mark_set("output_end", self.index("end-1c"))
+        self._insert_prompt()
+        self._try_submit_input() # Trying to submit leftover code (eg. second magic command)
+        self.see("end")
+            
+    def _insert_prompt(self):
+        # if previous output didn't put a newline, then do it now
+        if not self.index("output_insert").endswith(".0"):
+            self._insert_text_directly("\n", ("io",))
+        
+        prompt_tags = ("toplevel", "prompt")
+         
+        # if previous line has value or io then add little space
+        prev_line = self.index("output_insert - 1 lines")
+        prev_line_tags = self.tag_names(prev_line)
+        if "io" in prev_line_tags or "value" in prev_line_tags:
+            prompt_tags += ("vertically_spaced",)
+            #self.tag_add("last_result_line", prev_line)
+        
+        self._insert_text_directly(">>> ", prompt_tags)
+        self.edit_reset();
+    
+    def intercept_insert(self, index, txt, tags=()):
+        if (self._editing_allowed()
+            and self._in_current_input_range(index)):
+            #self._print_marks("before insert")
+            # I want all marks to stay in place
+            self.mark_gravity("input_start", tk.LEFT)
+            self.mark_gravity("output_insert", tk.LEFT)
+            
+            if get_runner().get_state() == "waiting_input":
+                tags = tags + ("io", "stdin")
+            else:
+                tags = tags + ("toplevel", "command")
+            
+            EnhancedTextWithLogging.intercept_insert(self, index, txt, tags)
+            
+            if get_runner().get_state() == "waiting_input":
+                if self._before_io:
+                    # tag first char of io differently
+                    self.tag_add("vertically_spaced", index)
+                    self._before_io = False
+                    
+                self._try_submit_input()
+            
+            self.see("insert")
+        else:
+            self.bell()
+            
+    def intercept_delete(self, index1, index2=None, **kw):
+        if index1 == "sel.first" and index2 == "sel.last" and not self.has_selection():
+            return
+        
+        if (self._editing_allowed() 
+            and self._in_current_input_range(index1)
+            and (index2 is None or self._in_current_input_range(index2))):
+            self.direct_delete(index1, index2, **kw)
+        else:
+            self.bell()
+    
+    def perform_return(self, event):
+        if get_runner().get_state() == "waiting_input":
+            # if we are fixing the middle of the input string and pressing ENTER
+            # then we expect the whole line to be submitted not linebreak to be inserted
+            # (at least that's how IDLE works)
+            self.mark_set("insert", "end") # move cursor to the end
+            
+            # Do the return without auto indent
+            EnhancedTextWithLogging.perform_return(self, event)
+             
+            self._try_submit_input()
+            
+        elif get_runner().get_state() == "waiting_toplevel_command":
+            # Same with editin middle of command, but only if it's a single line command
+            whole_input = self.get("input_start", "end-1c") # asking the whole input
+            if ("\n" not in whole_input
+                and self._code_is_ready_for_submission(whole_input)):
+                self.mark_set("insert", "end") # move cursor to the end
+                # Do the return without auto indent
+                EnhancedTextWithLogging.perform_return(self, event)
+            else:
+                # Don't want auto indent when code is ready for submission
+                source = self.get("input_start", "insert")
+                tail = self.get("insert", "end")
+                
+                if self._code_is_ready_for_submission(source + "\n", tail):
+                    # No auto-indent
+                    EnhancedTextWithLogging.perform_return(self, event)
+                else:
+                    # Allow auto-indent
+                    PythonText.perform_return(self, event)
+                
+            self._try_submit_input()
+            
+        return "break"
+    
+    def on_secondary_click(self, event):
+        super().on_secondary_click(event)
+        self._menu.tk_popup(event.x_root, event.y_root)
+        
+    def _in_current_input_range(self, index):
+        try:
+            return self.compare(index, ">=", "input_start")
+        except:
+            return False
+    
+    def _insert_text_directly(self, txt, tags=()):
+        def _insert(txt, tags):
+            if txt != "":
+                self.direct_insert("output_insert", txt, tags)
+                
+        # I want the insertion to go before marks 
+        #self._print_marks("before output")
+        self.mark_gravity("input_start", tk.RIGHT)
+        self.mark_gravity("output_insert", tk.RIGHT)
+        tags = tuple(tags)
+        
+        if "stderr" in tags or "error" in tags:
+            # show lines pointing to source lines as hyperlinks
+            for line in txt.splitlines(True):
+                parts = re.split(r'(File .* line \d+.*)$', line, maxsplit=1)
+                if len(parts) == 3 and "<pyshell" not in line:
+                    _insert(parts[0], tags)
+                    _insert(parts[1], tags + ("hyperlink",))
+                    _insert(parts[2], tags)
+                else:
+                    _insert(line, tags)
+        else:
+            _insert(txt, tags)
+            
+        #self._print_marks("after output")
+        # output_insert mark will move automatically because of its gravity
+    
+    
+    def _try_submit_input(self):
+        # see if there is already enough inputted text to submit
+        input_text = self.get("input_start", "insert")
+        tail = self.get("insert", "end")
+        
+        # user may have pasted more text than necessary for this request
+        submittable_text = self._extract_submittable_input(input_text, tail)
+        
+        if submittable_text is not None:
+            if get_runner().get_state() == "waiting_toplevel_command":
+                # clean up the tail
+                if len(tail) > 0:
+                    assert tail.strip() == ""
+                    self.delete("insert", "end-1c")
+                    
+            
+            # leftover text will be kept in widget, waiting for next request.
+            start_index = self.index("input_start")
+            end_index = self.index("input_start+{0}c".format(len(submittable_text)))
+            
+            # apply correct tags (if it's leftover then it doesn't have them yet)
+            if get_runner().get_state() == "waiting_input":
+                self.tag_add("io", start_index, end_index)
+                self.tag_add("stdin", start_index, end_index)
+            else:
+                self.tag_add("toplevel", start_index, end_index)
+                self.tag_add("command", start_index, end_index)
+                
+            
+            
+            # update start mark for next input range
+            self.mark_set("input_start", end_index)
+            
+            # Move output_insert mark after the requested_text
+            # Leftover input, if any, will stay after output_insert, 
+            # so that any output that will come in before
+            # next input request will go before leftover text
+            self.mark_set("output_insert", end_index)
+            
+            # remove tags from leftover text
+            for tag in ("io", "stdin", "toplevel", "command"):
+                # don't remove automagic, because otherwise I can't know it's auto 
+                self.tag_remove(tag, end_index, "end")
+                
+            self._submit_input(submittable_text)
+    
+    def _editing_allowed(self):
+        return get_runner().get_state() in ('waiting_toplevel_command', 'waiting_input')
+    
+    def _extract_submittable_input(self, input_text, tail):
+        
+        if get_runner().get_state() == "waiting_toplevel_command":
+            if input_text.endswith("\n"):
+                if input_text.strip().startswith("%"):
+                    # if several magic command are submitted, then take only first
+                    return input_text[:input_text.index("\n")+1]
+                elif self._code_is_ready_for_submission(input_text, tail):
+                    return input_text
+                else:
+                    return None
+            else:
+                return None
+            
+        elif get_runner().get_state() == "waiting_input":
+            input_request = self._current_input_request
+            method = input_request.method
+            limit = input_request.limit
+            # TODO: what about EOF?
+            if isinstance(limit, int) and limit < 0:
+                limit = None
+            
+            if method == "readline":
+                # TODO: is it correct semantics?
+                i = 0
+                if limit == 0:
+                    return ""
+                
+                while True:
+                    if i >= len(input_text):
+                        return None
+                    elif limit is not None and i+1 == limit:
+                        return input_text[:i+1]
+                    elif input_text[i] == "\n":
+                        return input_text[:i+1]
+                    else:
+                        i += 1
+            else:
+                raise AssertionError("only readline is supported at the moment")
+    
+    def _code_is_ready_for_submission(self, source, tail=""):
+        # Ready to submit if ends with empty line 
+        # or is complete single-line code
+        
+        if tail.strip() != "":
+            return False
+        
+        # First check if it has unclosed parens, unclosed string or ending with : or \
+        parser = roughparse.RoughParser(self.indentwidth, self.tabwidth)
+        parser.set_str(source.rstrip() + "\n")
+        if (parser.get_continuation_type() != roughparse.C_NONE
+                or parser.is_block_opener()):
+            return False
+        
+        # Multiline compound statements need to end with empty line to be considered
+        # complete.
+        lines = source.splitlines()
+        # strip starting empty and comment lines
+        while (len(lines) > 0
+               and (lines[0].strip().startswith("#")
+                    or lines[0].strip() == "")):
+            lines.pop(0)
+        
+        compound_keywords = ["if", "while", "for", "with", "try", "def", "class", "async", "await"]
+        if len(lines) > 0:
+            first_word = lines[0].strip().split()[0]
+            if (first_word in compound_keywords
+                and not source.replace(" ", "").replace("\t", "").endswith("\n\n")):
+                # last line is not empty
+                return False
+        
+        return True
+    
+    def _submit_input(self, text_to_be_submitted):
+        if get_runner().get_state() == "waiting_toplevel_command":
+            # register in history and count
+            if text_to_be_submitted in self._command_history:
+                self._command_history.remove(text_to_be_submitted)
+            self._command_history.append(text_to_be_submitted)
+            self._command_history_current_index = None # meaning command selection is not in process
+            
+            try:
+                if text_to_be_submitted.startswith("%"):
+                    command, _ = parse_shell_command(text_to_be_submitted)
+                    get_workbench().event_generate("MagicCommand", cmd_line=text_to_be_submitted)
+                    if command in self._command_handlers:
+                        self._command_handlers[command](text_to_be_submitted)
+                        get_workbench().event_generate("AfterKnownMagicCommand", cmd_line=text_to_be_submitted)
+                    else:
+                        self._insert_text_directly("Unknown magic command: " + command)
+                        self._insert_prompt()
+                else:
+                    get_runner().send_command(
+                        ToplevelCommand(command="execute_source",
+                                        source=text_to_be_submitted))
+                
+            except:
+                get_workbench().report_exception()
+                self._insert_prompt()
+                
+            get_workbench().event_generate("ShellCommand", command_text=text_to_be_submitted)
+        else:
+            assert get_runner().get_state() == "waiting_input"
+            get_runner().send_program_input(text_to_be_submitted)
+            get_workbench().event_generate("ShellInput", input_text=text_to_be_submitted)
+    
+    
+    def _arrow_up(self, event):
+        if not self._in_current_input_range("insert"):
+            return
+
+        insert_line = index2line(self.index("insert"))
+        input_start_line = index2line(self.index("input_start"))
+        if insert_line != input_start_line:
+            # we're in the middle of a multiline command
+            return
+        
+        if len(self._command_history) == 0 or self._command_history_current_index == 0:
+            # can't take previous command
+            return "break"
+        
+        if self._command_history_current_index is None:
+            self._command_history_current_index = len(self._command_history)-1
+        else:
+            self._command_history_current_index -= 1
+        
+        cmd = self._command_history[self._command_history_current_index]
+        if cmd[-1] == "\n": 
+            cmd = cmd[:-1] # remove the submission linebreak
+        self._propose_command(cmd)
+        return "break"
+    
+    def _arrow_down(self, event):
+        if not self._in_current_input_range("insert"):
+            return
+        
+        insert_line = index2line(self.index("insert"))
+        last_line = index2line(self.index("end-1c"))
+        if insert_line != last_line:
+            # we're in the middle of a multiline command
+            return
+        
+        if (len(self._command_history) == 0 
+            or self._command_history_current_index == len(self._command_history)-1):
+            # can't take next command
+            return "break"
+        
+        
+        if self._command_history_current_index is None:
+            self._command_history_current_index = len(self._command_history)-1
+        else:
+            self._command_history_current_index += 1
+
+        self._propose_command(self._command_history[self._command_history_current_index].strip("\n"))
+        return "break"
+    
+    def _propose_command(self, cmd_line):
+        self.delete("input_start", "end")
+        self.intercept_insert("input_start", cmd_line)
+        self.see("insert")
+    
+    def _text_key_press(self, event):
+        # TODO: this underline may confuse, when user is just copying on pasting
+        # try to add this underline only when mouse is over the value
+        """
+        if event.keysym in ("Control_L", "Control_R", "Command"):  # TODO: check in Mac
+            self.tag_configure("value", foreground="DarkBlue", underline=1)
+        """
+    
+    def _text_key_release(self, event):
+        if event.keysym in ("Control_L", "Control_R", "Command"):  # TODO: check in Mac
+            self.tag_configure("value", foreground="DarkBlue", underline=0)
+
+    def _clear_shell(self):
+        end_index = self.index("output_end")
+        self.direct_delete("1.0", end_index)
+
+    def compute_smart_home_destination_index(self):
+        """Is used by EnhancedText"""
+        
+        if self._in_current_input_range("insert"):
+            # on input line, go to just after prompt
+            return "input_start"
+        else:
+            return super().compute_smart_home_destination_index()
+    
+    def _hyperlink_enter(self, event):
+        self.config(cursor="hand2")
+        
+    def _hyperlink_leave(self, event):
+        self.config(cursor="")
+        
+    def _handle_hyperlink(self, event):
+        try:
+            line = self.get("insert linestart", "insert lineend")
+            matches = re.findall(r'File "([^"]+)", line (\d+)', line)
+            if len(matches) == 1 and len(matches[0]) == 2:
+                filename, lineno = matches[0]
+                lineno = int(lineno)
+                if os.path.exists(filename) and os.path.isfile(filename):
+                    # TODO: better use events instead direct referencing
+                    get_workbench().get_editor_notebook().show_file(filename, lineno)
+        except:
+            traceback.print_exc()
+    
+    
+    def _invalidate_current_data(self):
+        """
+        Grayes out input & output displayed so far
+        """
+        end_index = self.index("output_end")
+        
+        self.tag_add("inactive", "1.0", end_index)
+        self.tag_remove("value", "1.0", end_index)
+        
+        while len(self.active_object_tags) > 0:
+            self.tag_remove(self.active_object_tags.pop(), "1.0", "end")
+        
+        
+
+    
+
+    
+    
\ No newline at end of file
diff --git a/thonny/test/__init__.py b/thonny/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/thonny/test/plugins/__init__.py b/thonny/test/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/thonny/test/plugins/test_coloring.py b/thonny/test/plugins/test_coloring.py
new file mode 100644
index 0000000..a9ac6c9
--- /dev/null
+++ b/thonny/test/plugins/test_coloring.py
@@ -0,0 +1,45 @@
+import tkinter
+
+from thonny.plugins.coloring import SyntaxColorer
+import tkinter.font as tk_font
+
+
+TEST_STR1 = """def my_function():
+    str1 = "aslas'"
+    str2 = 'asdasd"asda
+    str3 = '''asdasdasd
+    asdas
+    sdsds'''
+"""
+
+
+def test_open_closed_strings():
+
+    text_widget = tkinter.Text()
+    text_widget.insert("insert", TEST_STR1)
+
+    font = tk_font.nametofont("TkDefaultFont")
+    colorer = SyntaxColorer(text_widget, font, font)
+    colorer._update_coloring()
+
+    open_ranges = text_widget.tag_ranges("STRING_OPEN")
+    closed_ranges = text_widget.tag_ranges("STRING_CLOSED") + text_widget.tag_ranges("STRING_CLOSED3") 
+
+    expected_open_ranges = {('3.11', '4.0'), }
+    expected_closed_ranges = {('2.11', '2.19'), ('4.11', '6.12'), }
+
+    open_ranges_set = set([(str(open_ranges[i]), str(open_ranges[i+1])) for i in range(0, len(open_ranges), 2)])
+    closed_ranges_set = set([(str(closed_ranges[i]), str(closed_ranges[i+1])) for i in range(0, len(closed_ranges), 2)])
+
+    assert open_ranges_set == expected_open_ranges
+    assert closed_ranges_set == expected_closed_ranges
+    print("test passed")
+
+
+def run_tests():
+    test_open_closed_strings()
+
+if __name__ == "__main__":
+    print("Test input: ")
+    print(TEST_STR1)
+    run_tests()
diff --git a/thonny/test/plugins/test_locals_marker.py b/thonny/test/plugins/test_locals_marker.py
new file mode 100644
index 0000000..877784d
--- /dev/null
+++ b/thonny/test/plugins/test_locals_marker.py
@@ -0,0 +1,41 @@
+import tkinter
+
+from thonny.plugins.locals_marker import LocalsHighlighter
+
+TEST_STR1 = """num_cars = 3
+def foo():
+    print(num_cars + num_cars)
+def too():
+    num_cars = 4
+    print(num_cars + num_cars)
+def joo():
+    global num_cars
+    num_cars = 2
+"""
+
+
+def test_regular_closed():
+
+    expected_local = {('5.4', '5.12'),
+                      ('6.10', '6.18'),
+                      ('6.21', '6.29'),
+                      }
+
+    text_widget = tkinter.Text()
+    text_widget.insert("end", TEST_STR1)
+
+    highlighter = LocalsHighlighter(text_widget)
+
+    actual_local = highlighter.get_positions()
+
+    assert actual_local == expected_local
+    print("Passed.")
+
+
+def run_tests():
+    test_regular_closed()
+
+if __name__ == "__main__":
+    print("Test input: ")
+    print(TEST_STR1)
+    run_tests()
\ No newline at end of file
diff --git a/thonny/test/plugins/test_name_highlighter.py b/thonny/test/plugins/test_name_highlighter.py
new file mode 100644
index 0000000..3d1e1a8
--- /dev/null
+++ b/thonny/test/plugins/test_name_highlighter.py
@@ -0,0 +1,104 @@
+import tkinter
+from thonny.plugins.highlight_names import VariablesHighlighter
+
+
+TEST_STR1 = """def foo():
+    foo()
+    pass
+def boo(narg):
+    foo = 2  # line 5
+    boo = foo + 4
+    print(narg + 2)
+for i in range(5):
+    boo()
+narg = 2  # line 10
+def bar():
+    x + x
+def blarg():
+    x = 2
+"""
+# tuple of tuples, where an inner tuple corresponds to a group of insert positions
+# that should produce the same output (corresponding expected output is in the
+# expected_indices tuple at the same index)
+#
+# consider TEST_STR1:
+#
+# The first group is four indices, where we would expect the two locations of the name "foo"
+# to be returned. Those expected two locations are specified at index 0 of tuple expected_indices.
+#
+# Second tuple is a group of one index, where we would expect output with the locations for "boo"
+# And if the insert location is at "pass", we would expect an empty set for output
+CURSOR_POSITIONS1 = (("1.4", "1.5", "1.7", "2.5"),
+                     ("4.6",),
+                     ("3.4",),
+                     ("5.7", "6.12"),
+                     ("4.10", "7.11"),
+                     ("10.2",),
+                     ("12.5", "12.9",),
+                     ("14.5",),
+                     )
+
+EXPECTED_INDICES1 = ({("1.4", "1.7"), ("2.4", "2.7")},
+                     {("4.4", "4.7"), ("9.4", "9.7")},
+                     set(),
+                     {("5.4", "5.7"), ("6.10", "6.13")},
+                     {("4.8", "4.12"), ("7.10", "7.14")},
+                     {("10.0", "10.4")},
+                     {("12.4", "12.5"), ("12.8", "12.9")},
+                     {("14.4", "14.5")},
+                     )
+
+TEST_STR2 = """import too
+def foo():
+    foo = too + 4
+    x = foo + bow
+# 5
+class TestClass:
+    def foo(self):
+        pass
+    def add3(self):
+        self.foo()  # 10
+        foo()
+"""
+CURSOR_POSITIONS2 = (("1.8", "3.10"),
+                     ("2.4", "2.5", "11.10"),
+                     ("3.5", "4.9"),
+                     )
+EXPECTED_INDICES2 = ({("1.7", "1.10"), ("3.10", "3.13"), },
+                     {("2.4", "2.7"), ("11.8", "11.11"), },
+                     {("3.4", "3.7"), ("4.8", "4.11"), },
+                     )
+
+TEST_GROUPS = (
+    (CURSOR_POSITIONS1, EXPECTED_INDICES1, TEST_STR1),
+    (CURSOR_POSITIONS2, EXPECTED_INDICES2, TEST_STR2),
+)
+
+
+def run_tests():
+    for i, test in enumerate(TEST_GROUPS):
+        print("Running test group %d: " % (i + 1))
+        _assert_returns_correct_indices(test[0], test[1], test[2])
+
+
+def _assert_returns_correct_indices(insert_pos_groups, expected_indices, input_str):
+    text_widget = tkinter.Text()
+    text_widget.insert("end", input_str)
+
+    nh = VariablesHighlighter()
+    nh.text = text_widget
+    for i, group in enumerate(insert_pos_groups):
+        for insert_pos in group:
+            text_widget.mark_set("insert", insert_pos)
+
+            actual = nh.get_positions()
+            expected = expected_indices[i]
+
+            assert actual == expected, "\nInsert position: %s" \
+                                       "\nExpected: %s" \
+                                       "\nGot: %s" % (insert_pos, expected, actual)
+        print("\rPassed %d of %d" % (i+1, len(insert_pos_groups)), end="")
+    print()
+
+if __name__ == "__main__":
+    run_tests()
diff --git a/thonny/test/plugins/test_paren_matcher.py b/thonny/test/plugins/test_paren_matcher.py
new file mode 100644
index 0000000..ec28b94
--- /dev/null
+++ b/thonny/test/plugins/test_paren_matcher.py
@@ -0,0 +1,43 @@
+import tkinter
+
+from thonny.plugins.paren_matcher import ParenMatcher
+
+TEST_STR1 = """age = int(input("Enter age: "))
+if age > 18:
+    l = ["H", "I"]
+    print(l)
+else:
+    print("Hello!", end='')
+    print("What's your name?")
+"""
+
+
+def test_regular_closed():
+    insert_pos_groups = (("1.9", "1.10", "1.13", "1.31"),
+                         ("1.30", "1.29", "1.25", "1.15"))
+    expected_indices = (("1.9", "1.30", []),
+                        ("1.15", "1.29", []))
+
+    text_widget = tkinter.Text()
+    text_widget.insert("end", TEST_STR1)
+
+    matcher = ParenMatcher(text_widget)
+    matcher.text = text_widget
+    for i, group in enumerate(insert_pos_groups):
+        for insert_pos in group:
+            text_widget.mark_set("insert", insert_pos)
+
+            actual = matcher.find_surrounding("1.0", "end")
+            expected = expected_indices[i]
+
+            assert actual == expected, "\nExpected: %s\nGot: %s" % (expected, actual)
+        print("\rPassed %d of %d" % (i+1, len(insert_pos_groups)), end="")
+
+
+def run_tests():
+    test_regular_closed()
+
+if __name__ == "__main__":
+    print("Test input: ")
+    print(TEST_STR1)
+    run_tests()
diff --git a/thonny/test/test_ast_utils.py b/thonny/test/test_ast_utils.py
new file mode 100644
index 0000000..58b67d6
--- /dev/null
+++ b/thonny/test/test_ast_utils.py
@@ -0,0 +1,79 @@
+import ast
+from thonny.ast_utils import pretty 
+from textwrap import dedent
+
+def test_pretty_without_end_markers():
+    p = pretty(ast.parse(dedent("""
+    age = int(input("Enter age: "))
+    if age > 18:
+        print("Hi")
+    else:
+        print("Hello!", end='')
+        print("What's your name?")
+    """).strip()))
+    
+    assert p == """/=Module
+    body=[...]
+        0=Assign @ 1.0
+            targets=[...]
+                0=Name @ 1.0
+                    id='age'
+                    ctx=Store
+            value=Call @ 1.6
+                func=Name @ 1.6
+                    id='int'
+                    ctx=Load
+                args=[...]
+                    0=Call @ 1.10
+                        func=Name @ 1.10
+                            id='input'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 1.16
+                                s='Enter age: '
+                        keywords=[]
+                keywords=[]
+        1=If @ 2.0
+            test=Compare @ 2.3
+                left=Name @ 2.3
+                    id='age'
+                    ctx=Load
+                ops=[...]
+                    0=Gt
+                comparators=[...]
+                    0=Num @ 2.9
+                        n=18
+            body=[...]
+                0=Expr @ 3.4
+                    value=Call @ 3.4
+                        func=Name @ 3.4
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 3.10
+                                s='Hi'
+                        keywords=[]
+            orelse=[...]
+                0=Expr @ 5.4
+                    value=Call @ 5.4
+                        func=Name @ 5.4
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 5.10
+                                s='Hello!'
+                        keywords=[...]
+                            0=keyword
+                                arg='end'
+                                value=Str @ 5.24
+                                    s=''
+                1=Expr @ 6.4
+                    value=Call @ 6.4
+                        func=Name @ 6.4
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 6.10
+                                s="What's your name?"
+                        keywords=[]"""
+
diff --git a/thonny/test/test_ast_utils_mark_text_ranges.py b/thonny/test/test_ast_utils_mark_text_ranges.py
new file mode 100644
index 0000000..61ec17f
--- /dev/null
+++ b/thonny/test/test_ast_utils_mark_text_ranges.py
@@ -0,0 +1,450 @@
+import ast
+from thonny.ast_utils import pretty 
+from textwrap import dedent
+from thonny import ast_utils
+
+def test_single_assignment():
+    check_marked_ast("x=1", """/=Module
+    body=[...]
+        0=Assign @ 1.0  -  1.3
+            targets=[...]
+                0=Name @ 1.0  -  1.1
+                    id='x'
+                    ctx=Store
+            value=Num @ 1.2  -  1.3
+                n=1""")
+
+
+def test_simple_io_program():
+    check_marked_ast("""age = int(input("Enter age: "))
+if age > 18:
+    print("Hi")
+else:
+    print("Hello!", end='')
+    print("What's your name?")
+""", 
+"""/=Module
+    body=[...]
+        0=Assign @ 1.0  -  1.31
+            targets=[...]
+                0=Name @ 1.0  -  1.3
+                    id='age'
+                    ctx=Store
+            value=Call @ 1.6  -  1.31
+                func=Name @ 1.6  -  1.9
+                    id='int'
+                    ctx=Load
+                args=[...]
+                    0=Call @ 1.10  -  1.30
+                        func=Name @ 1.10  -  1.15
+                            id='input'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 1.16  -  1.29
+                                s='Enter age: '
+                        keywords=[]
+                keywords=[]
+        1=If @ 2.0  -  6.30
+            test=Compare @ 2.3  -  2.11
+                left=Name @ 2.3  -  2.6
+                    id='age'
+                    ctx=Load
+                ops=[...]
+                    0=Gt
+                comparators=[...]
+                    0=Num @ 2.9  -  2.11
+                        n=18
+            body=[...]
+                0=Expr @ 3.4  -  3.15
+                    value=Call @ 3.4  -  3.15
+                        func=Name @ 3.4  -  3.9
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 3.10  -  3.14
+                                s='Hi'
+                        keywords=[]
+            orelse=[...]
+                0=Expr @ 5.4  -  5.27
+                    value=Call @ 5.4  -  5.27
+                        func=Name @ 5.4  -  5.9
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 5.10  -  5.18
+                                s='Hello!'
+                        keywords=[...]
+                            0=keyword
+                                arg='end'
+                                value=Str @ 5.24  -  5.26
+                                    s=''
+                1=Expr @ 6.4  -  6.30
+                    value=Call @ 6.4  -  6.30
+                        func=Name @ 6.4  -  6.9
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 6.10  -  6.29
+                                s="What's your name?"
+                        keywords=[]""")
+
+def test_two_trivial_defs():
+    check_marked_ast("""def f():
+    pass
+def f():
+    pass""", """/=Module
+    body=[...]
+        0=FunctionDef @ 1.0  -  2.8
+            name='f'
+            args=arguments
+                args=[]
+                vararg=None
+                kwonlyargs=[]
+                kw_defaults=[]
+                kwarg=None
+                defaults=[]
+            body=[...]
+                0=Pass @ 2.4  -  2.8
+            decorator_list=[]
+            returns=None
+        1=FunctionDef @ 3.0  -  4.8
+            name='f'
+            args=arguments
+                args=[]
+                vararg=None
+                kwonlyargs=[]
+                kw_defaults=[]
+                kwarg=None
+                defaults=[]
+            body=[...]
+                0=Pass @ 4.4  -  4.8
+            decorator_list=[]
+            returns=None""")
+
+def test_id_def():
+    check_marked_ast("""def f(x):
+    return x
+""", """/=Module
+    body=[...]
+        0=FunctionDef @ 1.0  -  2.12
+            name='f'
+            args=arguments
+                args=[...]
+                    0=arg @ 1.6  -  1.7
+                        arg='x'
+                        annotation=None
+                vararg=None
+                kwonlyargs=[]
+                kw_defaults=[]
+                kwarg=None
+                defaults=[]
+            body=[...]
+                0=Return @ 2.4  -  2.12
+                    value=Name @ 2.11  -  2.12
+                        id='x'
+                        ctx=Load
+            decorator_list=[]
+            returns=None""")
+
+def test_simple_while_program():
+    check_marked_ast("""x = int(input("Enter number: "))
+
+while x > 0:
+    print(x)
+    x -= 1
+""", """/=Module
+    body=[...]
+        0=Assign @ 1.0  -  1.32
+            targets=[...]
+                0=Name @ 1.0  -  1.1
+                    id='x'
+                    ctx=Store
+            value=Call @ 1.4  -  1.32
+                func=Name @ 1.4  -  1.7
+                    id='int'
+                    ctx=Load
+                args=[...]
+                    0=Call @ 1.8  -  1.31
+                        func=Name @ 1.8  -  1.13
+                            id='input'
+                            ctx=Load
+                        args=[...]
+                            0=Str @ 1.14  -  1.30
+                                s='Enter number: '
+                        keywords=[]
+                keywords=[]
+        1=While @ 3.0  -  5.10
+            test=Compare @ 3.6  -  3.11
+                left=Name @ 3.6  -  3.7
+                    id='x'
+                    ctx=Load
+                ops=[...]
+                    0=Gt
+                comparators=[...]
+                    0=Num @ 3.10  -  3.11
+                        n=0
+            body=[...]
+                0=Expr @ 4.4  -  4.12
+                    value=Call @ 4.4  -  4.12
+                        func=Name @ 4.4  -  4.9
+                            id='print'
+                            ctx=Load
+                        args=[...]
+                            0=Name @ 4.10  -  4.11
+                                id='x'
+                                ctx=Load
+                        keywords=[]
+                1=AugAssign @ 5.4  -  5.10
+                    target=Name @ 5.4  -  5.5
+                        id='x'
+                        ctx=Store
+                    op=Sub
+                    value=Num @ 5.9  -  5.10
+                        n=1
+            orelse=[]""")
+
+def test_call_with_pos_and_kw_arg():
+    check_marked_ast("""f(3, t=45)
+""", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.10
+            value=Call @ 1.0  -  1.10
+                func=Name @ 1.0  -  1.1
+                    id='f'
+                    ctx=Load
+                args=[...]
+                    0=Num @ 1.2  -  1.3
+                        n=3
+                keywords=[...]
+                    0=keyword
+                        arg='t'
+                        value=Num @ 1.7  -  1.9
+                            n=45""")
+
+def test_call_with_pos_star_kw():
+    check_marked_ast("""f(3, *kala, t=45)
+    """, 
+    """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.17
+            value=Call @ 1.0  -  1.17
+                func=Name @ 1.0  -  1.1
+                    id='f'
+                    ctx=Load
+                args=[...]
+                    0=Num @ 1.2  -  1.3
+                        n=3
+                    1=Starred @ 1.5  -  1.10
+                        value=Name @ 1.6  -  1.10
+                            id='kala'
+                            ctx=Load
+                        ctx=Load
+                keywords=[...]
+                    0=keyword
+                        arg='t'
+                        value=Num @ 1.14  -  1.16
+                            n=45""")
+
+def test_call_with_single_keyword():
+    check_marked_ast("""fff(t=45)
+""", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.9
+            value=Call @ 1.0  -  1.9
+                func=Name @ 1.0  -  1.3
+                    id='fff'
+                    ctx=Load
+                args=[]
+                keywords=[...]
+                    0=keyword
+                        arg='t'
+                        value=Num @ 1.6  -  1.8
+                            n=45""")
+
+def test_call_without_arguments():
+    check_marked_ast("""fff()
+""", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.5
+            value=Call @ 1.0  -  1.5
+                func=Name @ 1.0  -  1.3
+                    id='fff'
+                    ctx=Load
+                args=[]
+                keywords=[]""")
+
+def test_call_with_attribute_function_and_keyword_arg():
+    check_marked_ast("""rida.strip().split(maxsplit=1)
+""", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.30
+            value=Call @ 1.0  -  1.30
+                func=Attribute @ 1.0  -  1.18
+                    value=Call @ 1.0  -  1.12
+                        func=Attribute @ 1.0  -  1.10
+                            value=Name @ 1.0  -  1.4
+                                id='rida'
+                                ctx=Load
+                            attr='strip'
+                            ctx=Load
+                        args=[]
+                        keywords=[]
+                    attr='split'
+                    ctx=Load
+                args=[]
+                keywords=[...]
+                    0=keyword
+                        arg='maxsplit'
+                        value=Num @ 1.28  -  1.29
+                            n=1""")
+
+def test_del_from_list_with_integer():
+    check_marked_ast("""del x[0]""", """/=Module
+    body=[...]
+        0=Delete @ 1.0  -  1.8
+            targets=[...]
+                0=Subscript @ 1.4  -  1.8
+                    value=Name @ 1.4  -  1.5
+                        id='x'
+                        ctx=Load
+                    slice=Index
+                        value=Num @ 1.6  -  1.7
+                            n=0
+                    ctx=Del""")
+
+
+def test_del_from_list_with_string():
+    check_marked_ast("""del x["blah"]""", """/=Module
+    body=[...]
+        0=Delete @ 1.0  -  1.13
+            targets=[...]
+                0=Subscript @ 1.4  -  1.13
+                    value=Name @ 1.4  -  1.5
+                        id='x'
+                        ctx=Load
+                    slice=Index
+                        value=Str @ 1.6  -  1.12
+                            s='blah'
+                    ctx=Del""")
+
+def test_full_slice1():
+    check_marked_ast("""blah[:]
+""", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.7
+            value=Subscript @ 1.0  -  1.7
+                value=Name @ 1.0  -  1.4
+                    id='blah'
+                    ctx=Load
+                slice=Slice
+                    lower=None
+                    upper=None
+                    step=None
+                ctx=Load""")
+
+def test_full_slice2():
+    check_marked_ast("""blah[::]
+""", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.8
+            value=Subscript @ 1.0  -  1.8
+                value=Name @ 1.0  -  1.4
+                    id='blah'
+                    ctx=Load
+                slice=Slice
+                    lower=None
+                    upper=None
+                    step=None
+                ctx=Load""")
+
+def test_non_ascii_letters_with_calls_etc():
+    check_marked_ast("""täpitähed = "täpitähed"
+print(täpitähed["tšahh"])
+pöhh(pöhh=3)
+""", """/=Module
+    body=[...]
+        0=Assign @ 1.0  -  1.23
+            targets=[...]
+                0=Name @ 1.0  -  1.9
+                    id='täpitähed'
+                    ctx=Store
+            value=Str @ 1.12  -  1.23
+                s='täpitähed'
+        1=Expr @ 2.0  -  2.25
+            value=Call @ 2.0  -  2.25
+                func=Name @ 2.0  -  2.5
+                    id='print'
+                    ctx=Load
+                args=[...]
+                    0=Subscript @ 2.6  -  2.24
+                        value=Name @ 2.6  -  2.15
+                            id='täpitähed'
+                            ctx=Load
+                        slice=Index
+                            value=Str @ 2.16  -  2.23
+                                s='tšahh'
+                        ctx=Load
+                keywords=[]
+        2=Expr @ 3.0  -  3.12
+            value=Call @ 3.0  -  3.12
+                func=Name @ 3.0  -  3.4
+                    id='pöhh'
+                    ctx=Load
+                args=[]
+                keywords=[...]
+                    0=keyword
+                        arg='pöhh'
+                        value=Num @ 3.10  -  3.11
+                            n=3""")
+
+def test_nested_binops():
+    """http://bugs.python.org/issue18374"""
+    check_marked_ast("1+2-3", """/=Module
+    body=[...]
+        0=Expr @ 1.0  -  1.5
+            value=BinOp @ 1.0  -  1.5
+                left=BinOp @ 1.0  -  1.3
+                    left=Num @ 1.0  -  1.1
+                        n=1
+                    op=Add
+                    right=Num @ 1.2  -  1.3
+                        n=2
+                op=Sub
+                right=Num @ 1.4  -  1.5
+                    n=3""")
+
+def test_multiline_string():
+    """http://bugs.python.org/issue18370"""
+    check_marked_ast("""pass
+blah = \"\"\"first
+second
+third\"\"\"
+pass""",
+    """/=Module
+    body=[...]
+        0=Pass @ 1.0  -  1.4
+        1=Assign @ 2.0  -  4.8
+            targets=[...]
+                0=Name @ 2.0  -  2.4
+                    id='blah'
+                    ctx=Store
+            value=Str @ 2.7  -  4.8
+                s='first\\nsecond\\nthird'
+        2=Pass @ 5.0  -  5.4""")
+
+def check_marked_ast(source, expected_pretty_ast
+                     #,expected_for_py_34=None
+                     ):
+    
+    #if (sys.version_info[:2] == (3,4) 
+    #    and expected_for_py_34 is not None):
+    #    expected_pretty_ast = expected_for_py_34
+        
+    source = dedent(source)
+    root = ast.parse(source)
+    ast_utils.mark_text_ranges(root, source)
+    actual_pretty_ast = pretty(root)
+    #print("ACTUAL", actual_pretty_ast)
+    #print("EXPECTED", expected_pretty_ast)
+    assert actual_pretty_ast.strip() == expected_pretty_ast.strip() 
+    
diff --git a/thonny/tktextext.py b/thonny/tktextext.py
new file mode 100644
index 0000000..1f1db8d
--- /dev/null
+++ b/thonny/tktextext.py
@@ -0,0 +1,832 @@
+# coding=utf-8
+"""Extensions for tk.Text"""
+
+import time
+import traceback
+from logging import exception
+
+try:
+    import tkinter as tk
+    from tkinter import ttk
+    from tkinter import font as tkfont
+    from tkinter import TclError
+except ImportError:
+    import Tkinter as tk
+    import ttk
+    import tkFont as tkfont
+    from Tkinter import TclError
+    
+
+class TweakableText(tk.Text):
+    """Allows intercepting Text commands at Tcl-level"""
+    def __init__(self, master=None, cnf={}, read_only=False, **kw):
+        tk.Text.__init__(self, master=master, cnf=cnf, **kw)
+        
+        self._read_only = read_only
+        
+        self._original_widget_name = self._w + "_orig"
+        self.tk.call("rename", self._w, self._original_widget_name)
+        self.tk.createcommand(self._w, self._dispatch_tk_operation)
+        self._tk_proxies = {}
+        
+        self._original_insert = self._register_tk_proxy_function("insert", self.intercept_insert)
+        self._original_delete = self._register_tk_proxy_function("delete", self.intercept_delete)
+        self._original_mark = self._register_tk_proxy_function("mark", self.intercept_mark)
+    
+    def _register_tk_proxy_function(self, operation, function):
+        self._tk_proxies[operation] = function
+        setattr(self, operation, function)
+        
+        def original_function(*args):
+            self.tk.call((self._original_widget_name, operation) + args)
+            
+        return original_function
+    
+    def _dispatch_tk_operation(self, operation, *args):
+        f = self._tk_proxies.get(operation)
+        try:
+            if f:
+                return f(*args)
+            else:
+                return self.tk.call((self._original_widget_name, operation) + args)
+            
+        except TclError as e:
+            # Some Tk internal actions (eg. paste and cut) can cause this error
+            if (str(e).lower() == '''text doesn't contain any characters tagged with "sel"'''
+                and operation in ["delete", "index", "get"] 
+                and args in [("sel.first", "sel.last"), ("sel.first",)]):
+                
+                pass 
+            else:
+                traceback.print_exc()
+            
+            return "" # Taken from idlelib.WidgetRedirector
+    
+    def set_read_only(self, value):
+        self._read_only = value
+    
+    def is_read_only(self):
+        return self._read_only
+
+    def set_content(self, chars):
+        self.direct_delete("1.0", tk.END)
+        self.direct_insert("1.0", chars)
+        
+    def intercept_mark(self, *args):
+        self.direct_mark(*args)
+    
+    def intercept_insert(self, index, chars, tags=None):
+        assert isinstance(chars, str)
+        if chars >= "\uf704" and chars <= "\uf70d": # Function keys F1..F10 in Mac cause these
+            pass
+        elif self.is_read_only():
+            self.bell()
+        else:
+            self.direct_insert(index, chars, tags)
+    
+    def intercept_delete(self, index1, index2=None):
+        if index1 == "sel.first" and index2 == "sel.last" and not self.has_selection():
+            return
+        
+        if self.is_read_only():
+            self.bell()            
+        elif self._is_erroneous_delete(index1, index2):
+            pass
+        else:
+            self.direct_delete(index1, index2)
+    
+    def _is_erroneous_delete(self, index1, index2):
+        """Paste can cause deletes where index1 is sel.start but text has no selection. This would cause errors"""
+        return index1.startswith("sel.") and not self.has_selection()
+    
+    def direct_mark(self, *args):
+        self._original_mark(*args)
+        
+        if args[:2] == ('set', 'insert'):
+            self.event_generate("<<CursorMove>>")
+    
+    def index_sel_first(self):
+        # Tk will give error without this check
+        if self.tag_ranges("sel"):
+            return self.index("sel.first")
+        else:
+            return None
+    
+    def index_sel_last(self):
+        if self.tag_ranges("sel"):
+            return self.index("sel.last")
+        else:
+            return None
+
+    def has_selection(self):
+        return len(self.tag_ranges("sel")) > 0
+    
+    def get_selection_indices(self):
+        # If a selection is defined in the text widget, return (start,
+        # end) as Tkinter text indices, otherwise return (None, None)
+        if self.has_selection():
+            return self.index("sel.first"), self.index("sel.last")
+        else:
+            return None, None
+        
+    def direct_insert(self, index, chars, tags=None):
+        self._original_insert(index, chars, tags)
+        self.event_generate("<<TextChange>>")
+    
+    def direct_delete(self, index1, index2=None):
+        self._original_delete(index1, index2)
+        self.event_generate("<<TextChange>>")
+    
+
+
+class EnhancedText(TweakableText):
+    """Text widget with extra navigation and editing aids. 
+    Provides more comfortable deletion, indentation and deindentation,
+    and undo handling. Not specific to Python code.
+    
+    Most of the code is adapted from idlelib.EditorWindow.
+    """ 
+    def __init__(self, master=None, cnf={}, **kw):
+        # Parent class shouldn't autoseparate
+        # TODO: take client provided autoseparators value into account 
+        kw["autoseparators"] = False
+        
+        
+        TweakableText.__init__(self, master=master, cnf=cnf, **kw)
+        self.tabwidth = 8 # See comments in idlelib.EditorWindow 
+        self.indentwidth = 4 
+        self.usetabs = False
+        
+        self._last_event_kind = None
+        self._last_key_time = None
+        
+        self._bind_editing_aids()
+        self._bind_movement_aids()
+        self._bind_selection_aids()
+        self._bind_undo_aids()
+        self._bind_mouse_aids()
+    
+    def _bind_mouse_aids(self):
+        if _running_on_mac():
+            self.bind("<Button-2>", self.on_secondary_click)
+            self.bind("<Control-Button-1>", self.on_secondary_click)
+        else:  
+            self.bind("<Button-3>", self.on_secondary_click)
+        
+    
+    def _bind_editing_aids(self):
+        
+        def if_not_readonly(fun):
+            def dispatch(event):
+                if not self.is_read_only():
+                    return fun(event)
+                else:
+                    return "break"
+            return dispatch
+        
+        self.bind("<Control-BackSpace>", if_not_readonly(self.delete_word_left), True)
+        self.bind("<Control-Delete>", if_not_readonly(self.delete_word_right), True)
+        self.bind("<BackSpace>", if_not_readonly(self.perform_smart_backspace), True)
+        self.bind("<Return>", if_not_readonly(self.perform_return), True)
+        self.bind("<KP_Enter>", if_not_readonly(self.perform_return), True)
+        self.bind("<Tab>", if_not_readonly(self.perform_tab), True)
+        try:
+            # Is needed on eg. Ubuntu with Estonian keyboard
+            self.bind("<ISO_Left_Tab>", if_not_readonly(self.perform_tab), True)
+        except:
+            pass
+    
+    def _bind_movement_aids(self):
+        self.bind("<Home>", self.perform_smart_home, True)
+        self.bind("<Left>", self.move_to_edge_if_selection(0), True)
+        self.bind("<Right>", self.move_to_edge_if_selection(1), True)
+        self.bind("<Next>", self.perform_page_down, True)
+        self.bind("<Prior>", self.perform_page_up, True)
+    
+    def _bind_selection_aids(self):
+        self.bind("<Command-a>" if _running_on_mac() else "<Control-a>",
+                  self.select_all, True)
+    
+    def _bind_undo_aids(self):
+        self.bind("<<Undo>>", self._on_undo, True)
+        self.bind("<<Redo>>", self._on_redo, True)
+        self.bind("<<Cut>>", self._on_cut, True)
+        self.bind("<<Copy>>", self._on_copy, True)
+        self.bind("<<Paste>>", self._on_paste, True)
+        self.bind("<FocusIn>", self._on_get_focus, True)
+        self.bind("<FocusOut>", self._on_lose_focus, True)
+        self.bind("<Key>", self._on_key_press, True)
+        self.bind("<1>", self._on_mouse_click, True)
+        self.bind("<2>", self._on_mouse_click, True)
+        self.bind("<3>", self._on_mouse_click, True)
+        
+    
+    def delete_word_left(self, event):
+        self.event_generate('<Meta-Delete>')
+        self.edit_separator()
+        return "break"
+
+    def delete_word_right(self, event):
+        self.event_generate('<Meta-d>')
+        self.edit_separator()
+        return "break"
+
+    def perform_smart_backspace(self, event):
+        self._log_keypress_for_undo(event)
+        
+        text = self
+        first, last = self.get_selection_indices()
+        if first and last:
+            text.delete(first, last)
+            text.mark_set("insert", first)
+            return "break"
+        # Delete whitespace left, until hitting a real char or closest
+        # preceding virtual tab stop.
+        chars = text.get("insert linestart", "insert")
+        if chars == '':
+            if text.compare("insert", ">", "1.0"):
+                # easy: delete preceding newline
+                text.delete("insert-1c")
+            else:
+                text.bell()     # at start of buffer
+            return "break"
+        
+        if chars.strip() != "": # there are non-whitespace chars somewhere to the left of the cursor
+            # easy: delete preceding real char
+            text.delete("insert-1c")
+            self._log_keypress_for_undo(event)
+            return "break"
+        
+        # Ick.  It may require *inserting* spaces if we back up over a
+        # tab character!  This is written to be clear, not fast.
+        tabwidth = self.tabwidth
+        have = len(chars.expandtabs(tabwidth))
+        assert have > 0
+        want = ((have - 1) // self.indentwidth) * self.indentwidth
+        # Debug prompt is multilined....
+        #if self.context_use_ps1:
+        #    last_line_of_prompt = sys.ps1.split('\n')[-1]
+        #else:
+        last_line_of_prompt = ''
+        ncharsdeleted = 0
+        while 1:
+            if chars == last_line_of_prompt:
+                break
+            chars = chars[:-1]
+            ncharsdeleted = ncharsdeleted + 1
+            have = len(chars.expandtabs(tabwidth))
+            if have <= want or chars[-1] not in " \t":
+                break
+        text.delete("insert-%dc" % ncharsdeleted, "insert")
+        if have < want:
+            text.insert("insert", ' ' * (want - have))
+        return "break"
+
+    def perform_midline_tab(self, event=None):
+        "autocompleter can put its magic here"
+        # by default
+        return self.perform_smart_tab(event)
+    
+    def perform_smart_tab(self, event=None):
+        self._log_keypress_for_undo(event)
+        
+        # if intraline selection:
+        #     delete it
+        # elif multiline selection:
+        #     do indent-region
+        # else:
+        #     indent one level
+        
+        first, last = self.get_selection_indices()
+        if first and last:
+            if index2line(first) != index2line(last):
+                return self.indent_region(event)
+            self.delete(first, last)
+            self.mark_set("insert", first)
+        prefix = self.get("insert linestart", "insert")
+        raw, effective = classifyws(prefix, self.tabwidth)
+        if raw == len(prefix):
+            # only whitespace to the left
+            self._reindent_to(effective + self.indentwidth)
+        else:
+            # tab to the next 'stop' within or to right of line's text:
+            if self.usetabs:
+                pad = '\t'
+            else:
+                effective = len(prefix.expandtabs(self.tabwidth))
+                n = self.indentwidth
+                pad = ' ' * (n - effective % n)
+            self.insert("insert", pad)
+        self.see("insert")
+        return "break"
+
+    def get_cursor_position(self):
+        return map(int, self.index("insert").split("."))
+    
+    def get_line_count(self):
+        return list(map(int, self.index("end-1c").split(".")))[0]
+
+    def perform_return(self, event):
+        self.insert("insert", "\n")
+        return "break"
+    
+    def perform_page_down(self, event):
+        # if last line is visible then go to last line 
+        # (by default it doesn't move then)
+        try:
+            last_visible_idx = self.index("@0,%d" % self.winfo_height())
+            row, _ = map(int, last_visible_idx.split("."))
+            line_count = self.get_line_count()
+            
+            if (row == line_count 
+                or row == line_count-1): # otherwise tk doesn't show last line
+                self.mark_set("insert", "end")
+        except:
+            traceback.print_exc() 
+    
+    def perform_page_up(self, event):
+        # if first line is visible then go there 
+        # (by default it doesn't move then)    
+        try:
+            first_visible_idx = self.index("@0,0")
+            row, _ = map(int, first_visible_idx.split("."))
+            if row == 1:
+                self.mark_set("insert", "1.0")
+        except:
+            traceback.print_exc() 
+    
+    def compute_smart_home_destination_index(self):
+        """Is overridden in shell"""
+        
+        line = self.get("insert linestart", "insert lineend")
+        for insertpt in range(len(line)):
+            if line[insertpt] not in (' ','\t'):
+                break
+        else:
+            insertpt=len(line)
+            
+        lineat = int(self.index("insert").split('.')[1])
+        if insertpt == lineat:
+            insertpt = 0
+        return "insert linestart+"+str(insertpt)+"c"
+    
+    def perform_smart_home(self, event):
+        if (event.state & 4) != 0 and event.keysym == "Home":
+            # state&4==Control. If <Control-Home>, use the Tk binding.
+            return
+        
+        dest = self.compute_smart_home_destination_index()
+        
+        if (event.state&1) == 0:
+            # shift was not pressed
+            self.tag_remove("sel", "1.0", "end")
+        else:
+            if not self.index_sel_first():
+                # there was no previous selection
+                self.mark_set("my_anchor", "insert")
+            else:
+                if self.compare(self.index_sel_first(), "<",
+                                     self.index("insert")):
+                    self.mark_set("my_anchor", "sel.first") # extend back
+                else:
+                    self.mark_set("my_anchor", "sel.last") # extend forward
+            first = self.index(dest)
+            last = self.index("my_anchor")
+            if self.compare(first,">",last):
+                first,last = last,first
+            self.tag_remove("sel", "1.0", "end")
+            self.tag_add("sel", first, last)
+        self.mark_set("insert", dest)
+        self.see("insert")
+        return "break"
+
+    def move_to_edge_if_selection(self, edge_index):
+        """Cursor move begins at start or end of selection
+
+        When a left/right cursor key is pressed create and return to Tkinter a
+        function which causes a cursor move from the associated edge of the
+        selection.
+        """
+        def move_at_edge(event):
+            if (self.has_selection() 
+                and (event.state & 5) == 0): # no shift(==1) or control(==4) pressed
+                try:
+                    self.mark_set("insert", ("sel.first+1c", "sel.last-1c")[edge_index])
+                except tk.TclError:
+                    pass
+                
+        return move_at_edge
+    
+    def perform_tab(self, event=None):
+        self._log_keypress_for_undo(event)
+        if event.state & 0x0001: # shift is pressed (http://stackoverflow.com/q/32426250/261181)
+            return self.dedent_region(event)
+        else:
+            # check whether there are letters before cursor on this line
+            index = self.index("insert")
+            left_text = self.get(index + " linestart", index)
+            if left_text.strip() == "" or self.has_selection():
+                return self.perform_smart_tab(event)    
+            else:
+                return self.perform_midline_tab(event)
+    
+    def indent_region(self, event=None):
+        return self._change_indentation(True)
+
+    def dedent_region(self, event=None):
+        return self._change_indentation(False)
+    
+    def _change_indentation(self, increase=True):
+        head, tail, chars, lines = self._get_region()
+        
+        # Text widget plays tricks if selection ends on last line
+        # and content doesn't end with empty line,
+        text_last_line = index2line(self.index("end-1c"))
+        sel_last_line = index2line(tail)
+        if sel_last_line >= text_last_line:
+            while not self.get(head, "end").endswith("\n\n"):
+                self.insert("end", "\n")
+        
+        for pos in range(len(lines)):
+            line = lines[pos]
+            if line:
+                raw, effective = classifyws(line, self.tabwidth)
+                if increase:
+                    effective = effective + self.indentwidth
+                else:
+                    effective = max(effective - self.indentwidth, 0)
+                lines[pos] = self._make_blanks(effective) + line[raw:]
+        self._set_region(head, tail, chars, lines)
+        return "break"
+    
+    
+    def select_all(self, event):
+        self.tag_remove("sel", "1.0", tk.END)
+        self.tag_add('sel', '1.0', tk.END)
+    
+    def _reindent_to(self, column):
+        # Delete from beginning of line to insert point, then reinsert
+        # column logical (meaning use tabs if appropriate) spaces.
+        if self.compare("insert linestart", "!=", "insert"):
+            self.delete("insert linestart", "insert")
+        if column:
+            self.insert("insert", self._make_blanks(column))
+        
+    def _get_region(self):
+        first, last = self.get_selection_indices()
+        if first and last:
+            head = self.index(first + " linestart")
+            tail = self.index(last + "-1c lineend +1c")
+        else:
+            head = self.index("insert linestart")
+            tail = self.index("insert lineend +1c")
+        chars = self.get(head, tail)
+        lines = chars.split("\n")
+        return head, tail, chars, lines
+
+    def _set_region(self, head, tail, chars, lines):
+        newchars = "\n".join(lines)
+        if newchars == chars:
+            self.bell()
+            return
+        self.tag_remove("sel", "1.0", "end")
+        self.mark_set("insert", head)
+        self.delete(head, tail)
+        self.insert(head, newchars)
+        self.tag_add("sel", head, "insert")
+    
+    def _log_keypress_for_undo(self, e):
+        if e is None:
+            return
+        
+        # NB! this may not execute if the event is cancelled in another handler
+        event_kind = self._get_event_kind(e)
+        
+        if (event_kind != self._last_event_kind
+            or e.char in ("\r", "\n", " ", "\t")
+            or e.keysym in ["Return", "KP_Enter"]
+            or time.time() - self.last_key_time > 2
+            ):
+            self.edit_separator()
+            
+        self._last_event_kind = event_kind
+        self.last_key_time = time.time()
+
+    def _get_event_kind(self, event):
+        if event.keysym in ("BackSpace", "Delete"):
+            return "delete"
+        elif event.char:
+            return "insert"
+        else:
+            # eg. e.keysym in ("Left", "Up", "Right", "Down", "Home", "End", "Prior", "Next"):
+            return "other_key"
+
+    def _make_blanks(self, n):
+        # Make string that displays as n leading blanks.
+        if self.usetabs:
+            ntabs, nspaces = divmod(n, self.tabwidth)
+            return '\t' * ntabs + ' ' * nspaces
+        else:
+            return ' ' * n
+
+    def _on_undo(self, e):
+        self._last_event_kind = "undo"
+        
+    def _on_redo(self, e):
+        self._last_event_kind = "redo"
+        
+    def _on_cut(self, e):
+        self._last_event_kind = "cut"
+        self.edit_separator()        
+        
+    def _on_copy(self, e):
+        self._last_event_kind = "copy"
+        self.edit_separator()        
+        
+    def _on_paste(self, e):
+        self._last_event_kind = "paste"
+        self.edit_separator()    
+        self.see("insert")  
+        self.after_idle(lambda : self.see("insert"))  
+    
+    def _on_get_focus(self, e):
+        self._last_event_kind = "get_focus"
+        self.edit_separator()        
+        
+    def _on_lose_focus(self, e):
+        self._last_event_kind = "lose_focus"
+        self.edit_separator()        
+    
+    def _on_key_press(self, e):
+        return self._log_keypress_for_undo(e)
+
+    def _on_mouse_click(self, event):
+        self.edit_separator()
+
+    
+    def on_secondary_click(self, event=None):
+        "Use this for invoking context menu"
+        self.focus_set()
+
+
+class TextFrame(ttk.Frame):
+    "Decorates text with scrollbars, line numbers and print margin"
+    def __init__(self, master, line_numbers=False, line_length_margin=0,
+                 first_line_number=1, text_class=EnhancedText,
+                 horizontal_scrollbar=True, vertical_scrollbar=True,
+                 vertical_scrollbar_class=ttk.Scrollbar,
+                 horizontal_scrollbar_class=ttk.Scrollbar,
+                 **text_options):
+        ttk.Frame.__init__(self, master=master)
+        
+        final_text_options = {'borderwidth' : 0,
+                              'insertwidth' : 2,
+                              'spacing1' : 0,
+                              'spacing3' : 0,
+                              'highlightthickness' : 0,
+                              'inactiveselectbackground' : 'gray',
+                              'padx' : 5,
+                              'pady' : 5
+                               }
+        final_text_options.update(text_options)
+        self.text = text_class(self, **final_text_options)
+        self.text.grid(row=0, column=1, sticky=tk.NSEW)
+
+        self._margin = tk.Text(self, width=4, padx=5, pady=5,
+                               highlightthickness=0, bd=0, takefocus=False,
+                               font=self.text['font'],
+                               background='#e0e0e0', foreground='#999999',
+                               selectbackground='#e0e0e0', selectforeground='#999999',
+                               cursor='arrow',
+                               state='disabled',
+                               undo=False
+                               )
+        self._margin.bind("<ButtonRelease-1>", self.on_margin_click)
+        self._margin.bind("<Button-1>", self.on_margin_click)
+        self._margin.bind("<Button1-Motion>", self.on_margin_motion)
+        self._margin['yscrollcommand'] = self._margin_scroll
+        
+        # margin will be gridded later
+        self._first_line_number = first_line_number
+        self.set_line_numbers(line_numbers)
+        
+        if vertical_scrollbar:
+            self._vbar = vertical_scrollbar_class(self, orient=tk.VERTICAL)
+            self._vbar.grid(row=0, column=2, sticky=tk.NSEW)
+            self._vbar['command'] = self._vertical_scroll 
+            self.text['yscrollcommand'] = self._vertical_scrollbar_update  
+        
+        if horizontal_scrollbar:
+            self._hbar = horizontal_scrollbar_class(self, orient=tk.HORIZONTAL)
+            self._hbar.grid(row=1, column=0, sticky=tk.NSEW, columnspan=2)
+            self._hbar['command'] = self._horizontal_scroll
+            self.text['xscrollcommand'] = self._horizontal_scrollbar_update    
+        
+        self.columnconfigure(1, weight=1)
+        self.rowconfigure(0, weight=1)
+
+        self._recommended_line_length=line_length_margin
+        self._margin_line = tk.Canvas(self.text, borderwidth=0, width=1, height=1200, 
+                                     highlightthickness=0, background="lightgray")
+        self.update_margin_line()
+        
+        self.text.bind("<<TextChange>>", self._text_changed, True)
+        
+        # TODO: add context menu?
+
+    def focus_set(self):
+        self.text.focus_set()
+    
+    def set_line_numbers(self, value):
+        if value and not self._margin.winfo_ismapped():
+            self._margin.grid(row=0, column=0, sticky=tk.NSEW)
+            self.update_line_numbers()
+        elif not value and self._margin.winfo_ismapped():
+            self._margin.grid_forget()
+        
+        # insert first line number (NB! Without trailing linebreak. See update_line_numbers) 
+        self._margin.config(state='normal')
+        self._margin.delete("1.0", "end")
+        self._margin.insert("1.0", str(self._first_line_number))
+        self._margin.config(state='disabled')
+
+        self.update_line_numbers()
+    
+    def set_line_length_margin(self, value):
+        self._recommended_line_length = value
+        self.update_margin_line()
+    
+    def _text_changed(self, event):
+        self.update_line_numbers()
+        self.update_margin_line()
+    
+    def _vertical_scrollbar_update(self, *args):
+        self._vbar.set(*args)
+        self._margin.yview(tk.MOVETO, args[0])
+        
+    def _margin_scroll(self, *args):
+        # FIXME: this doesn't work properly
+        # Can't scroll to bottom when line numbers are not visible
+        # and can't type normally at the bottom, when line numbers are visible 
+        return
+        #self._vbar.set(*args)
+        #self.text.yview(tk.MOVETO, args[0])
+        
+    def _horizontal_scrollbar_update(self,*args):
+        self._hbar.set(*args)
+        self.update_margin_line()
+    
+    def _vertical_scroll(self,*args):
+        self.text.yview(*args)
+        self._margin.yview(*args)
+        
+    def _horizontal_scroll(self,*args):
+        self.text.xview(*args)
+        self.update_margin_line()
+    
+    def update_line_numbers(self):
+        text_line_count = int(self.text.index("end").split(".")[0])
+        margin_line_count = int(self._margin.index("end").split(".")[0])
+        
+        if text_line_count != margin_line_count:
+            self._margin.config(state='normal')
+            
+            # NB! Text acts weird with last symbol 
+            # (don't really understand whether it automatically keeps a newline there or not)
+            # Following seems to ensure both Text-s have same height
+            if text_line_count > margin_line_count:
+                delta = text_line_count - margin_line_count
+                start = margin_line_count + self._first_line_number - 1
+                for i in range(start, start + delta):
+                    self._margin.insert("end-1c", "\n" + str(i))
+            
+            else:
+                self._margin.delete(line2index(text_line_count)+"-1c", "end-1c")
+                
+            self._margin.config(state='disabled')
+        
+        # synchronize margin scroll position with text
+        # https://mail.python.org/pipermail/tkinter-discuss/2010-March/002197.html
+        first, _ = self.text.yview()
+        self._margin.yview_moveto(first)
+
+
+    def update_margin_line(self):
+        if self._recommended_line_length == 0:
+            self._margin_line.place_forget()
+        else:
+            try:
+                self.text.update_idletasks()
+                # How far left has text been scrolled
+                first_visible_idx = self.text.index("@0,0")
+                first_visible_col = int(first_visible_idx.split(".")[1])
+                bbox = self.text.bbox(first_visible_idx)
+                first_visible_col_x = bbox[0]
+                
+                margin_line_visible_col = self._recommended_line_length - first_visible_col
+                delta = first_visible_col_x
+            except:
+                # fall back to ignoring scroll position
+                margin_line_visible_col = self._recommended_line_length
+                delta = 0
+            
+            if margin_line_visible_col > -1:
+                x = (get_text_font(self.text).measure((margin_line_visible_col-1) * "M") 
+                     + delta + self.text["padx"])
+            else:
+                x = -10
+            
+            #print(first_visible_col, first_visible_col_x)
+            
+            self._margin_line.place(y=-10, x=x)
+
+    def on_margin_click(self, event=None):
+        try:
+            linepos = self._margin.index("@%s,%s" % (event.x, event.y)).split(".")[0]
+            self.text.mark_set("insert", "%s.0" % linepos)
+            self._margin.mark_set("margin_selection_start", "%s.0" % linepos)
+            if event.type == "4": # In Python 3.6 you can use tk.EventType.ButtonPress instead of "4" 
+                self.text.tag_remove("sel", "1.0", "end")
+        except tk.TclError:
+            exception()
+
+    def on_margin_motion(self, event=None):
+        try:
+            linepos = int(self._margin.index("@%s,%s" % (event.x, event.y)).split(".")[0])
+            margin_selection_start = int(self._margin.index("margin_selection_start").split(".")[0])
+            self.select_lines(min(margin_selection_start, linepos), max(margin_selection_start - 1, linepos - 1))
+            self.text.mark_set("insert", "%s.0" % linepos)
+        except tk.TclError:
+            exception()
+        
+def get_text_font(text):
+    font = text["font"]
+    if isinstance(font, str):
+        return tkfont.nametofont(font)
+    else:
+        return font
+
+
+def classifyws(s, tabwidth):
+    raw = effective = 0
+    for ch in s:
+        if ch == ' ':
+            raw = raw + 1
+            effective = effective + 1
+        elif ch == '\t':
+            raw = raw + 1
+            effective = (effective // tabwidth + 1) * tabwidth
+        else:
+            break
+    return raw, effective
+
+def index2line(index):
+    return int(float(index))
+
+def line2index(line):
+    return str(float(line))
+
+def fixwordbreaks(root):
+    # Adapted from idlelib.EditorWindow (Python 3.4.2)
+    # Modified to include non-ascii chars
+    
+    # Make sure that Tk's double-click and next/previous word
+    # operations use our definition of a word (i.e. an identifier)
+    tk = root.tk
+    tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
+    tk.call('set', 'tcl_wordchars',     u'[a-zA-Z0-9_À-ÖØ-öø-ÿĀ-ſƀ-ɏА-я]')
+    tk.call('set', 'tcl_nonwordchars', u'[^a-zA-Z0-9_À-ÖØ-öø-ÿĀ-ſƀ-ɏА-я]')
+
+def rebind_control_a(root):
+    # Tk 8.6 has <<SelectAll>> event but 8.5 doesn't
+    # http://stackoverflow.com/questions/22907200/remap-default-keybinding-in-tkinter
+    def control_a(event):
+        widget = event.widget
+        if isinstance(widget, tk.Text):
+            widget.tag_remove("sel","1.0","end")
+            widget.tag_add("sel","1.0","end")
+        
+    root.bind_class("Text", "<Control-a>", control_a)
+    
+
+def _running_on_mac():
+    return tk._default_root.call('tk', 'windowingsystem') == "aqua"
+
+if __name__ == "__main__":
+    # demo
+    root = tk.Tk()
+    frame = TextFrame(root, read_only=False, wrap=tk.NONE,
+                      line_numbers=True, line_length_margin=13,
+                      text_class=TweakableText)
+    frame.grid()
+    text = frame.text
+    
+    text.direct_insert("1.0", "Essa\n    'tessa\nkossa\nx=34+(45*89*(a+45)")
+    text.tag_configure('string', background='yellow')
+    text.tag_add("string", "2.0", "3.0")
+    
+    
+    text.tag_configure('paren', underline=True)
+    text.tag_add("paren", "4.6", "5.0")
+    
+    root.mainloop()
\ No newline at end of file
diff --git a/thonny/token_utils.py b/thonny/token_utils.py
new file mode 100644
index 0000000..0d7eb4d
--- /dev/null
+++ b/thonny/token_utils.py
@@ -0,0 +1,35 @@
+import keyword
+import builtins
+
+
+def matches_any(name, alternates):
+    "Return a named group pattern matching list of alternates."
+    return "(?P<%s>" % name + "|".join(alternates) + ")"
+
+KW = r"\b" + matches_any("KEYWORD", keyword.kwlist) + r"\b"
+_builtinlist = [str(name) for name in dir(builtins)
+                                    if not name.startswith('_') and \
+                                    name not in keyword.kwlist]
+
+# TODO: move builtin handling to global-local
+BUILTIN = r"([^.'\"\\#]\b|^)" + matches_any("BUILTIN", _builtinlist) + r"\b"
+COMMENT = matches_any("COMMENT", [r"#[^\n]*"])
+MAGIC_COMMAND = matches_any("MAGIC_COMMAND", [r"^%[^\n]*"]) # used only in shell
+STRINGPREFIX = r"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR|rb|rB|Rb|RB)?"
+
+SQSTRING_OPEN = STRINGPREFIX + r"'[^'\\\n]*(\\.[^'\\\n]*)*\n?"
+SQSTRING_CLOSED = STRINGPREFIX + r"'[^'\\\n]*(\\.[^'\\\n]*)*'"
+
+DQSTRING_OPEN = STRINGPREFIX + r'"[^"\\\n]*(\\.[^"\\\n]*)*\n?'
+DQSTRING_CLOSED = STRINGPREFIX + r'"[^"\\\n]*(\\.[^"\\\n]*)*"'
+
+SQ3STRING = STRINGPREFIX + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
+DQ3STRING = STRINGPREFIX + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
+
+SQ3DELIMITER = STRINGPREFIX + "'''"
+DQ3DELIMITER = STRINGPREFIX + '"""'
+
+STRING_OPEN = matches_any("STRING_OPEN", [SQSTRING_OPEN, DQSTRING_OPEN])
+STRING_CLOSED = matches_any("STRING_CLOSED", [SQSTRING_CLOSED, DQSTRING_CLOSED])
+STRING3_DELIMITER = matches_any("DELIMITER3", [SQ3DELIMITER, DQ3DELIMITER])
+STRING3 = matches_any("STRING3", [DQ3STRING, SQ3STRING])
diff --git a/thonny/ui_utils.py b/thonny/ui_utils.py
new file mode 100644
index 0000000..f7a4a48
--- /dev/null
+++ b/thonny/ui_utils.py
@@ -0,0 +1,989 @@
+# -*- coding: utf-8 -*-
+
+from tkinter import ttk, messagebox
+from tkinter.dialog import Dialog
+
+from thonny import tktextext, misc_utils
+from thonny.globals import get_workbench
+from thonny.misc_utils import running_on_mac_os, running_on_windows, running_on_linux
+import tkinter as tk
+import tkinter.messagebox as tkMessageBox
+import traceback
+
+import textwrap
+import re
+import collections
+import threading
+import signal
+import subprocess
+import os
+
+
+CALM_WHITE = '#fdfdfd'
+
+_images = set() # for keeping references to tkinter images to avoid garbace colleting them
+
+class AutomaticPanedWindow(tk.PanedWindow):
+    """
+    Enables inserting panes according to their position_key-s.
+    Automatically adds/removes itself to/from its master AutomaticPanedWindow.
+    Fixes some style glitches.
+    """ 
+    def __init__(self, master, position_key=None,
+                first_pane_size=1/3, last_pane_size=1/3, **kwargs):
+        if not "sashwidth" in kwargs:
+            kwargs["sashwidth"]=10
+        
+        if not "background" in kwargs:
+            kwargs["background"] = get_main_background()
+        
+        tk.PanedWindow.__init__(self, master, **kwargs)
+        
+        self.position_key = position_key
+        self.visible_panes = set()
+        self.first_pane_size = first_pane_size
+        self.last_pane_size = last_pane_size
+        self._restoring_pane_sizes = False
+        
+        self._last_window_size = (0,0)
+        self._full_size_not_final = True
+        self._configure_binding = self.winfo_toplevel().bind("<Configure>", self._on_window_resize, True)
+        self.bind("<B1-Motion>", self._on_mouse_dragged, True)
+    
+    def insert(self, pos, child, **kw):
+        if pos == "auto":
+            # According to documentation I should use self.panes()
+            # but this doesn't return expected widgets
+            for sibling in sorted(self.visible_panes, 
+                                  key=lambda p:p.position_key 
+                                        if hasattr(p, "position_key")
+                                        else 0):
+                if (not hasattr(sibling, "position_key") 
+                    or sibling.position_key == None
+                    or sibling.position_key > child.position_key):
+                    pos = sibling
+                    break
+            else:
+                pos = "end"
+            
+        if isinstance(pos, tk.Widget):
+            kw["before"] = pos
+        self.add(child, **kw)
+
+    def add(self, child, **kw):
+        if not "minsize" in kw:
+            kw["minsize"]=60
+            
+        tk.PanedWindow.add(self, child, **kw)
+        self.visible_panes.add(child)
+        self._update_visibility()
+        self._check_restore_pane_sizes()
+    
+    def remove(self, child):
+        tk.PanedWindow.remove(self, child)
+        self.visible_panes.remove(child)
+        self._update_visibility()
+        self._check_restore_pane_sizes()
+    
+    def forget(self, child):
+        tk.PanedWindow.forget(self, child)
+        self.visible_panes.remove(child)
+        self._update_visibility()
+        self._check_restore_pane_sizes()
+    
+    def destroy(self):
+        self.winfo_toplevel().unbind("<Configure>", self._configure_binding)
+        tk.PanedWindow.destroy(self)
+        
+    
+    def is_visible(self):
+        if not isinstance(self.master, AutomaticPanedWindow):
+            return self.winfo_ismapped()
+        else:
+            return self in self.master.visible_panes
+    
+    def _on_window_resize(self, event):
+        window = self.winfo_toplevel()
+        window_size = (window.winfo_width(), window.winfo_height())
+        initializing = hasattr(window, "initializing") and window.initializing
+        
+        if (not initializing
+            and not self._restoring_pane_sizes 
+            and (window_size != self._last_window_size or self._full_size_not_final)):
+            self._check_restore_pane_sizes()
+            self._last_window_size = window_size
+    
+    def _on_mouse_dragged(self, event):
+        if event.widget == self and not self._restoring_pane_sizes:
+            self._store_pane_sizes()
+            
+    
+    def _store_pane_sizes(self):
+        if len(self.panes()) > 1:
+            self.last_pane_size = self._get_pane_size("last")
+            if len(self.panes()) > 2:
+                self.first_pane_size = self._get_pane_size("first")
+    
+    def _check_restore_pane_sizes(self):
+        """last (and maybe first) pane sizes are stored, first (or middle)
+        pane changes its size when window is resized"""
+        
+        window = self.winfo_toplevel()
+        if hasattr(window, "initializing") and window.initializing:
+            return
+        
+        try:
+            self._restoring_pane_sizes = True
+            if len(self.panes()) > 1:
+                self._set_pane_size("last", self.last_pane_size)
+                if len(self.panes()) > 2:
+                    self._set_pane_size("first", self.first_pane_size)
+        finally:
+            self._restoring_pane_sizes = False
+    
+    def _get_pane_size(self, which):
+        self.update_idletasks()
+        
+        if which == "first":
+            coord = self.sash_coord(0)
+        else:
+            coord = self.sash_coord(len(self.panes())-2)
+            
+        if self.cget("orient") == tk.HORIZONTAL:
+            full_size = self.winfo_width()
+            sash_distance = coord[0]
+        else:
+            full_size = self.winfo_height()
+            sash_distance = coord[1]
+        
+        if which == "first":
+            return sash_distance
+        else:
+            return full_size - sash_distance 
+        
+    
+    def _set_pane_size(self, which, size):
+        #print("setsize", which, size)
+        self.update_idletasks()
+        
+        if self.cget("orient") == tk.HORIZONTAL:
+            full_size = self.winfo_width()
+        else:
+            full_size = self.winfo_height()
+        
+        self._full_size_not_final = full_size == 1
+        
+        if self._full_size_not_final:
+            return
+        
+        if isinstance(size, float):
+            size = int(full_size * size)
+        
+        #print("full vs size", full_size, size)
+        
+        if which == "first":
+            sash_index = 0
+            sash_distance = size 
+        else:
+            sash_index = len(self.panes())-2
+            sash_distance = full_size - size 
+        
+        if self.cget("orient") == tk.HORIZONTAL:
+            self.sash_place(sash_index, sash_distance, 0)
+            #print("PLACE", sash_index, sash_distance, 0)
+        else:
+            self.sash_place(sash_index, 0, sash_distance)
+            #print("PLACE", sash_index, 0, sash_distance)
+      
+    
+    def _update_visibility(self):
+        if not isinstance(self.master, AutomaticPanedWindow):
+            return
+        
+        if len(self.visible_panes) == 0 and self.is_visible():
+            self.master.forget(self)
+            
+        if len(self.panes()) > 0 and not self.is_visible():
+            self.master.insert("auto", self)
+        
+    
+
+class AutomaticNotebook(ttk.Notebook):
+    """
+    Enables inserting views according to their position keys.
+    Remember its own position key. Automatically updates its visibility.
+    """
+    def __init__(self, master, position_key):
+        ttk.Notebook.__init__(self, master)
+        self.position_key = position_key
+    
+    def add(self, child, **kw):
+        ttk.Notebook.add(self, child, **kw)
+        self._update_visibility()
+    
+    def insert(self, pos, child, **kw):
+        if pos == "auto":
+            for sibling in map(self.nametowidget, self.tabs()):
+                if (not hasattr(sibling, "position_key") 
+                    or sibling.position_key == None
+                    or sibling.position_key > child.position_key):
+                    pos = sibling
+                    break
+            else:
+                pos = "end"
+            
+        ttk.Notebook.insert(self, pos, child, **kw)
+        self._update_visibility()
+    
+    def hide(self, tab_id):
+        ttk.Notebook.hide(self, tab_id)
+        self._update_visibility()
+    
+    def forget(self, tab_id):
+        ttk.Notebook.forget(self, tab_id)
+        self._update_visibility()
+    
+    def is_visible(self):
+        return self in self.master.visible_panes
+    
+    def get_visible_child(self):
+        for child in self.winfo_children():
+            if str(child) == str(self.select()):
+                return child
+            
+        return None
+        
+        
+    def _update_visibility(self):
+        if not isinstance(self.master, AutomaticPanedWindow):
+            return
+        if len(self.tabs()) == 0 and self.is_visible():
+            self.master.remove(self)
+            
+        if len(self.tabs()) > 0 and not self.is_visible():
+            self.master.insert("auto", self)
+        
+
+class TreeFrame(ttk.Frame):
+    def __init__(self, master, columns, displaycolumns='#all', show_scrollbar=True):
+        ttk.Frame.__init__(self, master)
+        self.vert_scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
+        if show_scrollbar:
+            self.vert_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
+        
+        self.tree = ttk.Treeview(self, columns=columns, displaycolumns=displaycolumns, 
+                                 yscrollcommand=self.vert_scrollbar.set)
+        self.tree['show'] = 'headings'
+        self.tree.grid(row=0, column=0, sticky=tk.NSEW)
+        self.vert_scrollbar['command'] = self.tree.yview
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        self.tree.bind("<<TreeviewSelect>>", self.on_select, "+")
+        self.tree.bind("<Double-Button-1>", self.on_double_click, "+")
+        
+    def _clear_tree(self):
+        for child_id in self.tree.get_children():
+            self.tree.delete(child_id)
+    
+    def on_select(self, event):
+        pass
+    
+    def on_double_click(self, event):
+        pass
+
+
+
+def sequence_to_accelerator(sequence):
+    """Translates Tk event sequence to customary shortcut string
+    for showing in the menu"""
+    
+    if not sequence:
+        return ""
+    
+    if not sequence.startswith("<"):
+        return sequence
+    
+    accelerator = (sequence
+        .strip("<>")
+        .replace("Key-", "")
+        .replace("KeyPress-", "")
+        .replace("Control", "Ctrl")
+    )
+    
+    # Tweaking individual parts
+    parts = accelerator.split("-")
+    # tkinter shows shift with capital letter, but in shortcuts it's customary to include it explicitly
+    if len(parts[-1]) == 1 and parts[-1].isupper() and not "Shift" in parts:
+        parts.insert(-1, "Shift")
+    
+    # even when shift is not required, it's customary to show shortcut with capital letter
+    if len(parts[-1]) == 1:
+        parts[-1] = parts[-1].upper()
+    
+    accelerator = "+".join(parts)
+    
+    # Post processing
+    accelerator = (accelerator
+        .replace("Minus", "-").replace("minus", "-")
+        .replace("Plus", "+").replace("plus", "+"))
+    
+    return accelerator
+    
+
+        
+def get_zoomed(toplevel):
+    if "-zoomed" in toplevel.wm_attributes(): # Linux
+        return bool(toplevel.wm_attributes("-zoomed"))
+    else: # Win/Mac
+        return toplevel.wm_state() == "zoomed"
+          
+
+def set_zoomed(toplevel, value):
+    if "-zoomed" in toplevel.wm_attributes(): # Linux
+        toplevel.wm_attributes("-zoomed", str(int(value)))
+    else: # Win/Mac
+        if value:
+            toplevel.wm_state("zoomed")
+        else:
+            toplevel.wm_state("normal")
+
+class EnhancedTextWithLogging(tktextext.EnhancedText):
+    def direct_insert(self, index, chars, tags=()):
+        try:
+            # try removing line numbers
+            # TODO: shouldn't it take place only on paste?
+            # TODO: does it occur when opening a file with line numbers in it?
+            #if self._propose_remove_line_numbers and isinstance(chars, str):
+            #    chars = try_remove_linenumbers(chars, self)
+            concrete_index = self.index(index)
+            return tktextext.EnhancedText.direct_insert(self, index, chars, tags=tags)
+        finally:
+            get_workbench().event_generate("TextInsert", index=concrete_index, 
+                                           text=chars, tags=tags, text_widget=self)
+
+    
+    def direct_delete(self, index1, index2=None):
+        try:
+            # index1 may be eg "sel.first" and it doesn't make sense *after* deletion
+            concrete_index1 = self.index(index1)
+            if index2 is not None:
+                concrete_index2 = self.index(index2)
+            else:
+                concrete_index2 = None
+                
+            return tktextext.EnhancedText.direct_delete(self, index1, index2=index2)
+        finally:
+            get_workbench().event_generate("TextDelete", index1=concrete_index1,
+                                           index2=concrete_index2, text_widget=self)
+            
+    
+class SafeScrollbar(ttk.Scrollbar):
+    def set(self, first, last):
+        try:
+            ttk.Scrollbar.set(self, first, last)
+        except:
+            traceback.print_exc()
+
+class AutoScrollbar(SafeScrollbar):
+    # http://effbot.org/zone/tkinter-autoscrollbar.htm
+    # a vert_scrollbar that hides itself if it's not needed.  only
+    # works if you use the grid geometry manager.
+    def set(self, lo, hi):
+        # TODO: this can make GUI hang or max out CPU when scrollbar wobbles back and forth
+        if float(lo) <= 0.0 and float(hi) >= 1.0:
+            self.grid_remove()
+        else:
+            self.grid()
+        ttk.Scrollbar.set(self, lo, hi)
+    def pack(self, **kw):
+        raise tk.TclError("cannot use pack with this widget")
+    def place(self, **kw):
+        raise tk.TclError("cannot use place with this widget")
+
+def update_entry_text(entry, text):
+    original_state = entry.cget("state")
+    entry.config(state="normal")
+    entry.delete(0, "end")
+    entry.insert(0, text)
+    entry.config(state=original_state)
+
+
+class ScrollableFrame(tk.Frame):
+    # http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame
+    
+    def __init__(self, master):
+        tk.Frame.__init__(self, master, bg=CALM_WHITE)
+        
+        # set up scrolling with canvas
+        vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
+        self.canvas = tk.Canvas(self, bg=CALM_WHITE, bd=0, highlightthickness=0,
+                           yscrollcommand=vscrollbar.set)
+        vscrollbar.config(command=self.canvas.yview)
+        self.canvas.xview_moveto(0)
+        self.canvas.yview_moveto(0)
+        self.canvas.grid(row=0, column=0, sticky=tk.NSEW)
+        vscrollbar.grid(row=0, column=1, sticky=tk.NSEW)
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        
+        self.interior = tk.Frame(self.canvas, bg=CALM_WHITE)
+        self.interior.columnconfigure(0, weight=1)
+        self.interior.rowconfigure(0, weight=1)
+        self.interior_id = self.canvas.create_window(0,0, 
+                                                    window=self.interior, 
+                                                    anchor=tk.NW)
+        self.bind('<Configure>', self._configure_interior, "+")
+        self.bind('<Expose>', self._expose, "+")
+        
+    def _expose(self, event):
+        self.update_idletasks()
+        self._configure_interior(event)
+    
+    def _configure_interior(self, event):
+        # update the scrollbars to match the size of the inner frame
+        size = (self.canvas.winfo_width() , self.interior.winfo_reqheight())
+        self.canvas.config(scrollregion="0 0 %s %s" % size)
+        if (self.interior.winfo_reqwidth() != self.canvas.winfo_width()
+            and self.canvas.winfo_width() > 10):
+            # update the interior's width to fit canvas
+            #print("CAWI", self.canvas.winfo_width())
+            self.canvas.itemconfigure(self.interior_id,
+                                      width=self.canvas.winfo_width())
+
+class TtkDialog(Dialog):
+    def buttonbox(self):
+        '''add standard button box.
+
+        override if you do not want the standard buttons
+        '''
+
+        box = ttk.Frame(self)
+
+        w = ttk.Button(box, text="OK", width=10, command=self.ok, default=tk.ACTIVE)
+        w.pack(side=tk.LEFT, padx=5, pady=5)
+        w = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
+        w.pack(side=tk.LEFT, padx=5, pady=5)
+
+        self.bind("<Return>", self.ok, True)
+        self.bind("<Escape>", self.cancel, True)
+
+        box.pack()
+
+    
+
+class _QueryDialog(TtkDialog):
+
+    def __init__(self, title, prompt,
+                 initialvalue=None,
+                 minvalue = None, maxvalue = None,
+                 master = None,
+                 selection_range=None):
+
+        if not master:
+            master = tk._default_root
+
+        self.prompt   = prompt
+        self.minvalue = minvalue
+        self.maxvalue = maxvalue
+
+        self.initialvalue = initialvalue
+        self.selection_range = selection_range
+
+        Dialog.__init__(self, master, title)
+
+    def destroy(self):
+        self.entry = None
+        Dialog.destroy(self)
+
+    def body(self, master):
+
+        w = ttk.Label(master, text=self.prompt, justify=tk.LEFT)
+        w.grid(row=0, padx=5, sticky=tk.W)
+
+        self.entry = ttk.Entry(master, name="entry")
+        self.entry.grid(row=1, padx=5, sticky="we")
+
+        if self.initialvalue is not None:
+            self.entry.insert(0, self.initialvalue)
+            
+            if self.selection_range:
+                self.entry.icursor(self.selection_range[0])
+                self.entry.select_range(self.selection_range[0], self.selection_range[1])
+            else:
+                self.entry.select_range(0, tk.END)
+
+        return self.entry
+
+    def validate(self):
+        try:
+            result = self.getresult()
+        except ValueError:
+            messagebox.showwarning(
+                "Illegal value",
+                self.errormessage + "\nPlease try again",
+                parent = self
+            )
+            return 0
+
+        if self.minvalue is not None and result < self.minvalue:
+            messagebox.showwarning(
+                "Too small",
+                "The allowed minimum value is %s. "
+                "Please try again." % self.minvalue,
+                parent = self
+            )
+            return 0
+
+        if self.maxvalue is not None and result > self.maxvalue:
+            messagebox.showwarning(
+                "Too large",
+                "The allowed maximum value is %s. "
+                "Please try again." % self.maxvalue,
+                parent = self
+            )
+            return 0
+
+        self.result = result
+
+        return 1
+
+class _QueryString(_QueryDialog):
+    def __init__(self, *args, **kw):
+        if "show" in kw:
+            self.__show = kw["show"]
+            del kw["show"]
+        else:
+            self.__show = None
+        _QueryDialog.__init__(self, *args, **kw)
+
+    def body(self, master):
+        entry = _QueryDialog.body(self, master)
+        if self.__show is not None:
+            entry.configure(show=self.__show)
+        return entry
+
+    def getresult(self):
+        return self.entry.get()
+
+
+class ToolTip(object):
+    """Taken from http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml"""
+
+    def __init__(self, widget, options):
+        self.widget = widget
+        self.tipwindow = None
+        self.id = None
+        self.x = self.y = 0
+        self.options = options
+
+    def showtip(self, text):
+        "Display text in tooltip window"
+        self.text = text
+        if self.tipwindow or not self.text:
+            return
+        x, y, _, cy = self.widget.bbox("insert")
+        x = x + self.widget.winfo_rootx() + 27
+        y = y + cy + self.widget.winfo_rooty() +27
+        self.tipwindow = tw = tk.Toplevel(self.widget)
+        tw.wm_overrideredirect(1)
+        if running_on_mac_os():
+            # TODO: maybe it's because of Tk 8.5, not because of Mac
+            tw.wm_transient(self.widget)
+        tw.wm_geometry("+%d+%d" % (x, y))
+        try:
+            # For Mac OS
+            tw.tk.call("::tk::unsupported::MacWindowStyle",
+                       "style", tw._w,
+                       "help", "noActivates")
+        except tk.TclError:
+            pass        
+        label = tk.Label(tw, text=self.text, **self.options)
+        label.pack()
+
+    def hidetip(self):
+        tw = self.tipwindow
+        self.tipwindow = None
+        if tw:
+            tw.destroy()
+
+def create_tooltip(widget, text,
+                   background="#ffffe0", relief=tk.SOLID, borderwidth=1, padx=1, pady=0,
+                   **kw):
+    options = kw.copy()
+    options["background"] = background
+    options["relief"] = relief
+    options["borderwidth"] = borderwidth
+    options["padx"] = padx
+    options["pady"] = pady
+    
+    toolTip = ToolTip(widget, options)
+    def enter(event):
+        toolTip.showtip(text)
+    def leave(event):
+        toolTip.hidetip()
+    widget.bind('<Enter>', enter)
+    widget.bind('<Leave>', leave)
+
+def askstring(title, prompt, **kw):
+    '''get a string from the user
+
+    Arguments:
+
+        title -- the dialog title
+        prompt -- the label text
+        **kw -- see SimpleDialog class
+
+    Return value is a string
+    '''
+    d = _QueryString(title, prompt, **kw)
+    return d.result
+
+
+def get_current_notebook_tab_widget(notebook):    
+    for child in notebook.winfo_children():
+        if str(child) == str(notebook.select()):
+            return child
+        
+    return None
+
+def create_string_var(value, modification_listener=None):
+    """Creates a tk.StringVar with "modified" attribute
+    showing whether the variable has been modified after creation"""
+    return _create_var(tk.StringVar, value, modification_listener)
+
+def create_int_var(value, modification_listener=None):
+    """See create_string_var"""
+    return _create_var(tk.IntVar, value, modification_listener)
+
+def create_double_var(value, modification_listener=None):
+    """See create_string_var"""
+    return _create_var(tk.DoubleVar, value, modification_listener)
+
+def create_boolean_var(value, modification_listener=None):
+    """See create_string_var"""
+    return _create_var(tk.BooleanVar, value, modification_listener)
+
+def _create_var(class_, value, modification_listener):
+    var = class_(value=value)
+    var.modified = False
+    
+    def on_write(*args):
+        var.modified = True
+        if modification_listener:
+            try:
+                modification_listener()
+            except:
+                # Otherwise whole process will be brought down
+                # because for some reason Tk tries to call non-existing method
+                # on variable
+                get_workbench().report_exception()
+    
+    # TODO: https://bugs.python.org/issue22115 (deprecation warning)
+    var.trace("w", on_write)
+    return var
+
+def shift_is_pressed(event_state):
+    # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-handlers.html
+    # http://stackoverflow.com/q/32426250/261181
+    return event_state & 0x0001
+
+def control_is_pressed(event_state):
+    # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-handlers.html
+    # http://stackoverflow.com/q/32426250/261181
+    return event_state & 0x0004
+
+def select_sequence(win_version, mac_version, linux_version=None):
+    if running_on_windows():
+        return win_version
+    elif running_on_mac_os():
+        return mac_version
+    elif running_on_linux() and linux_version:
+        return linux_version
+    else:
+        return win_version
+
+def try_remove_linenumbers(text, master):
+    try:        
+        if has_line_numbers(text) and tkMessageBox.askyesno (
+                  title="Remove linenumbers",
+                  message="Do you want to remove linenumbers from pasted text?",
+                  default=tkMessageBox.YES,
+                  master=master):
+            return remove_line_numbers(text)
+        else:
+            return text
+    except:
+        traceback.print_exc()
+        return text
+
+
+def has_line_numbers(text):
+    lines = text.splitlines()
+    return (len(lines) > 2 
+            and all([len(split_after_line_number(line)) == 2 for line in lines]))
+
+def split_after_line_number(s): 
+    parts = re.split("(^\s*\d+\.?)", s)
+    if len(parts) == 1:
+        return parts
+    else:
+        assert len(parts) == 3 and parts[0] == ''
+        return parts[1:]
+
+def remove_line_numbers(s):
+    cleaned_lines = []
+    for line in s.splitlines():
+        parts = split_after_line_number(line)
+        if len(parts) != 2:
+            return s
+        else:
+            cleaned_lines.append(parts[1])
+    
+    return textwrap.dedent(("\n".join(cleaned_lines)) + "\n")
+    
+def get_main_background():
+    main_background_option = get_workbench().get_option("theme.main_background")
+    if main_background_option is not None:
+        return main_background_option
+    else:    
+        theme = ttk.Style().theme_use()
+        
+        if theme == "clam":
+            return "#dcdad5"
+        elif theme == "aqua":
+            return "systemSheetBackground"
+        else: 
+            return "SystemButtonFace"
+    
+def get_dialog_background_color():    
+    theme = ttk.Style().theme_use()
+    
+    if theme == "aqua":
+        return "systemSheetBackground"
+    else: 
+        return "SystemButtonFace"
+
+def center_window(win, master=None):
+    # looks like it doesn't take window border into account
+    win.update_idletasks()
+    
+    if getattr(master, "initializing", False):
+        # can't get reliable positions when main window is not in mainloop yet
+        left = (win.winfo_screenwidth() - 600) // 2
+        top = (win.winfo_screenheight() - 400) // 2
+    else:
+        if master is None:
+            left = win.winfo_screenwidth() - win.winfo_width() // 2
+            top = win.winfo_screenheight() - win.winfo_height() // 2
+        else:
+            left = master.winfo_rootx() + master.winfo_width() // 2 - win.winfo_width() // 2
+            top = master.winfo_rooty() + master.winfo_height() // 2 - win.winfo_height() // 2
+        
+    win.geometry("+%d+%d" % (left, top))
+
+class BusyTk(tk.Tk):
+    def __init__(self, async_result, description, title="Please wait!"):
+        self._async_result = async_result
+        tk.Tk.__init__(self)
+        self.update_idletasks()
+        screen_width = self.winfo_screenwidth()
+        screen_height = self.winfo_screenheight()
+        win_width = screen_width // 3
+        win_height = screen_height // 3
+        x = screen_width//2 - win_width//2
+        y = screen_height//2 - win_height//2
+        self.geometry("%dx%d+%d+%d" % (win_width, win_height, x, y))        
+        
+        main_frame = ttk.Frame(self)
+        main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15)
+        main_frame.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(0, weight=1)
+        self.title(title)
+        self.resizable(height=tk.FALSE, width=tk.FALSE)
+        self.protocol("WM_DELETE_WINDOW", self._ok)
+        self.desc_label = ttk.Label(main_frame, text=description)
+        self.desc_label.grid(padx=20, pady=20, sticky="nsew")
+        
+        self.update_idletasks()
+        self.after(500, self._poll)
+    
+    def _poll(self):
+        if self._async_result.ready():
+            self._ok()
+        else:
+            self.after(500, self._poll)
+            self.desc_label["text"] = self.desc_label["text"] + "."
+    
+    def _ok(self):
+        self.destroy() 
+
+
+def run_with_busy_window(action, args=(), description=""):
+    # http://stackoverflow.com/a/14299004/261181
+    from multiprocessing.pool import ThreadPool
+    pool = ThreadPool(processes=1)
+    
+    async_result = pool.apply_async(action, args) 
+    dlg = BusyTk(async_result, description=description)
+    dlg.mainloop()
+    
+    return async_result.get()  
+
+
+class SubprocessDialog(tk.Toplevel):
+    """Shows incrementally the output of given subprocess.
+    Allows cancelling"""
+    
+    def __init__(self, master, proc, title, long_description=None, autoclose=True):
+        self._proc = proc
+        self.stdout = ""
+        self.stderr = ""
+        self._stdout_thread = None
+        self._stderr_thread = None
+        self.returncode = None
+        self.cancelled = False
+        self._autoclose = autoclose
+        self._event_queue = collections.deque()
+        
+        tk.Toplevel.__init__(self, master)
+
+        self.rowconfigure(0, weight=1)
+        self.columnconfigure(0, weight=1)
+        main_frame = ttk.Frame(self) # To get styled background
+        main_frame.grid(sticky="nsew")
+
+        text_font=tk.font.nametofont("TkFixedFont").copy()
+        text_font["size"] = int(text_font["size"] * 0.9)
+        text_font["family"] = "Courier" if running_on_mac_os() else "Courier New"
+        text_frame = tktextext.TextFrame(main_frame, read_only=True, horizontal_scrollbar=False,
+                                         background=get_main_background(),
+                                         font=text_font,
+                                         wrap="word")
+        text_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=15, pady=15)
+        self.text = text_frame.text
+        self.text["width"] = 60
+        self.text["height"] = 7
+        if long_description is not None:
+            self.text.direct_insert("1.0", long_description + "\n\n")
+        
+        self.button = ttk.Button(main_frame, text="Cancel", command=self._close)
+        self.button.grid(row=1, column=0, pady=(0,15))
+        
+        main_frame.rowconfigure(0, weight=1)
+        main_frame.columnconfigure(0, weight=1)
+        
+
+        self.title(title)
+        if misc_utils.running_on_mac_os():
+            self.configure(background="systemSheetBackground")
+        #self.resizable(height=tk.FALSE, width=tk.FALSE)
+        self.transient(master)
+        self.grab_set() # to make it active and modal
+        self.text.focus_set()
+        
+        
+        self.bind('<Escape>', self._close_if_done, True) # escape-close only if process has completed 
+        self.protocol("WM_DELETE_WINDOW", self._close)
+        center_window(self, master)
+        
+        self._start_listening()
+    
+    def _start_listening(self):
+       
+        def listen_stream(stream_name):
+            stream = getattr(self._proc, stream_name)
+            while True:
+                data = stream.readline()
+                self._event_queue.append((stream_name, data))
+                setattr(self, stream_name, getattr(self, stream_name) + data)
+                if data == '':
+                    break
+            
+            self.returncode = self._proc.wait()
+        
+        self._stdout_thread = threading.Thread(target=listen_stream, args=["stdout"])
+        self._stdout_thread.start()
+        if self._proc.stderr is not None:
+            self._stderr_thread = threading.Thread(target=listen_stream, args=["stderr"])
+            self._stderr_thread.start()
+        
+        def poll_output_events():
+            while len(self._event_queue) > 0:
+                stream_name, data = self._event_queue.popleft()
+                self.text.direct_insert("end", data, tags=(stream_name, ))
+                self.text.see("end")
+            
+            self.returncode = self._proc.poll() 
+            if self.returncode == None:
+                self.after(200, poll_output_events)
+            else:
+                self.button["text"] = "OK"
+                self.button.focus_set()
+                if self.returncode != 0:
+                    self.text.direct_insert("end", "\n\nReturn code: ", ("stderr", ))
+                elif self._autoclose:
+                    self._close()
+        
+        poll_output_events()
+        
+    
+    def _close_if_done(self, event):
+        if self._proc.poll() is not None:
+            self._close(event)        
+
+    def _close(self, event=None):
+        if self._proc.poll() is None:
+            if messagebox.askyesno("Cancel the process?",
+                "The process is still running.\nAre you sure you want to cancel?"):
+                # try gently first
+                try:
+                    if running_on_windows():
+                        os.kill(self._proc.pid, signal.CTRL_BREAK_EVENT)  # @UndefinedVariable
+                    else:
+                        os.kill(self._proc.pid, signal.SIGINT)
+                        
+                    self._proc.wait(2)
+                except subprocess.TimeoutExpired:
+                    if self._proc.poll() is None:
+                        # now let's be more concrete
+                        self._proc.kill()
+                
+                
+                self.cancelled = True
+                # Wait for threads to finish
+                self._stdout_thread.join(2)
+                if self._stderr_thread is not None:
+                    self._stderr_thread.join(2)
+                
+                # fetch output about cancelling
+                while len(self._event_queue) > 0:
+                    stream_name, data = self._event_queue.popleft()
+                    self.text.direct_insert("end", data, tags=(stream_name, ))
+                self.text.direct_insert("end", "\n\nPROCESS CANCELLED")
+                self.text.see("end")
+                    
+                    
+            else:
+                return
+        else:
+            self.destroy()
+
+def get_busy_cursor():
+    if running_on_windows():
+        return "wait"
+    elif running_on_mac_os():
+        return "spinning"
+    else:
+        return "watch"
+
+def get_tk_version_str():
+    return tk._default_root.tk.call('info', 'patchlevel')
+
+def get_tk_version_info():
+    result = []
+    for part in get_tk_version_str().split("."):
+        try:
+            result.append(int(part))
+        except:
+            result.append(0)
+    return tuple(result) 
diff --git a/thonny/workbench.py b/thonny/workbench.py
new file mode 100644
index 0000000..e12ce79
--- /dev/null
+++ b/thonny/workbench.py
@@ -0,0 +1,1210 @@
+# -*- coding: utf-8 -*-
+
+import importlib
+import os.path
+import sys
+from tkinter import ttk
+import traceback
+
+from thonny import ui_utils
+from thonny.code import EditorNotebook
+from thonny.common import Record, UserError
+from thonny.config import try_load_configuration
+from thonny.misc_utils import running_on_mac_os, running_on_linux
+from thonny.ui_utils import sequence_to_accelerator, AutomaticPanedWindow, AutomaticNotebook,\
+    create_tooltip, get_current_notebook_tab_widget, select_sequence
+import tkinter as tk
+import tkinter.font as tk_font
+import tkinter.messagebox as tk_messagebox
+from thonny.running import Runner
+import thonny.globals
+import logging
+from thonny.globals import register_runner, get_runner
+from thonny.config_ui import ConfigurationDialog
+import pkgutil
+import socket
+import queue
+from _thread import start_new_thread
+import ast
+from thonny import THONNY_USER_DIR
+
+THONNY_PORT = 4957
+SERVER_SUCCESS = "OK"
+CONFIGURATION_FILE_NAME = os.path.join(THONNY_USER_DIR, "configuration.ini")
+SINGLE_INSTANCE_DEFAULT = True
+
+class Workbench(tk.Tk):
+    """
+    Thonny's main window and communication hub.
+    
+    Is responsible for:
+    
+        * creating the main window
+        * maintaining layout (_init_containers)
+        * loading plugins (_init_plugins, add_view, add_command)        
+        * providing references to main components (editor_notebook and runner)
+        * communication between other components (see event_generate and bind)
+        * configuration services (get_option, set_option, add_defaults)
+        * loading translations
+        * maintaining fonts (get_font, increasing and decreasing font size)
+    
+    After workbench and plugins get loaded, 3 kinds of events start happening:
+        
+        * User events (keypresses, mouse clicks, menu selections, ...)
+        * Virtual events (mostly via get_workbench().event_generate). These include:
+          events reported via and dispatched by Tk event system;
+          WorkbenchEvent-s, reported via and dispatched by enhanced get_workbench().event_generate.
+        * Events from the background process (program output notifications, input requests,
+          notifications about debugger's progress)
+          
+    """
+    def __init__(self, server_socket=None):
+        self._destroying = False
+        self.initializing = True
+        tk.Tk.__init__(self, className="Thonny")
+        # self.tk.call("tk", "scaling", 2.0)
+        tk.Tk.report_callback_exception = self._on_tk_exception
+        self._event_handlers = {}
+        self._images = set() # to avoid Python garbage collecting them
+        self._image_mapping = {} # to allow specify different images in a theme
+        self._backends = {}
+        self._theme_tweaker = None
+        thonny.globals.register_workbench(self)
+        
+        self._init_configuration()
+        self._init_diagnostic_logging()
+        logging.info("Loading early plugins from " + str(sys.path))
+        self._load_early_plugins()
+        
+        self._editor_notebook = None
+        self._select_theme()
+        self._init_fonts()
+        self._init_window()
+        self._init_menu()
+        
+        self.title("Thonny")
+        
+        self._init_containers()
+        
+        self._init_runner()
+            
+        self._init_commands()
+        self._load_plugins()
+        
+        self._update_toolbar()
+        try:
+            self._editor_notebook.load_startup_files()
+        except:
+            self.report_exception()
+            
+        self._editor_notebook.focus_set()
+        self._try_action(self._open_views)
+        
+        if server_socket is not None:
+            self._init_server_loop(server_socket)
+        
+        self.bind_class("CodeViewText", "<<CursorMove>>", self.update_title, True)
+        self.bind_class("CodeViewText", "<<Modified>>", self.update_title, True)
+        self.bind_class("CodeViewText", "<<TextChange>>", self.update_title, True)
+        self.get_editor_notebook().bind("<<NotebookTabChanged>>", self.update_title ,True)
+        
+        self.initializing = False
+    
+    def _try_action(self, action):
+        try:
+            action()
+        except:
+            self.report_exception()
+        
+    def _init_configuration(self):
+        self._configuration_manager = try_load_configuration(CONFIGURATION_FILE_NAME)
+        self._configuration_pages = {}
+
+        self.set_default("general.single_instance", SINGLE_INSTANCE_DEFAULT)
+        self.set_default("general.expert_mode", False)
+        self.set_default("debug_mode", False)
+
+    
+    def _init_diagnostic_logging(self):
+        logFormatter = logging.Formatter('%(levelname)s: %(message)s')
+        root_logger = logging.getLogger()
+        
+        log_file = os.path.join(THONNY_USER_DIR, "frontend.log")
+        file_handler = logging.FileHandler(log_file, encoding="UTF-8", mode="w")
+        file_handler.setFormatter(logFormatter)
+        file_handler.setLevel(logging.INFO);
+        root_logger.addHandler(file_handler)
+        
+        console_handler = logging.StreamHandler(sys.stdout)
+        console_handler.setFormatter(logFormatter)
+        console_handler.setLevel(logging.INFO);
+        root_logger.addHandler(console_handler)
+        
+        root_logger.setLevel(logging.INFO)
+        
+        import faulthandler
+        fault_out = open(os.path.join(THONNY_USER_DIR, "frontend_faults.log"), mode="w")
+        faulthandler.enable(fault_out)
+        
+    def _init_window(self):
+        
+        self.set_default("layout.zoomed", False)
+        self.set_default("layout.top", 15)
+        self.set_default("layout.left", 150)
+        self.set_default("layout.width", 700)
+        self.set_default("layout.height", 650)
+        self.set_default("layout.w_width", 200)
+        self.set_default("layout.e_width", 200)
+        self.set_default("layout.s_height", 200)
+        
+        # I don't actually need saved options for Full screen/maximize view,
+        # but it's easier to create menu items, if I use configuration manager's variables
+        self.set_default("view.full_screen", False)  
+        self.set_default("view.maximize_view", False)
+        
+        # In order to avoid confusion set these settings to False 
+        # even if they were True when Thonny was last run
+        self.set_option("view.full_screen", False)
+        self.set_option("view.maximize_view", False)
+        
+        
+        self.geometry("{0}x{1}+{2}+{3}".format(self.get_option("layout.width"),
+                                            self.get_option("layout.height"),
+                                            self.get_option("layout.left"),
+                                            self.get_option("layout.top")))
+        
+        if self.get_option("layout.zoomed"):
+            ui_utils.set_zoomed(self, True)
+        
+        self.protocol("WM_DELETE_WINDOW", self._on_close)
+        
+        # Window icons
+        window_icons = self.get_option("theme.window_icons") 
+        if window_icons:
+            imgs = [self.get_image(filename) for filename in window_icons]
+            self.iconphoto(True, *imgs)
+        elif running_on_linux() and ui_utils.get_tk_version_info() >= (8,6):
+            self.iconphoto(True, self.get_image("thonny.png"))
+        else:
+            icon_file = os.path.join(self.get_package_dir(), "res", "thonny.ico")
+            try:
+                self.iconbitmap(icon_file, default=icon_file)
+            except:
+                try:
+                    # seems to work in mac
+                    self.iconbitmap(icon_file)
+                except:
+                    pass # TODO: try to get working in Ubuntu  
+        
+        self.bind("<Configure>", self._on_configure, True)
+        
+    def _init_menu(self):
+        self.option_add('*tearOff', tk.FALSE)
+        self._menubar = tk.Menu(self, **self.get_option("theme.menubar_options", {
+            #"relief" : "flat",
+            "activeborderwidth" : 0
+        }))
+        self["menu"] = self._menubar
+        self._menus = {}
+        self._menu_item_groups = {} # key is pair (menu_name, command_label)
+        self._menu_item_testers = {} # key is pair (menu_name, command_label)
+        
+        # create standard menus in correct order
+        self.get_menu("file", "File")
+        self.get_menu("edit", "Edit")
+        self.get_menu("view", "View")
+        self.get_menu("run", "Run")
+        self.get_menu("tools", "Tools")
+        self.get_menu("help", "Help")
+    
+    def _load_early_plugins(self):
+        """load_early_plugin can't use nor GUI neither Runner"""
+        self._load_plugins("load_early_plugin")
+        
+    def _load_plugins(self, load_function_name="load_plugin"):
+        # built-in plugins
+        import thonny.plugins
+        self._load_plugins_from_path(thonny.plugins.__path__, "thonny.plugins.",
+                                     load_function_name=load_function_name)
+        
+        # 3rd party plugins from namespace package
+        try:
+            import thonnycontrib  # @UnresolvedImport
+        except ImportError:
+            # No 3rd party plugins installed
+            pass
+        else:
+            self._load_plugins_from_path(thonnycontrib.__path__, "thonnycontrib.",
+                                     load_function_name=load_function_name)
+        
+    def _load_plugins_from_path(self, path, prefix="", load_function_name="load_plugin"):
+        for _, module_name, _ in pkgutil.iter_modules(path, prefix):
+            try:
+                m = importlib.import_module(module_name)
+                if hasattr(m, load_function_name):
+                    getattr(m, load_function_name)()
+            except:
+                logging.exception("Failed loading plugin '" + module_name + "'")
+    
+                                
+    def _init_fonts(self):
+        self.set_default("view.io_font_family", 
+                        "Courier" if running_on_mac_os() else "Courier New")
+        
+        default_editor_family = "Courier New"
+        families = tk_font.families()
+        
+        for family in ["Consolas", "Ubuntu Mono", "Menlo", "DejaVu Sans Mono"]:
+            if family in families:
+                default_editor_family = family
+                break
+        
+        self.set_default("view.editor_font_family", default_editor_family)
+        self.set_default("view.editor_font_size", 
+                        14 if running_on_mac_os() else 11)
+
+        default_font = tk_font.nametofont("TkDefaultFont")
+
+        self._fonts = {
+            'IOFont' : tk_font.Font(family=self.get_option("view.io_font_family")),
+            'EditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family")),
+            'BoldEditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family"),
+                                            weight="bold"),
+            'ItalicEditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family"),
+                                            slant="italic"),
+            'BoldItalicEditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family"),
+                                            weight="bold", slant="italic"),
+            'TreeviewFont' : tk_font.Font(family=default_font.cget("family"),
+                                          size=default_font.cget("size"))
+        }
+        
+        self.update_fonts()
+        
+    def _init_runner(self):
+        try:
+            runner = Runner()
+            register_runner(runner)
+            runner.start()
+        except:
+            self.report_exception("Error when initializing backend")
+    
+    def _init_server_loop(self, server_socket):
+        """Socket will listen requests from newer Thonny instances,
+        which try to delegate opening files to older instance"""
+        self._requests_from_socket = queue.Queue()
+        
+        def server_loop():
+            while True:
+                logging.debug("Waiting for next client")
+                (client_socket, _) = server_socket.accept()
+                try:
+                    self._handle_socket_request(client_socket)
+                except:
+                    traceback.print_exc()
+        
+        start_new_thread(server_loop, ())
+        self._poll_socket_requests()
+
+    def _init_commands(self):
+        
+        self.add_command("exit", "file", "Exit",
+            self._on_close, 
+            default_sequence=select_sequence("<Alt-F4>", "<Command-q>"))
+        
+        
+        self.add_command("show_options", "tools", "Options...", self._cmd_show_options, group=180)
+        self.createcommand("::tk::mac::ShowPreferences", self._cmd_show_options)
+        
+        self.add_command("increase_font_size", "view", "Increase font size",
+            lambda: self._change_font_size(1),
+            default_sequence=select_sequence("<Control-plus>", "<Command-Shift-plus>"),
+            group=60)
+                
+        self.add_command("decrease_font_size", "view", "Decrease font size",
+            lambda: self._change_font_size(-1),
+            default_sequence=select_sequence("<Control-minus>", "<Command-minus>"),
+            group=60)
+        
+        self.bind("<Control-MouseWheel>", self._cmd_zoom_with_mouse, True)
+        
+        self.add_command("focus_editor", "view", "Focus editor",
+            self._cmd_focus_editor,
+            default_sequence="<Alt-e>",
+            group=70)
+        
+                
+        self.add_command("focus_shell", "view", "Focus shell",
+            self._cmd_focus_shell,
+            default_sequence="<Alt-s>",
+            group=70)
+        
+        if self.get_option("general.expert_mode"):
+            
+            self.add_command("toggle_maximize_view", "view", "Maximize view",
+                self._cmd_toggle_maximize_view,
+                flag_name="view.maximize_view",
+                default_sequence=None,
+                group=80)
+            self.bind_class("TNotebook", "<Double-Button-1>", self._maximize_view, True)
+            self.bind("<Escape>", self._unmaximize_view, True)
+            
+            if not running_on_mac_os():
+                # TODO: approach working in Win/Linux doesn't work in mac as it should and only confuses
+                self.add_command("toggle_maximize_view", "view", "Full screen",
+                    self._cmd_toggle_full_screen,
+                    flag_name="view.full_screen",
+                    default_sequence=select_sequence("<F11>", "<Command-Shift-F>"),
+                    group=80)
+        
+            
+    def _init_containers(self):
+        
+        # Main frame functions as
+        # - a backgroud behind padding of main_pw, without this OS X leaves white border
+        # - a container to be hidden, when a view is maximized and restored when view is back home
+        main_frame= ttk.Frame(self) # 
+        self._main_frame = main_frame
+        main_frame.grid(row=0, column=0, sticky=tk.NSEW)
+        self.columnconfigure(0, weight=1)
+        self.rowconfigure(0, weight=1)
+        self._maximized_view = None
+        
+        self._toolbar = ttk.Frame(main_frame, padding=0) # TODO: height=30 ?
+        self._toolbar.grid(column=0, row=0, sticky=tk.NSEW, padx=10, pady=(5,0))
+        
+        self.set_default("layout.main_pw_first_pane_size", 1/3)
+        self.set_default("layout.main_pw_last_pane_size", 1/3)
+        self._main_pw = AutomaticPanedWindow(main_frame, orient=tk.HORIZONTAL,
+            first_pane_size=self.get_option("layout.main_pw_first_pane_size"),
+            last_pane_size=self.get_option("layout.main_pw_last_pane_size")
+        )
+        
+        self._main_pw.grid(column=0, row=1, sticky=tk.NSEW, padx=10, pady=10)
+        main_frame.columnconfigure(0, weight=1)
+        main_frame.rowconfigure(1, weight=1)
+        
+        self.set_default("layout.west_pw_first_pane_size", 1/3)
+        self.set_default("layout.west_pw_last_pane_size", 1/3)
+        self.set_default("layout.center_pw_first_pane_size", 1/3)
+        self.set_default("layout.center_pw_last_pane_size", 1/3)
+        self.set_default("layout.east_pw_first_pane_size", 1/3)
+        self.set_default("layout.east_pw_last_pane_size", 1/3)
+        
+        self._west_pw = AutomaticPanedWindow(self._main_pw, 1, orient=tk.VERTICAL,
+            first_pane_size=self.get_option("layout.west_pw_first_pane_size"),
+            last_pane_size=self.get_option("layout.west_pw_last_pane_size")
+        )
+        self._center_pw = AutomaticPanedWindow(self._main_pw, 2, orient=tk.VERTICAL,
+            first_pane_size=self.get_option("layout.center_pw_first_pane_size"),
+            last_pane_size=self.get_option("layout.center_pw_last_pane_size")
+        )
+        self._east_pw = AutomaticPanedWindow(self._main_pw, 3, orient=tk.VERTICAL,
+            first_pane_size=self.get_option("layout.east_pw_first_pane_size"),
+            last_pane_size=self.get_option("layout.east_pw_last_pane_size")
+        )
+        
+        self._view_records = {}
+        self._view_notebooks = {
+            'nw' : AutomaticNotebook(self._west_pw, 1),
+            'w'  : AutomaticNotebook(self._west_pw, 2),
+            'sw' : AutomaticNotebook(self._west_pw, 3),
+            
+            's'  : AutomaticNotebook(self._center_pw, 3),
+            
+            'ne' : AutomaticNotebook(self._east_pw, 1),
+            'e'  : AutomaticNotebook(self._east_pw, 2),
+            'se' : AutomaticNotebook(self._east_pw, 3),
+        }
+        
+        for nb_name in self._view_notebooks:
+            self.set_default("layout.notebook_" + nb_name + "_visible_view", None)
+
+        self._editor_notebook = EditorNotebook(self._center_pw)
+        self._editor_notebook.position_key = 1
+        self._center_pw.insert("auto", self._editor_notebook)
+
+    def _select_theme(self):
+        style = ttk.Style()
+        
+        preferred_theme = self.get_option("theme.preferred_theme")
+        
+        if preferred_theme in style.theme_names():
+            style.theme_use(preferred_theme)
+        elif 'xpnative' in style.theme_names():
+            # in Win7 'xpnative' gives better scrollbars than 'vista'
+            style.theme_use('xpnative') 
+        elif 'vista' in style.theme_names():
+            style.theme_use('vista')
+        elif 'clam' in style.theme_names():
+            style.theme_use('clam')
+        
+        if self._theme_tweaker is not None:
+            self._theme_tweaker()
+
+        
+    def add_command(self, command_id, menu_name, command_label, handler,
+                    tester=None,
+                    default_sequence=None,
+                    flag_name=None,
+                    skip_sequence_binding=False,
+                    accelerator=None,
+                    group=99,
+                    position_in_group="end",
+                    image_filename=None,
+                    include_in_toolbar=False,
+                    bell_when_denied=True):
+        """Adds an item to specified menu.
+        
+        Args:
+            menu_name: Name of the menu the command should appear in.
+                Standard menu names are "file", "edit", "run", "view", "help".
+                If a menu with given name doesn't exist, then new menu is created
+                (with label=name).
+            command_label: Label for this command
+            handler: Function to be called when the command is invoked. 
+                Should be callable with one argument (the event or None).
+            tester: Function to be called for determining if command is available or not.
+                Should be callable with one argument (the event or None).
+                Should return True or False.
+                If None then command is assumed to be always available.
+            default_sequence: Default shortcut (Tk style)
+            flag_name: Used for toggle commands. Indicates the name of the boolean option.
+            group: Used for grouping related commands together. Value should be int. 
+                Groups with smaller numbers appear before.
+        
+        Returns:
+            None
+        """     
+        
+        def dispatch(event=None):
+            if not tester or tester():
+                denied = False
+                handler()
+            else:
+                denied = True
+                logging.debug("Command '" + command_id + "' execution denied")
+                if bell_when_denied:
+                    self.bell()
+                
+            self.event_generate("Command", command_id=command_id, denied=denied)
+        
+        sequence_option_name = "shortcuts." + command_id
+        self.set_default(sequence_option_name, default_sequence)
+        sequence = self.get_option(sequence_option_name) 
+        
+        if sequence and not skip_sequence_binding:
+            self.bind_all(sequence, dispatch, True)
+        
+        
+        def dispatch_from_menu():
+            # I don't like that Tk menu toggles checbutton variable
+            # automatically before calling the handler.
+            # So I revert the toggle before calling the actual handler.
+            # This way the handler doesn't have to worry whether it
+            # needs to toggle the variable or not, and it can choose to 
+            # decline the toggle.
+            if flag_name is not None:
+                var = self.get_variable(flag_name)
+                var.set(not var.get())
+                
+            dispatch(None)
+        
+        if image_filename:
+            image = self.get_image(image_filename)
+        else:
+            image = None
+        
+        if image and self.get_option("theme.icons_in_menus", True):
+            menu_image = image
+        elif flag_name: 
+            # no image or black next to a checkbox
+            menu_image = None
+        else:
+            menu_image = self.get_image ("16x16_blank.gif")
+        
+        if not accelerator and sequence:
+            accelerator = sequence_to_accelerator(sequence)
+        
+        menu = self.get_menu(menu_name)
+        menu.insert(
+            self._find_location_for_menu_item(menu_name, command_label, group, position_in_group),
+            "checkbutton" if flag_name else "command",
+            label=command_label,
+            accelerator=accelerator,
+            image=menu_image, 
+            compound=tk.LEFT,
+            variable=self.get_variable(flag_name) if flag_name else None,
+            command=dispatch_from_menu)
+        
+        # remember the details that can't be stored in Tkinter objects
+        self._menu_item_groups[(menu_name, command_label)] = group
+        self._menu_item_testers[(menu_name, command_label)] = tester
+        
+        if include_in_toolbar:
+            toolbar_group = self._get_menu_index(menu) * 100 + group
+            self._add_toolbar_button(image, command_label, accelerator, handler, tester,
+                toolbar_group)
+        
+    
+    def add_view(self, class_, label, default_location,
+                visible_by_default=False,
+                default_position_key=None):
+        """Adds item to "View" menu for showing/hiding given view. 
+        
+        Args:
+            view_class: Class or constructor for view. Should be callable with single
+                argument (the master of the view)
+            label: Label of the view tab
+            location: Location descriptor. Can be "nw", "sw", "s", "se", "ne"
+        
+        Returns: None        
+        """
+        view_id = class_.__name__
+        if default_position_key == None:
+            default_position_key = label
+        
+        self.set_default("view." + view_id + ".visible" , visible_by_default)
+        self.set_default("view." + view_id + ".location", default_location)
+        self.set_default("view." + view_id + ".position_key", default_position_key)
+        
+        self._view_records[view_id] = {
+            "class" : class_,
+            "label" : label,
+            "location" : self.get_option("view." + view_id + ".location"),
+            "position_key" : self.get_option("view." + view_id + ".position_key")
+        }
+        
+        visibility_flag = self.get_variable("view." + view_id + ".visible")
+        
+        # handler
+        def toggle_view_visibility():
+            if visibility_flag.get():
+                self.hide_view(view_id)
+            else:
+                self.show_view(view_id, True)
+        
+        self.add_command("toggle_" + view_id,
+            menu_name="view",
+            command_label=label,
+            handler=toggle_view_visibility,
+            flag_name="view." + view_id + ".visible",
+            group=10,
+            position_in_group="alphabetic")
+        
+        if visibility_flag.get():
+            self.show_view(view_id, False)
+    
+    def add_configuration_page(self, title, page_class):
+        self._configuration_pages[title] = page_class
+    
+    def map_image(self, original_image, new_image):
+        self._image_mapping[original_image] = new_image
+    
+    def add_backend(self, descriptor, proxy_class):
+        self._backends[descriptor] = proxy_class
+    
+    def get_backends(self):
+        return self._backends
+    
+    def get_option(self, name, default=None):
+        return self._configuration_manager.get_option(name, default)
+    
+    def set_option(self, name, value):
+        self._configuration_manager.set_option(name, value)
+    
+    def set_theme_tweaker(self, fun):
+        self._theme_tweaker = fun
+    
+    def set_default(self, name, default_value):
+        """Registers a new option.
+        
+        If the name contains a period, then the part left to the (first) period
+        will become the section of the option and rest will become name under that 
+        section.
+        
+        If the name doesn't contain a period, then it will be added under section 
+        "general".
+        """
+        self._configuration_manager.set_default(name, default_value)
+    
+    def get_variable(self, name):
+        return self._configuration_manager.get_variable(name)
+    
+    def get_font(self, name):
+        """
+        Supported names are EditorFont and BoldEditorFont
+        """
+        return self._fonts[name]
+    
+    
+    def get_menu(self, name, label=None):
+        """Gives the menu with given name. Creates if not created yet.
+        
+        Args:
+            name: meant to be used as not translatable menu name
+            label: translated label, used only when menu with given name doesn't exist yet
+        """
+        if name not in self._menus:
+            menu = tk.Menu(self._menubar, self.get_option("theme.menu_options", {
+                #"relief" : "flat",
+                #"activeborderwidth" : 0
+            }))
+            menu["postcommand"] = lambda: self._update_menu(menu, name)
+            self._menubar.add_cascade(label=label if label else name, menu=menu)
+            
+            self._menus[name] = menu
+            if label:
+                self._menus[label] = menu
+                
+        return self._menus[name]
+    
+    def get_view(self, view_id, create=True):
+        if "instance" not in self._view_records[view_id]:
+            if not create:
+                return None
+            class_ = self._view_records[view_id]["class"]
+            location = self._view_records[view_id]["location"]
+            master = self._view_notebooks[location]
+            
+            # create the view
+            view = class_(self) # View's master is workbench to allow making it maximized
+            view.position_key = self._view_records[view_id]["position_key"]
+            self._view_records[view_id]["instance"] = view
+
+            # create the view home_widget to be added into notebook
+            view.home_widget = ttk.Frame(master) 
+            view.home_widget.columnconfigure(0, weight=1)
+            view.home_widget.rowconfigure(0, weight=1)
+            view.home_widget.maximizable_widget = view
+            if hasattr(view, "position_key"):
+                view.home_widget.position_key = view.position_key
+            
+            # initially the view will be in it's home_widget
+            view.grid(row=0, column=0, sticky=tk.NSEW, in_=view.home_widget)
+            view.hidden = True
+            
+        return self._view_records[view_id]["instance"]
+    
+    def get_current_editor(self):
+        return self._editor_notebook.get_current_editor()
+    
+    def get_editor_notebook(self):
+        return self._editor_notebook
+    
+    def get_package_dir(self):
+        """Returns thonny package directory"""
+        return os.path.dirname(sys.modules["thonny"].__file__)
+    
+    def get_image(self, filename, tk_name=None):
+        
+        if filename in self._image_mapping:
+            filename = self._image_mapping[filename]
+        
+        # if path is relative then interpret it as living in res folder
+        if not os.path.isabs(filename):
+            filename = os.path.join(self.get_package_dir(), "res", filename)
+            
+        img = tk.PhotoImage(tk_name, file=filename)
+        self._images.add(img)
+        return img
+                      
+    def show_view(self, view_id, set_focus=True):
+        """View must be already registered.
+        
+        Args:
+            view_id: View class name 
+            without package name (eg. 'ShellView') """
+
+        # NB! Don't forget that view.home_widget is added to notebook, not view directly
+        # get or create
+        view = self.get_view(view_id)
+        notebook = view.home_widget.master
+        
+        if hasattr(view, "before_show") and view.before_show() == False:
+            return False
+            
+        if view.hidden:
+            notebook.insert("auto", view.home_widget, text=self._view_records[view_id]["label"])
+            view.hidden = False
+        
+        # switch to the tab
+        notebook.select(view.home_widget)
+        
+        # add focus
+        if set_focus:
+            view.focus_set()
+        
+        self.set_option("view." + view_id + ".visible", True)
+        self.event_generate("ShowView", view=view, view_id=view_id)
+        return view
+    
+    def hide_view(self, view_id):
+        # NB! Don't forget that view.home_widget is added to notebook, not view directly
+        
+        if "instance" in self._view_records[view_id]:
+            # TODO: handle the case, when view is maximized
+            view = self._view_records[view_id]["instance"]
+            
+            if hasattr(view, "before_hide") and view.before_hide() == False:
+                return False
+            
+            view.home_widget.master.forget(view.home_widget)
+            
+            self.set_option("view." + view_id + ".visible", False)
+            
+            self.event_generate("HideView", view=view, view_id=view_id)
+            view.hidden = True
+
+        
+
+    def event_generate(self, sequence, **kwargs):
+        """Uses custom event handling when sequence doesn't start with <.
+        In this case arbitrary attributes can be added to the event.
+        Otherwise forwards the call to Tk's event_generate"""
+        if sequence.startswith("<"):
+            tk.Tk.event_generate(self, sequence, **kwargs)
+        else:
+            if sequence in self._event_handlers:
+                for handler in self._event_handlers[sequence]:
+                    try:
+                        # Yes, I'm creating separate event object for each handler
+                        # so that they can't misuse the mutabilty
+                        event = WorkbenchEvent(sequence, **kwargs)
+                        handler(event)
+                    except:
+                        self.report_exception("Problem when handling '" + sequence + "'")
+                
+    def bind(self, sequence, func, add=None):
+        """Uses custom event handling when sequence doesn't start with <.
+        Otherwise forwards the call to Tk's bind"""
+        
+        if not add:
+            logging.warning("Workbench.bind({}, ..., add={}) -- did you really want to replace existing bindings?".format(sequence, add))
+        
+        if sequence.startswith("<"):
+            tk.Tk.bind(self, sequence, func, add)
+        else:
+            if sequence not in self._event_handlers or not add:
+                self._event_handlers[sequence] = set()
+                
+            self._event_handlers[sequence].add(func)
+
+    def unbind(self, sequence, funcid=None):
+        if sequence.startswith("<"):
+            tk.Tk.unbind(self, sequence, funcid=funcid)
+        else:
+            if (sequence in self._event_handlers 
+                and funcid in self._event_handlers[sequence]):
+                self._event_handlers[sequence].remove(funcid)
+                
+
+    def in_heap_mode(self):
+        # TODO: add a separate command for enabling the heap mode 
+        # untie the mode from HeapView
+        
+        return (self._configuration_manager.has_option("view.HeapView.visible")
+            and self.get_option("view.HeapView.visible"))
+    
+    def update_fonts(self):
+        editor_font_size = self._guard_font_size(self.get_option("view.editor_font_size"))
+        editor_font_family = self.get_option("view.editor_font_family")
+        io_font_family = self.get_option("view.io_font_family")
+        
+        self.get_font("IOFont").configure(family=io_font_family,
+                                          size=min(editor_font_size - 2,
+                                                   int(editor_font_size * 0.8 + 3)))
+        self.get_font("EditorFont").configure(family=editor_font_family,
+                                              size=editor_font_size)
+        self.get_font("BoldEditorFont").configure(family=editor_font_family,
+                                                  size=editor_font_size)
+        self.get_font("ItalicEditorFont").configure(family=editor_font_family,
+                                                  size=editor_font_size)
+        self.get_font("BoldItalicEditorFont").configure(family=editor_font_family,
+                                                  size=editor_font_size)
+        
+        
+        style = ttk.Style()
+        if running_on_mac_os():
+            treeview_font_size = int(editor_font_size * 0.7 + 4)
+            rowheight = int(treeview_font_size*1.2 + 4 )
+        else:
+            treeview_font_size = int(editor_font_size * 0.7 + 2)
+            rowheight = int(treeview_font_size * 2.0 + 6)
+            
+        self.get_font("TreeviewFont").configure(size=treeview_font_size)
+        style.configure("Treeview", rowheight=rowheight)
+        
+        if self._editor_notebook is not None:
+            self._editor_notebook.update_appearance()
+        
+    
+    def _get_menu_index(self, menu):
+        for i in range(len(self._menubar.winfo_children())):
+            if menu == self._menubar.winfo_children()[i]:
+                return i
+        else:
+            return None
+    
+    def _add_toolbar_button(self, image, command_label, accelerator, handler, 
+                            tester, toolbar_group):
+        
+        slaves = self._toolbar.grid_slaves(0, toolbar_group)
+        if len(slaves) == 0:
+            group_frame = ttk.Frame(self._toolbar)
+            group_frame.grid(row=0, column=toolbar_group, padx=(0, 10))
+        else:
+            group_frame = slaves[0]
+        
+        button = ttk.Button(group_frame, 
+                         command=handler, 
+                         image=image, 
+                         style="Toolbutton", # TODO: does this cause problems in some Macs?
+                         state=tk.NORMAL
+                         )
+        button.pack(side=tk.LEFT)
+        button.tester = tester 
+        tooltip_text = command_label
+        if accelerator and self.get_option("theme.shortcuts_in_tooltips", True):
+            tooltip_text += " (" + accelerator + ")"
+        create_tooltip(button, tooltip_text,
+                       **self.get_option("theme.tooltip_options", {'padx':3, 'pady':1})
+                       )
+        
+    def _update_toolbar(self):
+        for group_frame in self._toolbar.grid_slaves(0):
+            for button in group_frame.pack_slaves():
+                if button.tester and not button.tester():
+                    button["state"] = tk.DISABLED
+                else:
+                    button["state"] = tk.NORMAL
+        
+        self.after(300, self._update_toolbar)
+            
+    
+    def _cmd_zoom_with_mouse(self, event):
+        if event.delta > 0:
+            self._change_font_size(1)
+        else:
+            self._change_font_size(-1)
+    
+    def _change_font_size(self, delta):
+        
+        if delta != 0:
+            editor_font_size = self.get_option("view.editor_font_size")
+            editor_font_size += delta
+            self.set_option("view.editor_font_size", self._guard_font_size(editor_font_size))
+            self.update_fonts()
+    
+    def _guard_font_size(self, size):
+        # https://bitbucket.org/plas/thonny/issues/164/negative-font-size-crashes-thonny
+        MIN_SIZE = 4
+        MAX_SIZE = 200
+        if size < MIN_SIZE:
+            return MIN_SIZE
+        elif size > MAX_SIZE:
+            return MAX_SIZE
+        else:
+            return size
+        
+        
+    
+    def _check_update_window_width(self, delta):
+        if not ui_utils.get_zoomed(self):
+            self.update_idletasks()
+            # TODO: shift to left if right edge goes away from screen
+            # TODO: check with screen width
+            new_geometry = "{0}x{1}+{2}+{3}".format(self.winfo_width() + delta,
+                                                   self.winfo_height(),
+                                                   self.winfo_x(), self.winfo_y())
+            
+            self.geometry(new_geometry)
+            
+    
+    def _maximize_view(self, event=None):
+        if self._maximized_view is not None:
+            return
+        
+        # find the widget that can be relocated
+        widget = self.focus_get()
+        if isinstance(widget, EditorNotebook) or isinstance(widget, AutomaticNotebook):
+            current_tab = get_current_notebook_tab_widget(widget)
+            if current_tab is None:
+                return
+            
+            if not hasattr(current_tab, "maximizable_widget"):
+                return
+            
+            widget = current_tab.maximizable_widget
+        
+        while widget is not None:
+            if hasattr(widget, "home_widget"):
+                # if widget is view, then widget.master is workbench
+                widget.grid(row=0, column=0, sticky=tk.NSEW, in_=widget.master)
+                # hide main_frame
+                self._main_frame.grid_forget()
+                self._maximized_view = widget
+                self.get_variable("view.maximize_view").set(True)
+                break
+            else:
+                widget = widget.master
+    
+    def _unmaximize_view(self, event=None):
+        if self._maximized_view is None:
+            return
+        
+        # restore main_frame
+        self._main_frame.grid(row=0, column=0, sticky=tk.NSEW, in_=self)
+        # put the maximized view back to its home_widget
+        self._maximized_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self._maximized_view.home_widget)
+        self._maximized_view = None
+        self.get_variable("view.maximize_view").set(False)
+    
+    def _cmd_show_options(self):
+        dlg = ConfigurationDialog(self, self._configuration_pages)
+        dlg.focus_set()
+        dlg.transient(self)
+        dlg.grab_set()
+        self.wait_window(dlg)
+    
+    def _cmd_focus_editor(self):
+        self._editor_notebook.focus_set()
+    
+    def _cmd_focus_shell(self):
+        self.show_view("ShellView", True)
+    
+    def _cmd_toggle_full_screen(self):
+        var = self.get_variable("view.full_screen")
+        var.set(not var.get())
+        self.attributes("-fullscreen", var.get())
+    
+    def _cmd_toggle_maximize_view(self):
+        if self._maximized_view is not None:
+            self._unmaximize_view()
+        else:
+            self._maximize_view()
+            
+            
+    
+    def _update_menu(self, menu, menu_name):
+        if menu.index("end") == None:
+            return
+        
+        for i in range(menu.index("end")+1):
+            item_data = menu.entryconfigure(i)
+            if "label" in item_data:
+                command_label = menu.entrycget(i, "label")
+                tester = self._menu_item_testers[(menu_name, command_label)]
+
+                if tester and not tester():
+                    menu.entryconfigure(i, state=tk.DISABLED)
+                else:
+                    menu.entryconfigure(i, state=tk.ACTIVE)   
+                    
+                    
+        
+    
+    def _find_location_for_menu_item(self, menu_name, command_label, group,
+            position_in_group="end"):        
+        
+        menu = self.get_menu(menu_name)
+        
+        if menu.index("end") == None: # menu is empty
+            return "end"
+        
+        this_group_exists = False
+        for i in range(0, menu.index("end")+1):
+            data = menu.entryconfigure(i)
+            if "label" in data:
+                # it's a command, not separator
+                sibling_label = menu.entrycget(i, "label")
+                sibling_group = self._menu_item_groups[(menu_name, sibling_label)]
+
+                if sibling_group == group:
+                    this_group_exists = True
+                    if position_in_group == "alphabetic" and sibling_label > command_label:
+                        return i
+                    
+                if sibling_group > group:
+                    assert not this_group_exists # otherwise we would have found the ending separator
+                    menu.insert_separator(i)
+                    return i
+            else:
+                # We found a separator
+                if this_group_exists: 
+                    # it must be the ending separator for this group
+                    return i
+                
+        else:
+            # no group was bigger, ie. this should go to the end
+            if not this_group_exists:
+                menu.add_separator()
+                
+            return "end"
+
+    def _handle_socket_request(self, client_socket):
+        """runs in separate thread"""
+        # read the request
+        data = bytes()
+        while True:
+            new_data = client_socket.recv(1024)
+            if len(new_data) > 0:
+                data += new_data
+            else:
+                break
+        
+        self._requests_from_socket.put(data)
+        
+        # respond OK
+        client_socket.sendall(SERVER_SUCCESS.encode(encoding='utf-8'))
+        client_socket.shutdown(socket.SHUT_WR)
+        print("AFTER NEW REQUEST", client_socket)
+    
+    def _poll_socket_requests(self):
+        """runs in gui thread"""
+        try:
+            while not self._requests_from_socket.empty():
+                data = self._requests_from_socket.get()
+                args = ast.literal_eval(data.decode("UTF-8"))
+                assert isinstance(args, list)
+                for filename in args:
+                    if os.path.exists(filename):
+                        self._editor_notebook.show_file(filename)
+                        
+                self.become_topmost_window()
+        finally:
+            self.after(50, self._poll_socket_requests)
+
+    def _on_close(self):
+        if not self._editor_notebook.check_allow_closing():
+            return
+        
+        try:
+            self._save_layout()
+            #ui_utils.delete_images()
+            self.event_generate("WorkbenchClose")
+        except:
+            self.report_exception()
+
+        self.destroy()
+    
+    def focus_get(self):
+        try:
+            return tk.Tk.focus_get(self)
+        except:
+            # This may give error in Ubuntu
+            return None
+    
+    def destroy(self):
+        try:
+            self._destroying = True
+            tk.Tk.destroy(self)
+        except tk.TclError:
+            logging.exception("Error while destroying workbench")
+        finally:
+            runner = get_runner()
+            if runner != None:
+                runner.kill_backend()
+    
+    def _on_configure(self, event):
+        # called when window is moved or resized
+        if (hasattr(self, "_maximized_view") # configure may happen before the attribute is defined 
+            and self._maximized_view):
+            # grid again, otherwise it acts weird
+            self._maximized_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self._maximized_view.master)
+    
+    def _on_tk_exception(self, exc, val, tb):
+        # copied from tkinter.Tk.report_callback_exception with modifications
+        # see http://bugs.python.org/issue22384
+        sys.last_type = exc
+        sys.last_value = val
+        sys.last_traceback = tb
+        self.report_exception()
+    
+    def report_exception(self, title="Internal error"):
+        logging.exception(title)
+        if tk._default_root and not self._destroying:
+            (typ, value, _) = sys.exc_info()
+            if issubclass(typ, UserError):
+                msg = str(value)
+            else:
+                msg = traceback.format_exc()
+            tk_messagebox.showerror(title, msg)
+    
+    def _open_views(self):
+        for nb_name in self._view_notebooks:
+            view_name = self.get_option("layout.notebook_" + nb_name + "_visible_view")
+            if view_name != None:
+                self.show_view(view_name)
+                
+        
+        
+    def _save_layout(self):
+        self.update_idletasks()
+        
+        self.set_option("layout.zoomed", ui_utils.get_zoomed(self))
+        
+        # each AutomaticPanedWindow remember it's splits for both 2 and 3 panes
+        self.set_option("layout.main_pw_first_pane_size", self._main_pw.first_pane_size)
+        self.set_option("layout.main_pw_last_pane_size", self._main_pw.last_pane_size)
+        self.set_option("layout.east_pw_first_pane_size", self._east_pw.first_pane_size)
+        self.set_option("layout.east_pw_last_pane_size", self._east_pw.last_pane_size)
+        self.set_option("layout.center_pw_last_pane_size", self._center_pw.last_pane_size)
+        self.set_option("layout.west_pw_first_pane_size", self._west_pw.first_pane_size)
+        self.set_option("layout.west_pw_last_pane_size", self._west_pw.last_pane_size)
+        
+        for nb_name in self._view_notebooks:
+            widget = self._view_notebooks[nb_name].get_visible_child()
+            if hasattr(widget, "maximizable_widget"):
+                view = widget.maximizable_widget
+                view_name = type(view).__name__
+                self.set_option("layout.notebook_" + nb_name + "_visible_view", view_name)
+            else:
+                self.set_option("layout.notebook_" + nb_name + "_visible_view", None)
+        
+        if not ui_utils.get_zoomed(self):
+            self.set_option("layout.top", self.winfo_y())
+            self.set_option("layout.left", self.winfo_x())
+            self.set_option("layout.width", self.winfo_width())
+            self.set_option("layout.height", self.winfo_height())
+        
+        self._configuration_manager.save()
+    
+    #def focus_set(self):
+    #    tk.Tk.focus_set(self)
+    #    self._editor_notebook.focus_set()
+    
+    def update_title(self, event=None):
+        editor = self.get_editor_notebook().get_current_editor()
+        title_text = "Thonny"
+        if editor != None:
+            title_text += "  -  " + editor.get_long_description()
+            
+        self.title(title_text)
+    
+    def become_topmost_window(self):
+        # Looks like at least on Windows all following is required for the window to get focus
+        # (deiconify, ..., iconify, deiconify)
+        self.deiconify()
+        self.attributes('-topmost', True)
+        self.after_idle(self.attributes, '-topmost', False)
+        self.lift()
+        
+        if not running_on_linux():
+            # http://stackoverflow.com/a/13867710/261181
+            self.iconify()
+            self.deiconify()
+        
+        editor = self.get_current_editor()
+        if editor is not None:
+            # This method is meant to be called when new file is opened, so it's safe to 
+            # send the focus to the editor
+            editor.focus_set()
+        else:
+            self.focus_set()
+        
+
+class WorkbenchEvent(Record):
+    def __init__(self, sequence, **kwargs):
+        Record.__init__(self, **kwargs)
+        self.sequence = sequence
+

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-edu/pkg-team/thonny.git



More information about the debian-edu-commits mailing list