diff --git a/python/find_and_replace_dialog_wrapper.py b/python/find_and_replace_dialog_wrapper.py
index 377f319327e68e876b5f078b46b66a151b0e7680..cf9084a56cc778e1c0ee921a748745d0eda27561 100644
--- a/python/find_and_replace_dialog_wrapper.py
+++ b/python/find_and_replace_dialog_wrapper.py
@@ -12,7 +12,6 @@ from find_and_replace_dialog import Ui_FindAndReplaceDialog
 from color_selector_widget import ColorSelectorWidget
 from paths_gryphon import SNAPSHOTS_PATH
 from settings_manager import settings
-from message_popup import message_popup
 from mp_code_dynamic_tokens import dynamic_token_names
 
 class FindAndReplaceDialogWrapper(QDialog):
diff --git a/python/gui_manager.py b/python/gui_manager.py
index e9749a1a963360b2f99c9a198cc573e8394237a8..bce7f20a01791b82722ec2a09cc53011d69ecdbe 100644
--- a/python/gui_manager.py
+++ b/python/gui_manager.py
@@ -161,11 +161,9 @@ class GUIManager(QObject):
                                self.graphs_manager.signal_graphs_loaded,
                                          self.preferences_manager)
 
-        # persistent wrapper
+        # persistent dialog
         self.search_mp_files_dialog_wrapper = SearchMPFilesDialogWrapper(
                      self.w, self.preferences_manager, self.settings_manager)
-        # zzzz
-        self.search_mp_files_dialog_wrapper.show()
 
         # setup
         self.define_actions()
diff --git a/python/mp_code_view.py b/python/mp_code_view.py
index aad013e70fc6469f6940ffb5f84807538014f7fe..62a87423140ca97eb7bf20f1c17f8677f0f82032 100644
--- a/python/mp_code_view.py
+++ b/python/mp_code_view.py
@@ -2,9 +2,12 @@
 # https://stackoverflow.com/questions/33243852/codeeditor-example-in-pyqt
 # http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
 
+from os.path import join, split
 from PyQt5.QtWidgets import QWidget
 from PyQt5.QtWidgets import QPlainTextEdit
 from PyQt5.QtWidgets import QCompleter
+from PyQt5.QtWidgets import QAction
+from PyQt5.QtWidgets import QFileDialog
 from PyQt5.QtGui import QFont
 from PyQt5.QtGui import QFontMetrics
 from PyQt5.QtGui import QColor
@@ -12,6 +15,7 @@ from PyQt5.QtGui import QTextCursor
 from PyQt5.QtGui import QPainter
 from PyQt5.QtGui import QKeyEvent
 from PyQt5.QtGui import QTextOption
+from PyQt5.QtGui import QIcon
 from PyQt5.QtCore import QSize
 from PyQt5.QtCore import Qt
 from PyQt5.QtCore import QRect
@@ -23,6 +27,9 @@ from mp_code_expressions import KEYWORD_SET, WORD_EXPRESSION, \
                                 LEADING_WHITESPACE_EXPRESSION
 from find_and_replace import FindAndReplace
 from find_and_replace_dialog_wrapper import FindAndReplaceDialogWrapper
+from mp_json_io_manager import save_mp_code_file
+from preferences_manager import cached_preferences
+from message_popup import message_popup
 
 # the set of words and the number of words
 def words(text):
@@ -70,6 +77,12 @@ class MPCodeView(QPlainTextEdit):
                                       self.set_use_auto_indent)
         preferences_manager.signal_use_dark_mode_changed.connect(
                                       self.set_use_dark_mode)
+        self.copyAvailable.connect(self._copy_available)
+
+        # action
+        self.action_save_selection = QAction(QIcon(":/icons/save"),
+                                                "&Save selection...", self)
+        self.action_save_selection.triggered.connect(self._save_selection)
 
         # the cursor highlighter manages cursor highlighting on mouse click
         self.mp_code_cursor_highlighter = MPCodeCursorHighlighter(self)
@@ -112,6 +125,47 @@ class MPCodeView(QPlainTextEdit):
         self.completer.setCompletionMode(QCompleter.PopupCompletion)
         self.completer.activated.connect(self.insert_completion_text)
 
+        # state
+        self.copy_is_available = False
+
+    # copy available
+    @pyqtSlot(bool)
+    def _copy_available(self, copy_is_available):
+        self.copy_is_available = copy_is_available
+
+    # save selection
+    @pyqtSlot()
+    def _save_selection(self):
+        options = QFileDialog.Options()
+        options |= QFileDialog.DontUseNativeDialog
+
+        # suggested filename
+        suggested_filename = "selection.mp"
+
+        mp_code_filename, _ = QFileDialog.getSaveFileName(None,
+                             "Save MP Code selection",
+                             join(cached_preferences[
+                               "preferred_save_selection_dir"],
+                             suggested_filename),
+                             "MP Code files (*.mp);;All Files (*)",
+                             options=options)
+
+        if mp_code_filename:
+            # remember the preferred path
+            head, _tail = split(mp_code_filename)
+            cached_preferences["preferred_save_selection_dir"] = head
+
+            # get the selection
+            selected_text = self.textCursor().selectedText()
+            status = save_mp_code_file(selected_text, mp_code_filename)
+            print("Saved selection to", mp_code_filename)
+            if status:
+                # log failure
+                message_popup(None, status)
+            else:
+                # great, exported.
+                pass
+
     # standard menu plus custom items
     def contextMenuEvent(self, context_menu_event):
         menu = self.createStandardContextMenu()
@@ -119,6 +173,8 @@ class MPCodeView(QPlainTextEdit):
         menu.addAction(self.find_and_replace.action_find_and_replace)
         self.find_and_replace.action_find_and_replace.setDisabled(
                    FindAndReplaceDialogWrapper.a_wrapper_instance_is_visible)
+        menu.addAction(self.action_save_selection)
+        self.action_save_selection.setDisabled(not self.copy_is_available)
         menu.exec_(context_menu_event.globalPos())
         del menu
 
diff --git a/python/search_mp_files_dialog_wrapper.py b/python/search_mp_files_dialog_wrapper.py
index 194707dde4f722646507ab25aa49b6ea67a3699c..5d5f32f422df187a4995e21c0c66d4481768e508 100644
--- a/python/search_mp_files_dialog_wrapper.py
+++ b/python/search_mp_files_dialog_wrapper.py
@@ -12,10 +12,9 @@ from PyQt5.QtWidgets import QAction
 from search_mp_files_dialog import Ui_SearchMPFilesDialog
 from paths_examples import examples_paths
 from preferences_manager import cached_preferences
-from message_popup import message_popup
 from mp_code_dynamic_tokens import dynamic_token_names
 from search_mp_code_dialog_wrapper import SearchMPCodeDialogWrapper
-from message_popup import message_popup
+from startup_options import verbose
 
 class FileMetadata():
     """Properties:
@@ -144,7 +143,8 @@ class FileMetadataTableView(QTableView):
 def _append_library(library_path, file_metadata_list):
     p = Path(library_path)
     posix_paths = sorted(p.rglob("*.mp"))
-    print("Reading library %s count %d"%(library_path, len(posix_paths)))
+    if verbose():
+        print("Reading library %s count %d"%(library_path, len(posix_paths)))
     for posix_path in posix_paths:
         if posix_path.is_file():
             file_metadata_list.append(FileMetadata(posix_path))
@@ -258,7 +258,7 @@ class SearchMPFilesDialogWrapper(QDialog):
         row = self.proxy_model.mapToSource(model_index).row()
         posix_path = self.file_metadata_table_model.file_metadata_list[row]\
                                                              .posix_path
-        print("_select_index", posix_path)
+        print("Open search", posix_path)
         wrapper = SearchMPCodeDialogWrapper(self.parent,
                                             self.preferences_manager,
                                             self.settings_manager,