diff --git a/app/src/cc/arduino/packages/formatter/AStyle.java b/app/src/cc/arduino/packages/formatter/AStyle.java
index 7f7c244d6f6..70b6717ff66 100644
--- a/app/src/cc/arduino/packages/formatter/AStyle.java
+++ b/app/src/cc/arduino/packages/formatter/AStyle.java
@@ -33,10 +33,8 @@
 import processing.app.BaseNoGui;
 import processing.app.Editor;
 import processing.app.helpers.FileUtils;
-import processing.app.syntax.SketchTextArea;
 import processing.app.tools.Tool;
 
-import javax.swing.text.BadLocationException;
 import java.io.File;
 import java.io.IOException;
 
@@ -78,7 +76,7 @@ public void init(Editor editor) {
 
   @Override
   public void run() {
-    String originalText = editor.getText();
+    String originalText = editor.getCurrentTab().getText();
     String formattedText = aStyleInterface.AStyleMain(originalText, formatterConfiguration);
 
     if (formattedText.equals(originalText)) {
@@ -86,57 +84,12 @@ public void run() {
       return;
     }
 
-    SketchTextArea textArea = editor.getTextArea();
-
-    int line = getLineOfOffset(textArea);
-    int lineOffset = getLineOffset(textArea, line);
-
-    textArea.getUndoManager().beginInternalAtomicEdit();
-    editor.removeAllLineHighlights();
-    editor.setText(formattedText);
-    editor.getSketch().setModified(true);
-    textArea.getUndoManager().endInternalAtomicEdit();
-
-    if (line != -1 && lineOffset != -1) {
-      try {
-        setCaretPosition(textArea, line, lineOffset);
-      } catch (BadLocationException e) {
-        e.printStackTrace();
-      }
-    }
+    editor.getCurrentTab().setText(formattedText);
 
     // mark as finished
     editor.statusNotice(tr("Auto Format finished."));
   }
 
-  private void setCaretPosition(SketchTextArea textArea, int line, int lineOffset) throws BadLocationException {
-    int caretPosition;
-    if (line < textArea.getLineCount()) {
-      caretPosition = Math.min(textArea.getLineStartOffset(line) + lineOffset, textArea.getLineEndOffset(line) - 1);
-    } else {
-      caretPosition = textArea.getText().length() - 1;
-    }
-    textArea.setCaretPosition(caretPosition);
-  }
-
-  private int getLineOffset(SketchTextArea textArea, int line) {
-    try {
-      return textArea.getCaretPosition() - textArea.getLineStartOffset(line);
-    } catch (BadLocationException e) {
-      e.printStackTrace();
-    }
-    return -1;
-  }
-
-  private int getLineOfOffset(SketchTextArea textArea) {
-    try {
-      return textArea.getLineOfOffset(textArea.getCaretPosition());
-    } catch (BadLocationException e) {
-      e.printStackTrace();
-    }
-    return -1;
-  }
-
   @Override
   public String getMenuTitle() {
     return tr("Auto Format");
diff --git a/app/src/cc/arduino/view/GoToLineNumber.java b/app/src/cc/arduino/view/GoToLineNumber.java
index 3a3bc6fcaa9..475b0bbe502 100644
--- a/app/src/cc/arduino/view/GoToLineNumber.java
+++ b/app/src/cc/arduino/view/GoToLineNumber.java
@@ -127,7 +127,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) {
   private void okActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okActionPerformed
     try {
       int line = Integer.parseInt(lineNumber.getText());
-      editor.goToLine(line);
+      editor.getCurrentTab().goToLine(line);
       cancelActionPerformed(evt);
     } catch (Exception e) {
       // ignore
diff --git a/app/src/cc/arduino/view/findreplace/FindReplace.java b/app/src/cc/arduino/view/findreplace/FindReplace.java
index cffb6099c71..1b40e1b12a3 100644
--- a/app/src/cc/arduino/view/findreplace/FindReplace.java
+++ b/app/src/cc/arduino/view/findreplace/FindReplace.java
@@ -31,7 +31,6 @@
 
 import processing.app.Base;
 import processing.app.Editor;
-import processing.app.Sketch;
 import processing.app.helpers.OSUtils;
 
 import java.awt.*;
@@ -292,7 +291,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or
       return false;
     }
 
-    String text = editor.getText();
+    String text = editor.getCurrentTab().getText();
 
     if (ignoreCaseBox.isSelected()) {
       search = search.toLowerCase();
@@ -302,7 +301,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or
     int nextIndex;
     if (!backwards) {
       // int selectionStart = editor.textarea.getSelectionStart();
-      int selectionEnd = editor.getSelectionStop();
+      int selectionEnd = editor.getCurrentTab().getSelectionStop();
 
       nextIndex = text.indexOf(search, selectionEnd);
       if (wrap && nextIndex == -1) {
@@ -311,7 +310,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or
       }
     } else {
       // int selectionStart = editor.textarea.getSelectionStart();
-      int selectionStart = editor.getSelectionStart() - 1;
+      int selectionStart = editor.getCurrentTab().getSelectionStart() - 1;
 
       if (selectionStart >= 0) {
         nextIndex = text.lastIndexOf(search, selectionStart);
@@ -327,10 +326,9 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or
     if (nextIndex == -1) {
       // Nothing found on this tab: Search other tabs if required
       if (searchTabs) {
-        // editor.
-        Sketch sketch = editor.getSketch();
-        if (sketch.getCodeCount() > 1) {
-          int realCurrentTab = sketch.getCodeIndex(sketch.getCurrentCode());
+        int numTabs = editor.getTabs().size();
+        if (numTabs > 1) {
+          int realCurrentTab = editor.getCurrentTabIndex();
 
           if (originTab != realCurrentTab) {
             if (originTab < 0) {
@@ -338,20 +336,21 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or
             }
 
             if (!wrap) {
-              if ((!backwards && realCurrentTab + 1 >= sketch.getCodeCount()) || (backwards && realCurrentTab - 1 < 0)) {
+              if ((!backwards && realCurrentTab + 1 >= numTabs)
+                  || (backwards && realCurrentTab - 1 < 0)) {
                 return false; // Can't continue without wrap
               }
             }
 
             if (backwards) {
-              sketch.handlePrevCode();
+              editor.selectNextTab();
               this.setVisible(true);
-              int l = editor.getText().length() - 1;
-              editor.setSelection(l, l);
+              int l = editor.getCurrentTab().getText().length() - 1;
+              editor.getCurrentTab().setSelection(l, l);
             } else {
-              sketch.handleNextCode();
+              editor.selectPrevTab();
               this.setVisible(true);
-              editor.setSelection(0, 0);
+              editor.getCurrentTab().setSelection(0, 0);
             }
 
             return find(wrap, backwards, true, originTab);
@@ -365,7 +364,7 @@ private boolean find(boolean wrap, boolean backwards, boolean searchTabs, int or
     }
 
     if (nextIndex != -1) {
-      editor.setSelection(nextIndex, nextIndex + search.length());
+      editor.getCurrentTab().setSelection(nextIndex, nextIndex + search.length());
       return true;
     }
 
@@ -381,18 +380,17 @@ private void replace() {
       return;
     }
 
-    int newpos = editor.getSelectionStart() - findField.getText().length();
+    int newpos = editor.getCurrentTab().getSelectionStart() - findField.getText().length();
     if (newpos < 0) {
       newpos = 0;
     }
-    editor.setSelection(newpos, newpos);
+    editor.getCurrentTab().setSelection(newpos, newpos);
 
     boolean foundAtLeastOne = false;
 
     if (find(false, false, searchAllFilesBox.isSelected(), -1)) {
       foundAtLeastOne = true;
-      editor.setSelectedText(replaceField.getText());
-      editor.getSketch().setModified(true); // TODO is this necessary?
+      editor.getCurrentTab().setSelectedText(replaceField.getText());
     }
 
     if (!foundAtLeastOne) {
@@ -420,17 +418,16 @@ private void replaceAll() {
     }
 
     if (searchAllFilesBox.isSelected()) {
-      editor.getSketch().setCurrentCode(0); // select the first tab
+      editor.selectTab(0); // select the first tab
     }
 
-    editor.setSelection(0, 0); // move to the beginning
+    editor.getCurrentTab().setSelection(0, 0); // move to the beginning
 
     boolean foundAtLeastOne = false;
     while (true) {
       if (find(false, false, searchAllFilesBox.isSelected(), -1)) {
         foundAtLeastOne = true;
-        editor.setSelectedText(replaceField.getText());
-        editor.getSketch().setModified(true); // TODO is this necessary?
+        editor.getCurrentTab().setSelectedText(replaceField.getText());
       } else {
         break;
       }
diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index f7af1aa3359..f74f454bee5 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -583,13 +583,13 @@ protected void storeSketches() {
     PreferencesData.setInteger("last.sketch.count", index);
   }
 
-  protected void storeRecentSketches(Sketch sketch) {
+  protected void storeRecentSketches(SketchController sketch) {
     if (sketch.isUntitled()) {
       return;
     }
 
     Set<String> sketches = new LinkedHashSet<String>();
-    sketches.add(sketch.getMainFilePath());
+    sketches.add(sketch.getSketch().getMainFilePath());
     sketches.addAll(PreferencesData.getCollection("recent.sketches"));
 
     PreferencesData.setCollection("recent.sketches", sketches);
@@ -610,13 +610,13 @@ protected void handleActivated(Editor whichEditor) {
     activeEditor.rebuildRecentSketchesMenu();
     if (PreferencesData.getBoolean("editor.external")) {
       try {
-        int previousCaretPosition = activeEditor.getTextArea().getCaretPosition();
-        activeEditor.getSketch().load(true);
-        if (previousCaretPosition < activeEditor.getText().length()) {
-          activeEditor.getTextArea().setCaretPosition(previousCaretPosition);
-        }
+        // If the list of files on disk changed, recreate the tabs for them
+        if (activeEditor.getSketch().reload())
+          activeEditor.createTabs();
+        else // Let the current tab know it was activated, so it can reload
+          activeEditor.getCurrentTab().activated();
       } catch (IOException e) {
-        // noop
+        System.err.println(e);
       }
     }
 
@@ -760,50 +760,6 @@ public void handleNew() throws Exception {
   }
 
 
-  /**
-   * Replace the sketch in the current window with a new untitled document.
-   */
-  public void handleNewReplace() {
-    if (!activeEditor.checkModified()) {
-      return;  // sketch was modified, and user canceled
-    }
-    // Close the running window, avoid window boogers with multiple sketches
-    activeEditor.internalCloseRunner();
-
-    // Actually replace things
-    handleNewReplaceImpl();
-  }
-
-
-  protected void handleNewReplaceImpl() {
-    try {
-      File file = createNewUntitled();
-      if (file != null) {
-        activeEditor.handleOpenInternal(file);
-        activeEditor.untitled = true;
-      }
-
-    } catch (IOException e) {
-      activeEditor.statusError(e);
-    }
-  }
-
-
-  public void handleOpenReplace(File file) {
-    if (!activeEditor.checkModified()) {
-      return;  // sketch was modified, and user canceled
-    }
-    // Close the running window, avoid window boogers with multiple sketches
-    activeEditor.internalCloseRunner();
-
-    boolean loaded = activeEditor.handleOpenInternal(file);
-    if (!loaded) {
-      // replace the document without checking if that's ok
-      handleNewReplaceImpl();
-    }
-  }
-
-
   /**
    * Prompt for a sketch to open, and open it in a new window.
    *
@@ -865,9 +821,8 @@ protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocati
     if (!file.exists()) return null;
 
     // Cycle through open windows to make sure that it's not already open.
-    String path = file.getAbsolutePath();
     for (Editor editor : editors) {
-      if (editor.getSketch().getMainFilePath().equals(path)) {
+      if (editor.getSketch().getPrimaryFile().getFile().equals(file)) {
         editor.toFront();
         return editor;
       }
@@ -876,7 +831,7 @@ protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocati
     Editor editor = new Editor(this, file, storedLocation, defaultLocation, BaseNoGui.getPlatform());
 
     // Make sure that the sketch actually loaded
-    if (editor.getSketch() == null) {
+    if (editor.getSketchController() == null) {
       return null;  // Just walk away quietly
     }
 
@@ -888,7 +843,7 @@ protected Editor handleOpen(File file, int[] storedLocation, int[] defaultLocati
       // Store information on who's open and running
       // (in case there's a crash or something that can't be recovered)
       storeSketches();
-      storeRecentSketches(editor.getSketch());
+      storeRecentSketches(editor.getSketchController());
       rebuildRecentSketchesMenuItems();
       PreferencesData.save();
     }
@@ -957,9 +912,6 @@ public boolean handleClose(Editor editor) {
       return false;
     }
 
-    // Close the running window, avoid window boogers with multiple sketches
-    editor.internalCloseRunner();
-
     if (editors.size() == 1) {
       // This will store the sketch count as zero
       editors.remove(editor);
@@ -1014,10 +966,6 @@ public boolean handleQuit() {
     }
 
     if (handleQuitEach()) {
-      // make sure running sketches close before quitting
-      for (Editor editor : editors) {
-        editor.internalCloseRunner();
-      }
       // Save out the current prefs state
       PreferencesData.save();
 
@@ -1184,7 +1132,7 @@ public void actionPerformed(ActionEvent e) {
           public void actionPerformed(ActionEvent event) {
             UserLibrary l = (UserLibrary) getValue("library");
             try {
-              activeEditor.getSketch().importLibrary(l);
+              activeEditor.getSketchController().importLibrary(l);
             } catch (IOException e) {
               showWarning(tr("Error"), I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e);
             }
@@ -1719,7 +1667,7 @@ protected void addLibraries(JMenu menu, LibraryList libs) throws IOException {
         public void actionPerformed(ActionEvent event) {
           UserLibrary l = (UserLibrary) getValue("library");
           try {
-            activeEditor.getSketch().importLibrary(l);
+            activeEditor.getSketchController().importLibrary(l);
           } catch (IOException e) {
             showWarning(tr("Error"), I18n.format("Unable to list header files in {0}", l.getSrcFolder()), e);
           }
@@ -2174,54 +2122,6 @@ static public void saveFile(String str, File file) throws IOException {
   }
 
 
-  /**
-   * Copy a folder from one place to another. This ignores all dot files and
-   * folders found in the source directory, to avoid copying silly .DS_Store
-   * files and potentially troublesome .svn folders.
-   */
-  static public void copyDir(File sourceDir,
-                             File targetDir) throws IOException {
-    targetDir.mkdirs();
-    String files[] = sourceDir.list();
-    if (files == null) {
-      throw new IOException("Unable to list files from " + sourceDir);
-    }
-    for (String file : files) {
-      // Ignore dot files (.DS_Store), dot folders (.svn) while copying
-      if (file.charAt(0) == '.') continue;
-      //if (files[i].equals(".") || files[i].equals("..")) continue;
-      File source = new File(sourceDir, file);
-      File target = new File(targetDir, file);
-      if (source.isDirectory()) {
-        //target.mkdirs();
-        copyDir(source, target);
-        target.setLastModified(source.lastModified());
-      } else {
-        copyFile(source, target);
-      }
-    }
-  }
-
-
-  /**
-   * Remove all files in a directory and the directory itself.
-   */
-  static public void removeDir(File dir) {
-    BaseNoGui.removeDir(dir);
-  }
-
-
-  /**
-   * Recursively remove all files within a directory,
-   * used with removeDir(), or when the contents of a dir
-   * should be removed, but not the directory itself.
-   * (i.e. when cleaning temp files from lib/build)
-   */
-  static public void removeDescendants(File dir) {
-    BaseNoGui.removeDescendants(dir);
-  }
-
-
   /**
    * Calculate the size of the contents of a folder.
    * Used to determine whether sketches are empty or not.
@@ -2247,48 +2147,6 @@ static public int calcFolderSize(File folder) {
     return size;
   }
 
-
-  /**
-   * Recursively creates a list of all files within the specified folder,
-   * and returns a list of their relative paths.
-   * Ignores any files/folders prefixed with a dot.
-   */
-  static public String[] listFiles(String path, boolean relative) {
-    return listFiles(new File(path), relative);
-  }
-
-
-  static public String[] listFiles(File folder, boolean relative) {
-    String path = folder.getAbsolutePath();
-    Vector<String> vector = new Vector<String>();
-    listFiles(relative ? (path + File.separator) : "", path, vector);
-    String outgoing[] = new String[vector.size()];
-    vector.copyInto(outgoing);
-    return outgoing;
-  }
-
-
-  static protected void listFiles(String basePath,
-                                  String path, Vector<String> vector) {
-    File folder = new File(path);
-    String list[] = folder.list();
-    if (list == null) return;
-
-    for (int i = 0; i < list.length; i++) {
-      if (list[i].charAt(0) == '.') continue;
-
-      File file = new File(path, list[i]);
-      String newPath = file.getAbsolutePath();
-      if (newPath.startsWith(basePath)) {
-        newPath = newPath.substring(basePath.length());
-      }
-      vector.add(newPath);
-      if (file.isDirectory()) {
-        listFiles(basePath, newPath, vector);
-      }
-    }
-  }
-
   public void handleAddLibrary() {
     JFileChooser fileChooser = new JFileChooser(System.getProperty("user.home"));
     fileChooser.setDialogTitle(tr("Select a zip file or a folder containing the library you'd like to add"));
diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java
index 2f7b5bfdf79..b5f96aef2b1 100644
--- a/app/src/processing/app/Editor.java
+++ b/app/src/processing/app/Editor.java
@@ -31,30 +31,19 @@
 import cc.arduino.view.findreplace.FindReplace;
 import com.jcraft.jsch.JSchException;
 import jssc.SerialPortException;
-import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
-import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
-import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
-import org.fife.ui.rtextarea.Gutter;
-import org.fife.ui.rtextarea.RTextScrollPane;
 import processing.app.debug.RunnerException;
 import processing.app.forms.PasswordAuthorizationDialog;
 import processing.app.helpers.Keys;
 import processing.app.helpers.OSUtils;
 import processing.app.helpers.PreferencesMapException;
 import processing.app.legacy.PApplet;
-import processing.app.syntax.ArduinoTokenMakerFactory;
 import processing.app.syntax.PdeKeywords;
-import processing.app.syntax.SketchTextArea;
-import processing.app.syntax.SketchTextAreaEditorKit;
-import processing.app.tools.DiscourseFormat;
 import processing.app.tools.MenuScroller;
 import processing.app.tools.Tool;
 
 import javax.swing.*;
-import javax.swing.border.MatteBorder;
 import javax.swing.event.*;
 import javax.swing.text.BadLocationException;
-import javax.swing.text.PlainDocument;
 import javax.swing.undo.CannotRedoException;
 import javax.swing.undo.CannotUndoException;
 import javax.swing.undo.UndoManager;
@@ -88,22 +77,31 @@ public class Editor extends JFrame implements RunnerListener {
 
   public static final int MAX_TIME_AWAITING_FOR_RESUMING_SERIAL_MONITOR = 10000;
 
-  private final Platform platform;
+  final Platform platform;
   private JMenu recentSketchesMenu;
   private JMenu programmersMenu;
+  private final Box upper;
+  private ArrayList<EditorTab> tabs = new ArrayList<>();
+  private int currentTabIndex = -1;
 
-  private static class ShouldSaveIfModified implements Predicate<Sketch> {
+  private static class ShouldSaveIfModified
+      implements Predicate<SketchController> {
 
     @Override
-    public boolean test(Sketch sketch) {
-      return PreferencesData.getBoolean("editor.save_on_verify") && sketch.isModified() && !sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath());
+    public boolean test(SketchController controller) {
+      return PreferencesData.getBoolean("editor.save_on_verify")
+             && controller.getSketch().isModified()
+             && !controller.isReadOnly(
+                                       BaseNoGui.librariesIndexer
+                                           .getInstalledLibraries(),
+                                       BaseNoGui.getExamplesPath());
     }
   }
 
-  private static class ShouldSaveReadOnly implements Predicate<Sketch> {
+  private static class ShouldSaveReadOnly implements Predicate<SketchController> {
 
     @Override
-    public boolean test(Sketch sketch) {
+    public boolean test(SketchController sketch) {
       return sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath());
     }
   }
@@ -163,14 +161,15 @@ public boolean test(Sketch sketch) {
   private JSplitPane splitPane;
 
   // currently opened program
+  SketchController sketchController;
   Sketch sketch;
 
-  private EditorLineStatus lineStatus;
+  EditorLineStatus lineStatus;
 
   //JEditorPane editorPane;
 
-  private SketchTextArea textarea;
-  private RTextScrollPane scrollPane;
+  /** Contains all EditorTabs, of which only one will be visible */
+  private JPanel codePanel;
 
   //Runner runtime;
 
@@ -192,7 +191,6 @@ public boolean test(Sketch sketch) {
   Runnable presentHandler;
   private Runnable runAndSaveHandler;
   private Runnable presentAndSaveHandler;
-  private Runnable stopHandler;
   Runnable exportHandler;
   private Runnable exportAppHandler;
 
@@ -258,7 +256,7 @@ public void windowDeactivated(WindowEvent e) {
     contentPain.add(pane, BorderLayout.CENTER);
 
     Box box = Box.createVerticalBox();
-    Box upper = Box.createVerticalBox();
+    upper = Box.createVerticalBox();
 
     if (toolbarMenu == null) {
       toolbarMenu = new JMenu();
@@ -270,9 +268,6 @@ public void windowDeactivated(WindowEvent e) {
     header = new EditorHeader(this);
     upper.add(header);
 
-    textarea = createTextArea();
-    textarea.setName("editor");
-
     // assemble console panel, consisting of status area and the console itself
     JPanel consolePanel = new JPanel();
     consolePanel.setLayout(new BorderLayout());
@@ -289,19 +284,9 @@ public void windowDeactivated(WindowEvent e) {
     lineStatus = new EditorLineStatus();
     consolePanel.add(lineStatus, BorderLayout.SOUTH);
 
-    // RTextScrollPane
-    scrollPane = new RTextScrollPane(textarea, true);
-    scrollPane.setBorder(new MatteBorder(0, 6, 0, 0, Theme.getColor("editor.bgcolor")));
-    scrollPane.setViewportBorder(BorderFactory.createEmptyBorder());
-    scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers"));
-    scrollPane.setIconRowHeaderEnabled(false);
-    
-    Gutter gutter = scrollPane.getGutter();
-    gutter.setBookmarkingEnabled(false);
-    //gutter.setBookmarkIcon(CompletionsRenderer.getIcon(CompletionType.TEMPLATE));
-    gutter.setIconRowHeaderInheritsGutterBackground(true);
+    codePanel = new JPanel(new BorderLayout());
+    upper.add(codePanel);
 
-    upper.add(scrollPane);
     splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, upper, consolePanel);
 
     splitPane.setOneTouchExpandable(true);
@@ -337,9 +322,6 @@ public void windowDeactivated(WindowEvent e) {
     // listener = new EditorListener(this, textarea);
     pane.add(box);
 
-    // get shift down/up events so we can show the alt version of toolbar buttons
-    textarea.addKeyListener(toolbar);
-
     pane.setTransferHandler(new FileDropHandler());
 
 //    System.out.println("t1");
@@ -365,7 +347,7 @@ public void windowDeactivated(WindowEvent e) {
 
     // Open the document that was passed in
     boolean loaded = handleOpenInternal(file);
-    if (!loaded) sketch = null;
+    if (!loaded) sketchController = null;
 
 //    System.out.println("t5");
 
@@ -396,7 +378,7 @@ public boolean importData(JComponent src, Transferable transferable) {
           List<File> list = (List<File>)
             transferable.getTransferData(DataFlavor.javaFileListFlavor);
           for (File file : list) {
-            if (sketch.addFile(file)) {
+            if (sketchController.addFile(file)) {
               successful++;
             }
           }
@@ -414,7 +396,7 @@ public boolean importData(JComponent src, Transferable transferable) {
             } else if (piece.startsWith("file:/")) {
               path = piece.substring(5);
             }
-            if (sketch.addFile(new File(path))) {
+            if (sketchController.addFile(new File(path))) {
               successful++;
             }
           }
@@ -493,45 +475,11 @@ protected int[] getPlacement() {
    * with things in the Preferences window.
    */
   public void applyPreferences() {
-
-    // apply the setting for 'use external editor'
     boolean external = PreferencesData.getBoolean("editor.external");
-
-    textarea.setEditable(!external);
     saveMenuItem.setEnabled(!external);
     saveAsMenuItem.setEnabled(!external);
-
-    textarea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding"));
-    scrollPane.setFoldIndicatorEnabled(PreferencesData.getBoolean("editor.code_folding"));
-    scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers"));
-
-    if (external) {
-      // disable line highlight and turn off the caret when disabling
-      textarea.setBackground(Theme.getColor("editor.external.bgcolor"));
-      textarea.setHighlightCurrentLine(false);
-      textarea.setEditable(false);
-
-    } else {
-      textarea.setBackground(Theme.getColor("editor.bgcolor"));
-      textarea.setHighlightCurrentLine(Theme.getBoolean("editor.linehighlight"));
-      textarea.setEditable(true);
-    }
-
-    // apply changes to the font size for the editor
-    //TextAreaPainter painter = textarea.getPainter();
-    textarea.setFont(PreferencesData.getFont("editor.font"));
-    //Font font = painter.getFont();
-    //textarea.getPainter().setFont(new Font("Courier", Font.PLAIN, 36));
-
-    // in case tab expansion stuff has changed
-    // listener.applyPreferences();
-
-    // in case moved to a new location
-    // For 0125, changing to async version (to be implemented later)
-    //sketchbook.rebuildMenus();
-    // For 0126, moved into Base, which will notify all editors.
-    //base.rebuildMenusAsync();
-
+    for (EditorTab tab: tabs)
+      tab.applyPreferences();
   }
 
 
@@ -759,7 +707,7 @@ public void actionPerformed(ActionEvent e) {
     item = newJMenuItemAlt(tr("Export compiled Binary"), 'S');
     item.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          if (new ShouldSaveReadOnly().test(sketch) && !handleSave(true)) {
+          if (new ShouldSaveReadOnly().test(sketchController) && !handleSave(true)) {
             System.out.println(tr("Export canceled, changes must first be saved."));
             return;
           }
@@ -797,7 +745,7 @@ public void actionPerformed(ActionEvent e) {
     item = new JMenuItem(tr("Add File..."));
     item.addActionListener(new ActionListener() {
       public void actionPerformed(ActionEvent e) {
-        sketch.handleAddFile();
+        sketchController.handleAddFile();
       }
     });
     sketchMenu.add(item);
@@ -1028,57 +976,14 @@ private String findClassInZipFile(String base, File file) {
       }
     }
     return null;
-  }
-
-
-  private SketchTextArea createTextArea() throws IOException {
-    final SketchTextArea textArea = new SketchTextArea(base.getPdeKeywords());
-    textArea.setFocusTraversalKeysEnabled(false);
-    textArea.requestFocusInWindow();
-    textArea.setMarkOccurrences(PreferencesData.getBoolean("editor.advanced"));
-    textArea.setMarginLineEnabled(false);
-    textArea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding"));
-    textArea.setAntiAliasingEnabled(PreferencesData.getBoolean("editor.antialias"));
-    textArea.setTabsEmulated(PreferencesData.getBoolean("editor.tabs.expand"));
-    textArea.setTabSize(PreferencesData.getInteger("editor.tabs.size"));
-    textArea.addHyperlinkListener(new HyperlinkListener() {
-      @Override
-      public void hyperlinkUpdate(HyperlinkEvent hyperlinkEvent) {
-        try {
-          platform.openURL(sketch.getFolder(), hyperlinkEvent.getURL().toExternalForm());
-        } catch (Exception e) {
-          Base.showWarning(e.getMessage(), e.getMessage(), e);
-        }
-      }
-    });
-    textArea.addCaretListener(new CaretListener() {
-
-      @Override
-      public void caretUpdate(CaretEvent e) {
-        int lineStart = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getMark());
-        int lineEnd = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getDot());
-
-        lineStatus.set(lineStart, lineEnd);
-      }
-
-    });
-
-    ToolTipManager.sharedInstance().registerComponent(textArea);
-
-    configurePopupMenu(textArea);
-    return textArea;
-  }
+	}
 
   public void updateKeywords(PdeKeywords keywords) {
-    // update GUI for "Find In Reference"
-    textarea.setKeywords(keywords);
-    // update document for syntax highlighting
-    RSyntaxDocument document = (RSyntaxDocument) textarea.getDocument();
-    document.setTokenMakerFactory(new ArduinoTokenMakerFactory(keywords));
-    document.setSyntaxStyle(RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
+    for (EditorTab tab : tabs)
+      tab.updateKeywords(keywords);
   }
 
-  private JMenuItem createToolMenuItem(String className) {
+  JMenuItem createToolMenuItem(String className) {
     try {
       Class<?> toolClass = Class.forName(className);
       final Tool tool = (Tool) toolClass.newInstance();
@@ -1410,7 +1315,7 @@ private JMenu buildEditMenu() {
     JMenuItem cutItem = newJMenuItem(tr("Cut"), 'X');
     cutItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          handleCut();
+          getCurrentTab().handleCut();
         }
       });
     menu.add(cutItem);
@@ -1418,7 +1323,7 @@ public void actionPerformed(ActionEvent e) {
     JMenuItem copyItem = newJMenuItem(tr("Copy"), 'C');
     copyItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          textarea.copy();
+          getCurrentTab().getTextArea().copy();
         }
       });
     menu.add(copyItem);
@@ -1426,11 +1331,7 @@ public void actionPerformed(ActionEvent e) {
     JMenuItem copyForumItem = newJMenuItemShift(tr("Copy for Forum"), 'C');
     copyForumItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-//          SwingUtilities.invokeLater(new Runnable() {
-//              public void run() {
-          new DiscourseFormat(Editor.this, false).show();
-//              }
-//            });
+          getCurrentTab().handleHTMLCopy();
         }
       });
     menu.add(copyForumItem);
@@ -1438,11 +1339,7 @@ public void actionPerformed(ActionEvent e) {
     JMenuItem copyHTMLItem = newJMenuItemAlt(tr("Copy as HTML"), 'C');
     copyHTMLItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-//          SwingUtilities.invokeLater(new Runnable() {
-//              public void run() {
-          new DiscourseFormat(Editor.this, true).show();
-//              }
-//            });
+          getCurrentTab().handleDiscourseCopy();
         }
       });
     menu.add(copyHTMLItem);
@@ -1450,8 +1347,7 @@ public void actionPerformed(ActionEvent e) {
     JMenuItem pasteItem = newJMenuItem(tr("Paste"), 'V');
     pasteItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          textarea.paste();
-          sketch.setModified(true);
+          getCurrentTab().handlePaste();
         }
       });
     menu.add(pasteItem);
@@ -1459,7 +1355,7 @@ public void actionPerformed(ActionEvent e) {
     JMenuItem selectAllItem = newJMenuItem(tr("Select All"), 'A');
     selectAllItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          textarea.selectAll();
+          getCurrentTab().handleSelectAll();
         }
       });
     menu.add(selectAllItem);
@@ -1477,7 +1373,7 @@ public void actionPerformed(ActionEvent e) {
     JMenuItem commentItem = newJMenuItem(tr("Comment/Uncomment"), '/');
     commentItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          handleCommentUncomment();
+          getCurrentTab().handleCommentUncomment();
         }
     });
     menu.add(commentItem);
@@ -1486,7 +1382,7 @@ public void actionPerformed(ActionEvent e) {
     increaseIndentItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0));
     increaseIndentItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          handleIndentOutdent(true);
+          getCurrentTab().handleIndentOutdent(true);
         }
     });
     menu.add(increaseIndentItem);
@@ -1496,7 +1392,7 @@ public void actionPerformed(ActionEvent e) {
     decreseIndentItem.setName("menuDecreaseIndent");
     decreseIndentItem.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
-          handleIndentOutdent(false);
+          getCurrentTab().handleIndentOutdent(false);
         }
     });
     menu.add(decreseIndentItem);
@@ -1510,7 +1406,7 @@ public void actionPerformed(ActionEvent e) {
           find = new FindReplace(Editor.this, Base.FIND_DIALOG_STATE);
         }
         if (!OSUtils.isMacOS()) {
-          find.setFindText(getSelectedText());
+          find.setFindText(getCurrentTab().getSelectedText());
         }
         find.setLocationRelativeTo(Editor.this);
         find.setVisible(true);
@@ -1545,7 +1441,7 @@ public void actionPerformed(ActionEvent e) {
           if (find == null) {
             find = new FindReplace(Editor.this, Base.FIND_DIALOG_STATE);
           }
-          find.setFindText(getSelectedText());
+          find.setFindText(getCurrentTab().getSelectedText());
         }
       });
       menu.add(useSelectionForFindItem);
@@ -1600,8 +1496,7 @@ public UndoAction() {
 
     public void actionPerformed(ActionEvent e) {
       try {
-        textarea.undoLastAction();
-        sketch.setModified(true);
+        getCurrentTab().handleUndo();
       } catch (CannotUndoException ex) {
         //System.out.println("Unable to undo: " + ex);
         //ex.printStackTrace();
@@ -1609,8 +1504,7 @@ public void actionPerformed(ActionEvent e) {
     }
 
     protected void updateUndoState() {
-      
-      UndoManager undo = textarea.getUndoManager();
+      UndoManager undo = getCurrentTab().getUndoManager();
 
       if (undo.canUndo()) {
         this.setEnabled(true);
@@ -1635,8 +1529,7 @@ public RedoAction() {
 
     public void actionPerformed(ActionEvent e) {
       try {
-        textarea.redoLastAction();
-        sketch.setModified(true);
+        getCurrentTab().handleRedo();
       } catch (CannotRedoException ex) {
         //System.out.println("Unable to redo: " + ex);
         //ex.printStackTrace();
@@ -1644,8 +1537,8 @@ public void actionPerformed(ActionEvent e) {
     }
 
     protected void updateRedoState() {
-      UndoManager undo = textarea.getUndoManager();
-      
+      UndoManager undo = getCurrentTab().getUndoManager();
+
       if (undo.canRedo()) {
         redoItem.setEnabled(true);
         redoItem.setText(undo.getRedoPresentationName());
@@ -1674,7 +1567,6 @@ private void resetHandlers() {
     presentHandler = new BuildHandler(true);
     runAndSaveHandler = new BuildHandler(false, true);
     presentAndSaveHandler = new BuildHandler(true, true);
-    stopHandler = new DefaultStopHandler();
     exportHandler = new DefaultExportHandler();
     exportAppHandler = new DefaultExportAppHandler();
   }
@@ -1684,246 +1576,149 @@ private void resetHandlers() {
 
 
   /**
-   * Gets the current sketch object.
+   * Gets the current sketch controller.
+   */
+  public SketchController getSketchController() {
+    return sketchController;
+  }
+
+  /**
+   * Gets the current sketch.
    */
   public Sketch getSketch() {
     return sketch;
   }
 
-
   /**
-   * Get the TextArea object for use (not recommended). This should only
-   * be used in obscure cases that really need to hack the internals of the
-   * JEditTextArea. Most tools should only interface via the get/set functions
-   * found in this class. This will maintain compatibility with future releases,
-   * which will not use TextArea.
+   * Gets the currently displaying tab.
    */
-  public SketchTextArea getTextArea() {
-    return textarea;
+  public EditorTab getCurrentTab() {
+    return tabs.get(currentTabIndex);
   }
 
-
   /**
-   * Get the contents of the current buffer. Used by the Sketch class.
+   * Gets the index of the currently displaying tab.
    */
-  public String getText() {
-    return textarea.getText();
+  public int getCurrentTabIndex() {
+    return currentTabIndex;
   }
 
-
   /**
-   * Replace the entire contents of the front-most tab.
+   * Returns an (unmodifiable) list of currently opened tabs.
    */
-  public void setText(String what) {
-    textarea.setText(what);
+  public List<EditorTab> getTabs() {
+    return Collections.unmodifiableList(tabs);
   }
 
-
-
+  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
   /**
-   * Called to update the text but not switch to a different set of code
-   * (which would affect the undo manager).
+   * Change the currently displayed tab.
+   * Note that the GUI might not update immediately, since this needs
+   * to run in the Event dispatch thread.
+   * @param index The index of the tab to select
    */
-//  public void setText2(String what, int start, int stop) {
-//    beginCompoundEdit();
-//    textarea.setText(what);
-//    endCompoundEdit();
-//
-//    // make sure that a tool isn't asking for a bad location
-//    start = Math.max(0, Math.min(start, textarea.getDocumentLength()));
-//    stop = Math.max(0, Math.min(start, textarea.getDocumentLength()));
-//    textarea.select(start, stop);
-//
-//    textarea.requestFocus();  // get the caret blinking
-//  }
-
-
-  public String getSelectedText() {
-    return textarea.getSelectedText();
+  public void selectTab(final int index) {
+    currentTabIndex = index;
+    undoAction.updateUndoState();
+    redoAction.updateRedoState();
+    updateTitle();
+    header.rebuild();
+    getCurrentTab().activated();
+
+    // This must be run in the GUI thread
+    SwingUtilities.invokeLater(() -> {
+      codePanel.removeAll();
+      codePanel.add(tabs.get(index), BorderLayout.CENTER);
+      tabs.get(index).requestFocusInWindow(); // get the caret blinking
+      // For some reason, these are needed. Revalidate says it should be
+      // automatically called when components are added or removed, but without
+      // it, the component switched to is not displayed. repaint() is needed to
+      // clear the entire text area of any previous text.
+      codePanel.revalidate();
+      codePanel.repaint();
+    });
   }
 
-
-  public void setSelectedText(String what) {
-    textarea.replaceSelection(what);
+  public void selectNextTab() {
+    selectTab((currentTabIndex + 1) % tabs.size());
   }
 
-  public void setSelection(int start, int stop) {
-    textarea.select(start, stop);
+  public void selectPrevTab() {
+    selectTab((currentTabIndex - 1 + tabs.size()) % tabs.size());
   }
 
-
-  /**
-   * Get the beginning point of the current selection.
-   */
-  public int getSelectionStart() {
-    return textarea.getSelectionStart();
+  public EditorTab findTab(final SketchFile file) {
+    return tabs.get(findTabIndex(file));
   }
 
-
   /**
-   * Get the end point of the current selection.
+   * Finds the index of the tab showing the given file. Matches the file against
+   * EditorTab.getSketchFile() using ==.
+   *
+   * @returns The index of the tab for the given file, or -1 if no such tab was
+   *          found.
    */
-  public int getSelectionStop() {
-    return textarea.getSelectionEnd();
+  public int findTabIndex(final SketchFile file) {
+    for (int i = 0; i < tabs.size(); ++i) {
+      if (tabs.get(i).getSketchFile() == file)
+        return i;
+    }
+    return -1;
   }
 
-
   /**
-   * Get text for a specified line.
+   * Finds the index of the tab showing the given file. Matches the file against
+   * EditorTab.getSketchFile().getFile() using equals.
+   *
+   * @returns The index of the tab for the given file, or -1 if no such tab was
+   *          found.
    */
-  private String getLineText(int line) {
-    try {
-      return textarea.getText(textarea.getLineStartOffset(line), textarea.getLineEndOffset(line));
-    } catch (BadLocationException e) {
-      return "";
+  public int findTabIndex(final File file) {
+    for (int i = 0; i < tabs.size(); ++i) {
+      if (tabs.get(i).getSketchFile().getFile().equals(file))
+        return i;
     }
+    return -1;
   }
 
-
-  public int getScrollPosition() {
-    return scrollPane.getVerticalScrollBar().getValue();
-  }
-
-
-  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
-
-
   /**
-   * Switch between tabs, this swaps out the Document object
-   * that's currently being manipulated.
+   * Create tabs for each of the current sketch's files, removing any existing
+   * tabs.
    */
-  protected void setCode(final SketchCodeDocument codeDoc) {
-    RSyntaxDocument document = (RSyntaxDocument) codeDoc.getDocument();
-
-    if (document == null) {  // this document not yet inited
-      document = new RSyntaxDocument(new ArduinoTokenMakerFactory(base.getPdeKeywords()), RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
-      document.putProperty(PlainDocument.tabSizeAttribute, PreferencesData.getInteger("editor.tabs.size"));
-
-      // insert the program text into the document object
+  public void createTabs() {
+    tabs.clear();
+    currentTabIndex = -1;
+    tabs.ensureCapacity(sketch.getCodeCount());
+    for (SketchFile file : sketch.getFiles()) {
       try {
-        document.insertString(0, codeDoc.getCode().getProgram(), null);
-      } catch (BadLocationException bl) {
-        bl.printStackTrace();
+        addTab(file, null);
+      } catch(IOException e) {
+        // TODO: Improve / move error handling
+        System.err.println(e);
       }
-      // set up this guy's own undo manager
-//      code.undo = new UndoManager();
-      
-      codeDoc.setDocument(document);
     }
-
-    if(codeDoc.getUndo() == null){
-      codeDoc.setUndo(new LastUndoableEditAwareUndoManager(textarea, this));
-      document.addUndoableEditListener(codeDoc.getUndo());
-		}
-    
-    // Update the document object that's in use
-    textarea.switchDocument(document, codeDoc.getUndo());
-    
-    // HACK multiple tabs: for update Listeners of Gutter, forcin call: Gutter.setTextArea(RTextArea)
-    // BUG: https://github.com/bobbylight/RSyntaxTextArea/issues/84
-    scrollPane.setViewportView(textarea);
-    
-    textarea.select(codeDoc.getSelectionStart(), codeDoc.getSelectionStop());
-    textarea.requestFocus();  // get the caret blinking
-     
-    final int position = codeDoc.getScrollPosition();
-
-    // invokeLater: Expect the document to be rendered correctly to set the new position
-    SwingUtilities.invokeLater(new Runnable() {
-      @Override
-      public void run() {
-          scrollPane.getVerticalScrollBar().setValue(position);
-          undoAction.updateUndoState();
-          redoAction.updateRedoState();
-      }
-    });
-
-    updateTitle();
+    selectTab(0);
   }
 
-
-  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
-
-
   /**
-   * Implements Edit &rarr; Cut.
+   * Add a new tab.
+   *
+   * @param file
+   *          The file to show in the tab.
+   * @param contents
+   *          The contents to show in the tab, or null to load the contents from
+   *          the given file.
+   * @throws IOException
    */
-  private void handleCut() {
-    textarea.cut();
-  }
-
-
-  private void handleDiscourseCopy() {
-    new DiscourseFormat(Editor.this, false).show();
-  }
-
-
-  private void handleHTMLCopy() {
-    new DiscourseFormat(Editor.this, true).show();
+  protected void addTab(SketchFile file, String contents) throws IOException {
+    EditorTab tab = new EditorTab(this, file, contents);
+    tabs.add(tab);
   }
 
+  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 
-  void handleCommentUncomment() {
-
-    Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaToggleCommentAction);
-    action.actionPerformed(null);
-
- }
-
-
-  private void handleIndentOutdent(boolean indent) {
-    if (indent) {
-      Action action = textarea.getActionMap().get(SketchTextAreaEditorKit.rtaIncreaseIndentAction);
-      action.actionPerformed(null);
-    } else {
-      Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaDecreaseIndentAction);
-      action.actionPerformed(null);
-    }
-  }
-
-  private String getCurrentKeyword() {
-		String text = "";
-		if (textarea.getSelectedText() != null)
-			text = textarea.getSelectedText().trim();
-
-		try {
-			int current = textarea.getCaretPosition();
-			int startOffset = 0;
-			int endIndex = current;
-			String tmp = textarea.getDocument().getText(current, 1);
-			// TODO probably a regexp that matches Arduino lang special chars
-			// already exists.
-			String regexp = "[\\s\\n();\\\\.!='\\[\\]{}]";
-
-			while (!tmp.matches(regexp)) {
-				endIndex++;
-				tmp = textarea.getDocument().getText(endIndex, 1);
-			}
-			// For some reason document index start at 2.
-			// if( current - start < 2 ) return;
-
-			tmp = "";
-			while (!tmp.matches(regexp)) {
-				startOffset++;
-				if (current - startOffset < 0) {
-					tmp = textarea.getDocument().getText(0, 1);
-					break;
-				} else
-					tmp = textarea.getDocument().getText(current - startOffset, 1);
-			}
-			startOffset--;
-
-			int length = endIndex - current + startOffset;
-			text = textarea.getDocument().getText(current - startOffset, length);
-
-		} catch (BadLocationException bl) {
-			bl.printStackTrace();
-		}
-		return text;
-	}
-
-  private void handleFindReference(ActionEvent e) {
-    String text = getCurrentKeyword();
+  void handleFindReference(ActionEvent e) {
+    String text = getCurrentTab().getCurrentKeyword();
 
     String referenceFile = base.getPdeKeywords().getReference(text);
     if (referenceFile == null) {
@@ -1951,9 +1746,8 @@ public void handleRun(final boolean verbose, Runnable verboseHandler, Runnable n
     handleRun(verbose, new ShouldSaveIfModified(), verboseHandler, nonVerboseHandler);
   }
 
-  private void handleRun(final boolean verbose, Predicate<Sketch> shouldSavePredicate, Runnable verboseHandler, Runnable nonVerboseHandler) {
-    internalCloseRunner();
-    if (shouldSavePredicate.test(sketch)) {
+  private void handleRun(final boolean verbose, Predicate<SketchController> shouldSavePredicate, Runnable verboseHandler, Runnable nonVerboseHandler) {
+    if (shouldSavePredicate.test(sketchController)) {
       handleSave(true);
     }
     toolbar.activateRun();
@@ -1994,8 +1788,7 @@ public BuildHandler(boolean verbose, boolean saveHex) {
     public void run() {
       try {
         removeAllLineHighlights();
-        sketch.prepare();
-        sketch.build(verbose, saveHex);
+        sketchController.build(verbose, saveHex);
         statusNotice(tr("Done compiling."));
       } catch (PreferencesMapException e) {
         statusError(I18n.format(
@@ -2012,19 +1805,13 @@ public void run() {
   }
 
   public void removeAllLineHighlights() {
-    textarea.removeAllLineHighlights();
+    for (EditorTab tab : tabs)
+      tab.getTextArea().removeAllLineHighlights();
   }
 
   public void addLineHighlight(int line) throws BadLocationException {
-    textarea.addLineHighlight(line, new Color(1, 0, 0, 0.2f));
-    textarea.setCaretPosition(textarea.getLineStartOffset(line));
-  }
-
-  private class DefaultStopHandler implements Runnable {
-    public void run() {
-      // TODO
-      // DAM: we should try to kill the compilation or upload process here.
-    }
+    getCurrentTab().getTextArea().addLineHighlight(line, new Color(1, 0, 0, 0.2f));
+    getCurrentTab().getTextArea().setCaretPosition(getCurrentTab().getTextArea().getLineStartOffset(line));
   }
 
 
@@ -2034,8 +1821,6 @@ public void run() {
   private void handleStop() {  // called by menu or buttons
 //    toolbar.activate(EditorToolbar.STOP);
 
-    internalCloseRunner();
-
     toolbar.deactivateRun();
 //    toolbar.deactivate(EditorToolbar.STOP);
 
@@ -2043,32 +1828,21 @@ private void handleStop() {  // called by menu or buttons
     toFront();
   }
 
-
-  /**
-   * Handle internal shutdown of the runner.
-   */
-  public void internalCloseRunner() {
-
-    if (stopHandler != null)
-    try {
-      stopHandler.run();
-    } catch (Exception e) { }
-  }
-
-
   /**
    * Check if the sketch is modified and ask user to save changes.
    * @return false if canceling the close/quit operation
    */
   protected boolean checkModified() {
-    if (!sketch.isModified()) return true;
+    if (!sketch.isModified())
+      return true;
 
     // As of Processing 1.0.10, this always happens immediately.
     // http://dev.processing.org/bugs/show_bug.cgi?id=1456
 
     toFront();
 
-    String prompt = I18n.format(tr("Save changes to \"{0}\"?  "), sketch.getName());
+    String prompt = I18n.format(tr("Save changes to \"{0}\"?  "),
+                                sketch.getName());
 
     if (!OSUtils.isMacOS()) {
       int result =
@@ -2128,25 +1902,6 @@ protected boolean checkModified() {
     }
   }
 
-
-  /**
-   * Open a sketch from a particular path, but don't check to save changes.
-   * Used by Sketch.saveAs() to re-open a sketch after the "Save As"
-   */
-  protected void handleOpenUnchecked(File file, int codeIndex,
-                                     int selStart, int selStop, int scrollPos) {
-    internalCloseRunner();
-    handleOpenInternal(file);
-    // Replacing a document that may be untitled. If this is an actual
-    // untitled document, then editor.untitled will be set by Base.
-    untitled = false;
-
-    sketch.setCurrentCode(codeIndex);
-    textarea.select(selStart, selStop);
-    scrollPane.getVerticalScrollBar().setValue(scrollPos);
-  }
-
-
   /**
    * Second stage of open, occurs after having checked to see if the
    * modifications (if any) to the previous sketch need to be saved.
@@ -2156,7 +1911,7 @@ protected boolean handleOpenInternal(File sketchFile) {
     // in a folder of the same name
     String fileName = sketchFile.getName();
 
-    File file = SketchData.checkSketchFile(sketchFile);
+    File file = Sketch.checkSketchFile(sketchFile);
 
     if (file == null) {
       if (!fileName.endsWith(".ino") && !fileName.endsWith(".pde")) {
@@ -2212,13 +1967,14 @@ protected boolean handleOpenInternal(File sketchFile) {
     }
 
     try {
-      sketch = new Sketch(this, file);
+      sketch = new Sketch(file);
     } catch (IOException e) {
       Base.showWarning(tr("Error"), tr("Could not create the sketch."), e);
       return false;
     }
-    header.rebuild();
-    updateTitle();
+    sketchController = new SketchController(this, sketch);
+    createTabs();
+
     // Disable untitled setting from previous document, if any
     untitled = false;
 
@@ -2227,13 +1983,16 @@ protected boolean handleOpenInternal(File sketchFile) {
   }
 
   private void updateTitle() {
-    if (sketch == null) {
+    if (sketchController == null) {
       return;
     }
-    if (sketch.getName().equals(sketch.getCurrentCode().getPrettyName())) {
-      setTitle(I18n.format(tr("{0} | Arduino {1}"), sketch.getName(), BaseNoGui.VERSION_NAME_LONG));
+    SketchFile current = getCurrentTab().getSketchFile();
+    if (current.isPrimary()) {
+      setTitle(I18n.format(tr("{0} | Arduino {1}"), sketch.getName(),
+                           BaseNoGui.VERSION_NAME_LONG));
     } else {
-      setTitle(I18n.format(tr("{0} - {1} | Arduino {2}"), sketch.getName(), sketch.getCurrentCode().getFileName(), BaseNoGui.VERSION_NAME_LONG));
+      setTitle(I18n.format(tr("{0} - {1} | Arduino {2}"), sketch.getName(),
+                           current.getFileName(), BaseNoGui.VERSION_NAME_LONG));
     }
   }
 
@@ -2275,15 +2034,15 @@ private boolean handleSave2() {
     statusNotice(tr("Saving..."));
     boolean saved = false;
     try {
-      boolean wasReadOnly = sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath());
+      boolean wasReadOnly = sketchController.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath());
       String previousMainFilePath = sketch.getMainFilePath();
-      saved = sketch.save();
+      saved = sketchController.save();
       if (saved) {
         statusNotice(tr("Done Saving."));
         if (wasReadOnly) {
           base.removeRecentSketchPath(previousMainFilePath);
         }
-        base.storeRecentSketches(sketch);
+        base.storeRecentSketches(sketchController);
         base.rebuildRecentSketchesMenuItems();
       } else {
         statusEmpty();
@@ -2320,8 +2079,8 @@ public boolean handleSaveAs() {
     //public void run() {
     statusNotice(tr("Saving..."));
     try {
-      if (sketch.saveAs()) {
-        base.storeRecentSketches(sketch);
+      if (sketchController.saveAs()) {
+        base.storeRecentSketches(sketchController);
         base.rebuildRecentSketchesMenuItems();
         statusNotice(tr("Done Saving."));
         // Disabling this for 0125, instead rebuild the menu inside
@@ -2388,7 +2147,11 @@ private boolean serialPrompt() {
    */
   synchronized public void handleExport(final boolean usingProgrammer) {
     if (PreferencesData.getBoolean("editor.save_on_verify")) {
-      if (sketch.isModified() && !sketch.isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
+      if (sketch.isModified()
+          && !sketchController.isReadOnly(
+                                          BaseNoGui.librariesIndexer
+                                              .getInstalledLibraries(),
+                                          BaseNoGui.getExamplesPath())) {
         handleSave(true);
       }
     }
@@ -2414,7 +2177,7 @@ public void run() {
 
         uploading = true;
 
-        boolean success = sketch.exportApplet(false);
+        boolean success = sketchController.exportApplet(false);
         if (success) {
           statusNotice(tr("Done uploading."));
         }
@@ -2509,7 +2272,7 @@ public void run() {
 
         uploading = true;
 
-        boolean success = sketch.exportApplet(true);
+        boolean success = sketchController.exportApplet(true);
         if (success) {
           statusNotice(tr("Done uploading."));
         }
@@ -2781,12 +2544,12 @@ private void handlePrint() {
     PrinterJob printerJob = PrinterJob.getPrinterJob();
     if (pageFormat != null) {
       //System.out.println("setting page format " + pageFormat);
-      printerJob.setPrintable(textarea, pageFormat);
+      printerJob.setPrintable(getCurrentTab().getTextArea(), pageFormat);
     } else {
-      printerJob.setPrintable(textarea);
+      printerJob.setPrintable(getCurrentTab().getTextArea());
     }
     // set the name of the job to the code name
-    printerJob.setJobName(sketch.getCurrentCode().getPrettyName());
+    printerJob.setJobName(getCurrentTab().getSketchFile().getPrettyName());
 
     if (printerJob.printDialog()) {
       try {
@@ -2830,23 +2593,23 @@ public void statusError(Exception e) {
 
     if (e instanceof RunnerException) {
       RunnerException re = (RunnerException) e;
-      if (re.hasCodeIndex()) {
-        sketch.setCurrentCode(re.getCodeIndex());
+      if (re.hasCodeFile()) {
+        selectTab(findTabIndex(re.getCodeFile()));
       }
       if (re.hasCodeLine()) {
         int line = re.getCodeLine();
         // subtract one from the end so that the \n ain't included
-        if (line >= textarea.getLineCount()) {
+        if (line >= getCurrentTab().getTextArea().getLineCount()) {
           // The error is at the end of this current chunk of code,
           // so the last line needs to be selected.
-          line = textarea.getLineCount() - 1;
-          if (getLineText(line).length() == 0) {
+          line = getCurrentTab().getTextArea().getLineCount() - 1;
+          if (getCurrentTab().getLineText(line).length() == 0) {
             // The last line may be zero length, meaning nothing to select.
             // If so, back up one more line.
             line--;
           }
         }
-        if (line < 0 || line >= textarea.getLineCount()) {
+        if (line < 0 || line >= getCurrentTab().getTextArea().getLineCount()) {
           System.err.println(I18n.format(tr("Bad error line: {0}"), line));
         } else {
           try {
@@ -2891,7 +2654,6 @@ private void statusEmpty() {
     statusNotice(EMPTY);
   }
 
-
   // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 
   protected void onBoardOrPortChange() {
@@ -2905,110 +2667,4 @@ protected void onBoardOrPortChange() {
   }
 
 
-  private void configurePopupMenu(final SketchTextArea textarea){
-
-    JPopupMenu menu = textarea.getPopupMenu();
-
-    menu.addSeparator();
-
-    JMenuItem item = createToolMenuItem("cc.arduino.packages.formatter.AStyle");
-    if (item == null) {
-      throw new NullPointerException("Tool cc.arduino.packages.formatter.AStyle unavailable");
-    }
-    item.setName("menuToolsAutoFormat");
-
-    menu.add(item);
-    
-    item = newJMenuItem(tr("Comment/Uncomment"), '/');
-    item.addActionListener(new ActionListener() {
-        public void actionPerformed(ActionEvent e) {
-          handleCommentUncomment();
-        }
-    });
-    menu.add(item);
-
-    item = newJMenuItem(tr("Increase Indent"), ']');
-    item.addActionListener(new ActionListener() {
-        public void actionPerformed(ActionEvent e) {
-          handleIndentOutdent(true);
-        }
-    });
-    menu.add(item);
-
-    item = newJMenuItem(tr("Decrease Indent"), '[');
-    item.setName("menuDecreaseIndent");
-    item.addActionListener(new ActionListener() {
-        public void actionPerformed(ActionEvent e) {
-          handleIndentOutdent(false);
-        }
-    });
-    menu.add(item);
-
-    item = new JMenuItem(tr("Copy for Forum"));
-    item.addActionListener(new ActionListener() {
-      public void actionPerformed(ActionEvent e) {
-        handleDiscourseCopy();
-      }
-    });
-    menu.add(item);
-
-    item = new JMenuItem(tr("Copy as HTML"));
-    item.addActionListener(new ActionListener() {
-      public void actionPerformed(ActionEvent e) {
-        handleHTMLCopy();
-      }
-    });
-    menu.add(item);
-
-    final JMenuItem referenceItem = new JMenuItem(tr("Find in Reference"));
-    referenceItem.addActionListener(this::handleFindReference);
-    menu.add(referenceItem);  
-
-    final JMenuItem openURLItem = new JMenuItem(tr("Open URL"));
-    openURLItem.addActionListener(new ActionListener() {
-      public void actionPerformed(ActionEvent e) {
-        Base.openURL(e.getActionCommand());
-      }
-    });
-    menu.add(openURLItem);   
-    
-    menu.addPopupMenuListener(new PopupMenuListener() {
-
-      @Override
-      public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
-            String referenceFile = base.getPdeKeywords().getReference(getCurrentKeyword());
-            referenceItem.setEnabled(referenceFile != null);
-    
-            int offset = textarea.getCaretPosition();
-            org.fife.ui.rsyntaxtextarea.Token token = RSyntaxUtilities.getTokenAtOffset(textarea, offset);
-            if (token != null && token.isHyperlink()) {
-              openURLItem.setEnabled(true);
-              openURLItem.setActionCommand(token.getLexeme());
-            } else {
-              openURLItem.setEnabled(false);
-            }
-      }
-
-      @Override
-      public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
-      }
-
-      @Override
-      public void popupMenuCanceled(PopupMenuEvent e) {
-      }
-    });
-
-  }
-
-  public void goToLine(int line) {
-    if (line <= 0) {
-      return;
-    }
-    try {
-      textarea.setCaretPosition(textarea.getLineStartOffset(line - 1));
-    } catch (BadLocationException e) {
-      //ignore
-    }
-  }
-
 }
diff --git a/app/src/processing/app/EditorHeader.java b/app/src/processing/app/EditorHeader.java
index 84dc49df4c1..b0e3c9c0300 100644
--- a/app/src/processing/app/EditorHeader.java
+++ b/app/src/processing/app/EditorHeader.java
@@ -32,7 +32,7 @@
 import java.awt.*;
 import java.awt.event.*;
 import java.io.IOException;
-
+import java.util.List;
 import javax.swing.*;
 
 
@@ -82,26 +82,24 @@ public class EditorHeader extends JComponent {
   public class Actions {
     public final Action newTab = new SimpleAction(tr("New Tab"),
         Keys.ctrlShift(KeyEvent.VK_N),
-        () -> editor.getSketch().handleNewCode());
+        () -> editor.getSketchController().handleNewCode());
 
     public final Action renameTab = new SimpleAction(tr("Rename"),
-        () -> editor.getSketch().handleRenameCode());
+        () -> editor.getSketchController().handleRenameCode());
 
     public final Action deleteTab = new SimpleAction(tr("Delete"), () -> {
       try {
-        editor.getSketch().handleDeleteCode();
+        editor.getSketchController().handleDeleteCode();
       } catch (IOException e) {
         e.printStackTrace();
       }
     });
 
     public final Action prevTab = new SimpleAction(tr("Previous Tab"),
-        Keys.ctrlAlt(KeyEvent.VK_LEFT),
-        () -> editor.sketch.handlePrevCode());
+        Keys.ctrlAlt(KeyEvent.VK_LEFT), () -> editor.selectPrevTab());
 
     public final Action nextTab = new SimpleAction(tr("Next Tab"),
-        Keys.ctrlAlt(KeyEvent.VK_RIGHT),
-        () -> editor.sketch.handleNextCode());
+        Keys.ctrlAlt(KeyEvent.VK_RIGHT), () -> editor.selectNextTab());
 
     Actions() {
       // Explicitly bind keybindings for the actions with accelerators above
@@ -170,10 +168,10 @@ public void mousePressed(MouseEvent e) {
             popup.show(EditorHeader.this, x, y);
 
           } else {
-            Sketch sketch = editor.getSketch();
-            for (int i = 0; i < sketch.getCodeCount(); i++) {
+            int numTabs = editor.getTabs().size();
+            for (int i = 0; i < numTabs; i++) {
               if ((x > tabLeft[i]) && (x < tabRight[i])) {
-                sketch.setCurrentCode(i);
+                editor.selectTab(i);
                 repaint();
               }
             }
@@ -186,7 +184,7 @@ public void mousePressed(MouseEvent e) {
   public void paintComponent(Graphics screen) {
     if (screen == null) return;
 
-    Sketch sketch = editor.getSketch();
+    SketchController sketch = editor.getSketchController();
     if (sketch == null) return;  // ??
 
     Dimension size = getSize();
@@ -229,21 +227,22 @@ public void paintComponent(Graphics screen) {
     g.setColor(backgroundColor);
     g.fillRect(0, 0, imageW, imageH);
 
-    int codeCount = sketch.getCodeCount();
+    List<EditorTab> tabs = editor.getTabs();
+
+    int codeCount = tabs.size();
     if ((tabLeft == null) || (tabLeft.length < codeCount)) {
       tabLeft = new int[codeCount];
       tabRight = new int[codeCount];
     }
 
     int x = 6; // offset from left edge of the component
-    for (int i = 0; i < sketch.getCodeCount(); i++) {
-      SketchCode code = sketch.getCode(i);
-
-      String codeName = code.isExtension(sketch.getHiddenExtensions()) ?
-        code.getPrettyName() : code.getFileName();
+    int i = 0;
+    for (EditorTab tab : tabs) {
+      SketchFile file = tab.getSketchFile();
+      String filename = file.getPrettyName();
 
       // if modified, add the li'l glyph next to the name
-      String text = "  " + codeName + (code.isModified() ? " \u00A7" : "  ");
+      String text = "  " + filename + (file.isModified() ? " \u00A7" : "  ");
 
       Graphics2D g2 = (Graphics2D) g;
       int textWidth = (int)
@@ -252,7 +251,7 @@ public void paintComponent(Graphics screen) {
       int pieceCount = 2 + (textWidth / PIECE_WIDTH);
       int pieceWidth = pieceCount * PIECE_WIDTH;
 
-      int state = (code == sketch.getCurrentCode()) ? SELECTED : UNSELECTED;
+      int state = (i == editor.getCurrentTabIndex()) ? SELECTED : UNSELECTED;
       g.drawImage(pieces[state][LEFT], x, 0, null);
       x += PIECE_WIDTH;
 
@@ -272,6 +271,7 @@ public void paintComponent(Graphics screen) {
 
       g.drawImage(pieces[state][RIGHT], x, 0, null);
       x += PIECE_WIDTH - 1;  // overlap by 1 pixel
+      i++;
     }
 
     menuLeft = sizeW - (16 + pieces[0][MENU].getWidth(this));
@@ -317,13 +317,14 @@ public void rebuildMenu() {
     Sketch sketch = editor.getSketch();
     if (sketch != null) {
       menu.addSeparator();
+
       int i = 0;
-      for (SketchCode code : sketch.getCodes()) {
+      for (EditorTab tab : editor.getTabs()) {
+        SketchFile file = tab.getSketchFile();
         final int index = i++;
-        item = new JMenuItem(code.isExtension(sketch.getDefaultExtension()) ? 
-                             code.getPrettyName() : code.getFileName());
+        item = new JMenuItem(file.getPrettyName());
         item.addActionListener((ActionEvent e) -> {
-          editor.getSketch().setCurrentCode(index);
+          editor.selectTab(index);
         });
         menu.add(item);
       }
diff --git a/app/src/processing/app/EditorStatus.java b/app/src/processing/app/EditorStatus.java
index b6551c7fa00..cd56c4171c5 100644
--- a/app/src/processing/app/EditorStatus.java
+++ b/app/src/processing/app/EditorStatus.java
@@ -143,7 +143,7 @@ public void edit(String message, String dflt) {
     editField.setVisible(true);
     editField.setText(dflt);
     editField.selectAll();
-    editField.requestFocus();
+    editField.requestFocusInWindow();
 
     repaint();
   }
@@ -242,7 +242,7 @@ private void initialize() {
       // answering to rename/new code question
       if (mode == EDIT) {  // this if() isn't (shouldn't be?) necessary
         String answer = editField.getText();
-        editor.getSketch().nameCode(answer);
+        editor.getSketchController().nameCode(answer);
         unedit();
       }
     });
@@ -281,7 +281,7 @@ public void keyTyped(KeyEvent event) {
 
         if (c == KeyEvent.VK_ENTER) {  // accept the input
           String answer = editField.getText();
-          editor.getSketch().nameCode(answer);
+          editor.getSketchController().nameCode(answer);
           unedit();
           event.consume();
 
diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java
new file mode 100644
index 00000000000..3ac09bc8054
--- /dev/null
+++ b/app/src/processing/app/EditorTab.java
@@ -0,0 +1,608 @@
+/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
+
+/*
+  Part of the Arduino project - http://www.arduino.cc
+
+  Copyright (c) 2015 Matthijs Kooijman
+  Copyright (c) 2004-09 Ben Fry and Casey Reas
+  Copyright (c) 2001-04 Massachusetts Institute of Technology
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License version 2
+  as published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software Foundation,
+  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+
+package processing.app;
+
+import static processing.app.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+
+import javax.swing.Action;
+import javax.swing.BorderFactory;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.ToolTipManager;
+import javax.swing.border.MatteBorder;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.PlainDocument;
+import javax.swing.undo.UndoManager;
+import javax.swing.text.DefaultCaret;
+
+import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
+import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
+import org.fife.ui.rtextarea.Gutter;
+import org.fife.ui.rtextarea.RTextScrollPane;
+import org.fife.ui.rtextarea.RUndoManager;
+
+import processing.app.helpers.DocumentTextChangeListener;
+import processing.app.syntax.ArduinoTokenMakerFactory;
+import processing.app.syntax.PdeKeywords;
+import processing.app.syntax.SketchTextArea;
+import processing.app.syntax.SketchTextAreaEditorKit;
+import processing.app.tools.DiscourseFormat;
+
+/**
+ * Single tab, editing a single file, in the main window.
+ */
+public class EditorTab extends JPanel implements SketchFile.TextStorage {
+  protected Editor editor;
+  protected SketchTextArea textarea;
+  protected RTextScrollPane scrollPane;
+  protected SketchFile file;
+  protected boolean modified;
+  /** Is external editing mode currently enabled? */
+  protected boolean external;
+  
+  /**
+   * Create a new EditorTab
+   *
+   * @param editor
+   *          The Editor this tab runs in
+   * @param file
+   *          The file to display in this tab
+   * @param contents
+   *          Initial contents to display in this tab. Can be used when editing
+   *          a file that doesn't exist yet. If null is passed, code.load() is
+   *          called and displayed instead.
+   * @throws IOException
+   */
+  public EditorTab(Editor editor, SketchFile file, String contents)
+      throws IOException {
+    super(new BorderLayout());
+
+    // Load initial contents contents from file if nothing was specified.
+    if (contents == null) {
+      contents = file.load();
+      modified = false;
+    } else {
+      modified = true;
+    }
+
+    this.editor = editor;
+    this.file = file;
+    RSyntaxDocument document = createDocument(contents);
+    this.textarea = createTextArea(document);
+    this.scrollPane = createScrollPane(this.textarea);
+    file.setStorage(this);
+    applyPreferences();
+    add(this.scrollPane, BorderLayout.CENTER);
+
+    RUndoManager undo = new LastUndoableEditAwareUndoManager(this.textarea,
+        this.editor);
+    document.addUndoableEditListener(undo);
+    textarea.setUndoManager(undo);
+  }
+
+  private RSyntaxDocument createDocument(String contents) {
+    RSyntaxDocument document = new RSyntaxDocument(new ArduinoTokenMakerFactory(editor.base.getPdeKeywords()), RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
+    document.putProperty(PlainDocument.tabSizeAttribute, PreferencesData.getInteger("editor.tabs.size"));
+
+    // insert the program text into the document object
+    try {
+      document.insertString(0, contents, null);
+    } catch (BadLocationException bl) {
+      bl.printStackTrace();
+    }
+    document.addDocumentListener(new DocumentTextChangeListener(
+        () -> setModified(true)));
+    return document;
+  }
+  
+  private RTextScrollPane createScrollPane(SketchTextArea textArea) throws IOException {
+    RTextScrollPane scrollPane = new RTextScrollPane(textArea, true);
+    scrollPane.setBorder(new MatteBorder(0, 6, 0, 0, Theme.getColor("editor.bgcolor")));
+    scrollPane.setViewportBorder(BorderFactory.createEmptyBorder());
+    scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers"));
+    scrollPane.setIconRowHeaderEnabled(false);
+
+    Gutter gutter = scrollPane.getGutter();
+    gutter.setBookmarkingEnabled(false);
+    //gutter.setBookmarkIcon(CompletionsRenderer.getIcon(CompletionType.TEMPLATE));
+    gutter.setIconRowHeaderInheritsGutterBackground(true);
+
+    return scrollPane;
+  }
+
+  private SketchTextArea createTextArea(RSyntaxDocument document)
+      throws IOException {
+    final SketchTextArea textArea = new SketchTextArea(document, editor.base.getPdeKeywords());
+    textArea.setName("editor");
+    textArea.setFocusTraversalKeysEnabled(false);
+    //textArea.requestFocusInWindow();
+    textArea.setMarkOccurrences(PreferencesData.getBoolean("editor.advanced"));
+    textArea.setMarginLineEnabled(false);
+    textArea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding"));
+    textArea.setAntiAliasingEnabled(PreferencesData.getBoolean("editor.antialias"));
+    textArea.setTabsEmulated(PreferencesData.getBoolean("editor.tabs.expand"));
+    textArea.setTabSize(PreferencesData.getInteger("editor.tabs.size"));
+    textArea.addHyperlinkListener(new HyperlinkListener() {
+      @Override
+      public void hyperlinkUpdate(HyperlinkEvent hyperlinkEvent) {
+        try {
+          editor.platform.openURL(editor.getSketch().getFolder(),
+                                  hyperlinkEvent.getURL().toExternalForm());
+        } catch (Exception e) {
+          Base.showWarning(e.getMessage(), e.getMessage(), e);
+        }
+      }
+    });
+    textArea.addCaretListener(new CaretListener() {
+      @Override
+      public void caretUpdate(CaretEvent e) {
+        int lineStart = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getMark());
+        int lineEnd = textArea.getDocument().getDefaultRootElement().getElementIndex(e.getDot());
+
+        editor.lineStatus.set(lineStart, lineEnd);
+      }
+
+    });
+
+    ToolTipManager.sharedInstance().registerComponent(textArea);
+
+    configurePopupMenu(textArea);
+    return textArea;
+  }
+  
+  private void configurePopupMenu(final SketchTextArea textarea){
+
+    JPopupMenu menu = textarea.getPopupMenu();
+
+    menu.addSeparator();
+
+    JMenuItem item = editor.createToolMenuItem("cc.arduino.packages.formatter.AStyle");
+    if (item == null) {
+      throw new NullPointerException("Tool cc.arduino.packages.formatter.AStyle unavailable");
+    }
+    item.setName("menuToolsAutoFormat");
+
+    menu.add(item);
+    
+    item = new JMenuItem(tr("Comment/Uncomment"), '/');
+    item.addActionListener(new ActionListener() {
+        public void actionPerformed(ActionEvent e) {
+          handleCommentUncomment();
+        }
+    });
+    menu.add(item);
+
+    item = new JMenuItem(tr("Increase Indent"), ']');
+    item.addActionListener(new ActionListener() {
+        public void actionPerformed(ActionEvent e) {
+          handleIndentOutdent(true);
+        }
+    });
+    menu.add(item);
+
+    item = new JMenuItem(tr("Decrease Indent"), '[');
+    item.setName("menuDecreaseIndent");
+    item.addActionListener(new ActionListener() {
+        public void actionPerformed(ActionEvent e) {
+          handleIndentOutdent(false);
+        }
+    });
+    menu.add(item);
+
+    item = new JMenuItem(tr("Copy for Forum"));
+    item.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleDiscourseCopy();
+      }
+    });
+    menu.add(item);
+
+    item = new JMenuItem(tr("Copy as HTML"));
+    item.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        handleHTMLCopy();
+      }
+    });
+    menu.add(item);
+
+    final JMenuItem referenceItem = new JMenuItem(tr("Find in Reference"));
+    referenceItem.addActionListener(editor::handleFindReference);
+    menu.add(referenceItem);  
+
+    final JMenuItem openURLItem = new JMenuItem(tr("Open URL"));
+    openURLItem.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        Base.openURL(e.getActionCommand());
+      }
+    });
+    menu.add(openURLItem);   
+    
+    menu.addPopupMenuListener(new PopupMenuListener() {
+
+      @Override
+      public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+            String referenceFile = editor.base.getPdeKeywords().getReference(getCurrentKeyword());
+            referenceItem.setEnabled(referenceFile != null);
+    
+            int offset = textarea.getCaretPosition();
+            org.fife.ui.rsyntaxtextarea.Token token = RSyntaxUtilities.getTokenAtOffset(textarea, offset);
+            if (token != null && token.isHyperlink()) {
+              openURLItem.setEnabled(true);
+              openURLItem.setActionCommand(token.getLexeme());
+            } else {
+              openURLItem.setEnabled(false);
+            }
+      }
+
+      @Override
+      public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+      }
+
+      @Override
+      public void popupMenuCanceled(PopupMenuEvent e) {
+      }
+    });
+
+  }
+  
+  public void applyPreferences() {
+    textarea.setCodeFoldingEnabled(PreferencesData.getBoolean("editor.code_folding"));
+    scrollPane.setFoldIndicatorEnabled(PreferencesData.getBoolean("editor.code_folding"));
+    scrollPane.setLineNumbersEnabled(PreferencesData.getBoolean("editor.linenumbers"));
+
+    // apply the setting for 'use external editor', but only if it changed
+    if (external != PreferencesData.getBoolean("editor.external")) {
+      external = !external;
+      if (external) {
+        // disable line highlight and turn off the caret when disabling
+        textarea.setBackground(Theme.getColor("editor.external.bgcolor"));
+        textarea.setHighlightCurrentLine(false);
+        textarea.setEditable(false);
+        // Detach from the code, since we are no longer the authoritative source
+        // for file contents.
+        file.setStorage(null);
+        // Reload, in case the file contents already changed.
+        reload();
+      } else {
+        textarea.setBackground(Theme.getColor("editor.bgcolor"));
+        textarea.setHighlightCurrentLine(Theme.getBoolean("editor.linehighlight"));
+        textarea.setEditable(true);
+        file.setStorage(this);
+        // Reload once just before disabling external mode, to ensure we have
+        // the latest contents.
+        reload();
+      }
+    }
+    // apply changes to the font size for the editor
+    textarea.setFont(PreferencesData.getFont("editor.font"));
+  }
+  
+  public void updateKeywords(PdeKeywords keywords) {
+    // update GUI for "Find In Reference"
+    textarea.setKeywords(keywords);
+    // update document for syntax highlighting
+    RSyntaxDocument document = (RSyntaxDocument) textarea.getDocument();
+    document.setTokenMakerFactory(new ArduinoTokenMakerFactory(keywords));
+    document.setSyntaxStyle(RSyntaxDocument.SYNTAX_STYLE_CPLUSPLUS);
+  }
+
+  /**
+   * Called when this tab is made the current one, or when it is the current one
+   * and the window is activated.
+   */
+  public void activated() {
+    // When external editing is enabled, reload the text whenever we get activated.
+    if (external) {
+      reload();
+    }
+  }
+
+  /**
+   * Reload the contents of our file.
+   */
+  public void reload() {
+    String text;
+    try {
+      text = file.load();
+    } catch (IOException e) {
+      System.err.println(I18n.format("Warning: Failed to reload file: \"{0}\"",
+                                     file.getFileName()));
+      return;
+    }
+    setText(text);
+    setModified(false);
+  }
+
+  /**
+   * Get the TextArea object for use (not recommended). This should only
+   * be used in obscure cases that really need to hack the internals of the
+   * JEditTextArea. Most tools should only interface via the get/set functions
+   * found in this class. This will maintain compatibility with future releases,
+   * which will not use TextArea.
+   */
+  public SketchTextArea getTextArea() {
+    return textarea;
+  }
+
+  /**
+   * Get the sketch this tab is editing a file from.
+   */
+  public SketchController getSketch() {
+    return editor.getSketchController();
+  }
+  
+  /**
+   * Get the SketchFile that is being edited in this tab.
+   */
+  public SketchFile getSketchFile() {
+    return this.file;
+  }
+  
+  /**
+   * Get the contents of the text area.
+   */
+  public String getText() {
+    return textarea.getText();
+  }
+
+  /**
+   * Replace the entire contents of this tab.
+   */
+  public void setText(String what) {
+    // Remove all highlights, since these will all end up at the start of the
+    // text otherwise. Preserving them is tricky, so better just remove them.
+    textarea.removeAllLineHighlights();
+    // Set the caret update policy to NEVER_UPDATE while completely replacing
+    // the current text. Normally, the caret tracks inserts and deletions, but
+    // replacing the entire text will always make the caret end up at the end,
+    // which isn't really useful. With NEVER_UPDATE, the caret will just keep
+    // its absolute position (number of characters from the start), which isn't
+    // always perfect, but the best we can do without making a diff of the old
+    // and new text and some guesswork.
+    // Note that we cannot use textarea.setText() here, since that first removes
+    // text and then inserts the new text. Even with NEVER_UPDATE, the caret
+    // always makes sure to stay valid, so first removing all text makes it
+    // reset to 0. Also note that simply saving and restoring the caret position
+    // will work, but then the scroll position might change in response to the
+    // caret position.
+    DefaultCaret caret = (DefaultCaret) textarea.getCaret();
+    int policy = caret.getUpdatePolicy();
+    caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
+    try {
+      RSyntaxDocument doc = (RSyntaxDocument)textarea.getDocument();
+      int oldLength = doc.getLength();
+      // The undo manager already seems to group the insert and remove together
+      // automatically, but better be explicit about it.
+      textarea.getUndoManager().beginInternalAtomicEdit();
+      try {
+        doc.insertString(oldLength, what, null);
+        doc.remove(0, oldLength);
+      } catch (BadLocationException e) {
+        System.err.println("Unexpected failure replacing text");
+      } finally {
+        textarea.getUndoManager().endInternalAtomicEdit();
+      }
+    } finally {
+      caret.setUpdatePolicy(policy);
+    }
+  }
+
+  /**
+   * Is the text modified since the last save / load?
+   */
+  public boolean isModified() {
+    return modified;
+  }
+
+  /**
+   * Clear modified status. Should only be called by SketchFile through the
+   * TextStorage interface.
+   */
+  public void clearModified() {
+    setModified(false);
+  }
+
+  private void setModified(boolean value) {
+    if (value != modified) {
+      modified = value;
+      // TODO: Improve decoupling
+      editor.getSketchController().calcModified();
+    }
+  }
+
+  public String getSelectedText() {
+    return textarea.getSelectedText();
+  }
+
+
+  public void setSelectedText(String what) {
+    textarea.replaceSelection(what);
+  }
+
+  public void setSelection(int start, int stop) {
+    textarea.select(start, stop);
+  }
+  
+  public int getScrollPosition() {
+    return scrollPane.getVerticalScrollBar().getValue();
+  }
+    
+  public void setScrollPosition(int pos) {
+    scrollPane.getVerticalScrollBar().setValue(pos);
+  }
+
+  /**
+   * Get the beginning point of the current selection.
+   */
+  public int getSelectionStart() {
+    return textarea.getSelectionStart();
+  }
+
+  /**
+   * Get the end point of the current selection.
+   */
+  public int getSelectionStop() {
+    return textarea.getSelectionEnd();
+  }
+
+  /**
+   * Get text for a specified line.
+   */
+  public String getLineText(int line) {
+    try {
+      return textarea.getText(textarea.getLineStartOffset(line), textarea.getLineEndOffset(line));
+    } catch (BadLocationException e) {
+      return "";
+    }
+  }
+
+  /**
+   * Jump to the given line
+   * @param line The line number to jump to, 1-based. 
+   */
+  public void goToLine(int line) {
+    if (line <= 0) {
+      return;
+    }
+    try {
+      textarea.setCaretPosition(textarea.getLineStartOffset(line - 1));
+    } catch (BadLocationException e) {
+      //ignore
+    }
+  }
+  
+  void handleCut() {
+    textarea.cut();
+  }
+ 
+  void handleCopy() {
+    textarea.copy();
+  }
+  
+  void handlePaste() {
+    textarea.paste();
+  }
+  
+  void handleSelectAll() {
+    textarea.selectAll();
+  }
+
+  void handleCommentUncomment() {
+    Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaToggleCommentAction);
+    action.actionPerformed(null);
+
+  }
+
+  void handleDiscourseCopy() {
+    new DiscourseFormat(editor, this, false).show();
+  }
+
+
+  void handleHTMLCopy() {
+    new DiscourseFormat(editor, this, true).show();
+  }
+
+  void handleIndentOutdent(boolean indent) {
+    if (indent) {
+      Action action = textarea.getActionMap().get(SketchTextAreaEditorKit.rtaIncreaseIndentAction);
+      action.actionPerformed(null);
+    } else {
+      Action action = textarea.getActionMap().get(RSyntaxTextAreaEditorKit.rstaDecreaseIndentAction);
+      action.actionPerformed(null);
+    }
+  }
+
+  void handleUndo() {
+    textarea.undoLastAction();
+  }
+  
+  void handleRedo() {
+    textarea.redoLastAction();
+  }
+  
+  public UndoManager getUndoManager() {
+    return textarea.getUndoManager();
+  }
+  
+  public String getCurrentKeyword() {
+    String text = "";
+    if (textarea.getSelectedText() != null)
+      text = textarea.getSelectedText().trim();
+
+    try {
+      int current = textarea.getCaretPosition();
+      int startOffset = 0;
+      int endIndex = current;
+      String tmp = textarea.getDocument().getText(current, 1);
+      // TODO probably a regexp that matches Arduino lang special chars
+      // already exists.
+      String regexp = "[\\s\\n();\\\\.!='\\[\\]{}]";
+
+      while (!tmp.matches(regexp)) {
+        endIndex++;
+        tmp = textarea.getDocument().getText(endIndex, 1);
+      }
+      // For some reason document index start at 2.
+      // if( current - start < 2 ) return;
+
+      tmp = "";
+      while (!tmp.matches(regexp)) {
+        startOffset++;
+        if (current - startOffset < 0) {
+          tmp = textarea.getDocument().getText(0, 1);
+          break;
+        } else
+          tmp = textarea.getDocument().getText(current - startOffset, 1);
+      }
+      startOffset--;
+
+      int length = endIndex - current + startOffset;
+      text = textarea.getDocument().getText(current - startOffset, length);
+
+    } catch (BadLocationException bl) {
+      bl.printStackTrace();
+    }
+    return text;
+  }
+
+  @Override
+  public boolean requestFocusInWindow() {
+    /** If focus is requested, focus the textarea instead. */
+    return textarea.requestFocusInWindow();
+  }
+
+}
\ No newline at end of file
diff --git a/app/src/processing/app/EditorToolbar.java b/app/src/processing/app/EditorToolbar.java
index e433d372c49..7313baa44a8 100644
--- a/app/src/processing/app/EditorToolbar.java
+++ b/app/src/processing/app/EditorToolbar.java
@@ -27,7 +27,6 @@
 import javax.swing.event.MouseInputListener;
 import java.awt.*;
 import java.awt.event.KeyEvent;
-import java.awt.event.KeyListener;
 import java.awt.event.MouseEvent;
 
 import static processing.app.I18n.tr;
@@ -36,7 +35,7 @@
 /**
  * run/stop/etc buttons for the ide
  */
-public class EditorToolbar extends JComponent implements MouseInputListener, KeyListener {
+public class EditorToolbar extends JComponent implements MouseInputListener, KeyEventDispatcher {
 
   /**
    * Rollover titles for each button.
@@ -136,6 +135,7 @@ public EditorToolbar(Editor editor, JMenu menu) {
 
     addMouseListener(this);
     addMouseMotionListener(this);
+    KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this);
   }
 
   private void loadButtons() {
@@ -440,24 +440,12 @@ public Dimension getMaximumSize() {
     return new Dimension(3000, BUTTON_HEIGHT);
   }
 
-
-  public void keyPressed(KeyEvent e) {
-    if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
-      shiftPressed = true;
-      repaint();
-    }
-  }
-
-
-  public void keyReleased(KeyEvent e) {
-    if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
-      shiftPressed = false;
+  public boolean dispatchKeyEvent(final KeyEvent e) {
+    if (shiftPressed != e.isShiftDown()) {
+      shiftPressed = !shiftPressed;
       repaint();
     }
+    // Return false to continue processing this keyEvent
+    return false;
   }
-
-
-  public void keyTyped(KeyEvent e) {
-  }
-
 }
diff --git a/app/src/processing/app/Sketch.java b/app/src/processing/app/Sketch.java
deleted file mode 100644
index 444888c907d..00000000000
--- a/app/src/processing/app/Sketch.java
+++ /dev/null
@@ -1,1420 +0,0 @@
-/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
-
-/*
-  Part of the Processing project - http://processing.org
-
-  Copyright (c) 2004-10 Ben Fry and Casey Reas
-  Copyright (c) 2001-04 Massachusetts Institute of Technology
-
-  This program is free software; you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation; either version 2 of the License, or
-  (at your option) any later version.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software Foundation,
-  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-*/
-
-package processing.app;
-
-import cc.arduino.Compiler;
-import cc.arduino.CompilerProgressListener;
-import cc.arduino.UploaderUtils;
-import cc.arduino.packages.Uploader;
-import org.apache.commons.codec.digest.DigestUtils;
-import processing.app.debug.RunnerException;
-import processing.app.forms.PasswordAuthorizationDialog;
-import processing.app.helpers.FileUtils;
-import processing.app.helpers.OSUtils;
-import processing.app.helpers.PreferencesMapException;
-import processing.app.packages.LibraryList;
-import processing.app.packages.UserLibrary;
-
-import javax.swing.*;
-import java.awt.*;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static processing.app.I18n.tr;
-
-
-/**
- * Stores information about files in the current sketch
- */
-public class Sketch {
-  private final Editor editor;
-
-  /** true if any of the files have been modified. */
-  private boolean modified;
-
-  private SketchCodeDocument current;
-  private int currentIndex;
-
-  private final SketchData data;
-
-  /**
-   * path is location of the main .pde file, because this is also
-   * simplest to use when opening the file from the finder/explorer.
-   */
-  public Sketch(Editor _editor, File file) throws IOException {
-    editor = _editor;
-    data = new SketchData(file);
-    load();
-  }
-
-
-  /**
-   * Build the list of files.
-   * <P>
-   * Generally this is only done once, rather than
-   * each time a change is made, because otherwise it gets to be
-   * a nightmare to keep track of what files went where, because
-   * not all the data will be saved to disk.
-   * <P>
-   * This also gets called when the main sketch file is renamed,
-   * because the sketch has to be reloaded from a different folder.
-   * <P>
-   * Another exception is when an external editor is in use,
-   * in which case the load happens each time "run" is hit.
-   */
-  private void load() throws IOException {
-    load(false);
-  }
-
-  protected void load(boolean forceUpdate) throws IOException {
-    data.load();
-
-    for (SketchCode code : data.getCodes()) {
-      if (code.getMetadata() == null)
-        code.setMetadata(new SketchCodeDocument(this, code));
-    }
-
-    // set the main file to be the current tab
-    if (editor != null) {
-      setCurrentCode(currentIndex, forceUpdate);
-    }
-  }
-
-
-  private boolean renamingCode;
-
-  /**
-   * Handler for the New Code menu option.
-   */
-  public void handleNewCode() {
-    editor.status.clearState();
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    // if read-only, give an error
-    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
-      // if the files are read-only, need to first do a "save as".
-      Base.showMessage(tr("Sketch is Read-Only"),
-                       tr("Some files are marked \"read-only\", so you'll\n" +
-                         "need to re-save the sketch in another location,\n" +
-                         "and try again."));
-      return;
-    }
-
-    renamingCode = false;
-    editor.status.edit(tr("Name for new file:"), "");
-  }
-
-
-  /**
-   * Handler for the Rename Code menu option.
-   */
-  public void handleRenameCode() {
-    editor.status.clearState();
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    if (currentIndex == 0 && editor.untitled) {
-      Base.showMessage(tr("Sketch is Untitled"),
-                       tr("How about saving the sketch first \n" +
-                         "before trying to rename it?"));
-      return;
-    }
-
-    // if read-only, give an error
-    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
-      // if the files are read-only, need to first do a "save as".
-      Base.showMessage(tr("Sketch is Read-Only"),
-                       tr("Some files are marked \"read-only\", so you'll\n" +
-                         "need to re-save the sketch in another location,\n" +
-                         "and try again."));
-      return;
-    }
-
-    // ask for new name of file (internal to window)
-    // TODO maybe just popup a text area?
-    renamingCode = true;
-    String prompt = (currentIndex == 0) ?
-      "New name for sketch:" : "New name for file:";
-    String oldName = (current.getCode().isExtension("ino")) ?
-      current.getCode().getPrettyName() : current.getCode().getFileName();
-    editor.status.edit(prompt, oldName);
-  }
-
-
-  /**
-   * This is called upon return from entering a new file name.
-   * (that is, from either newCode or renameCode after the prompt)
-   * This code is almost identical for both the newCode and renameCode
-   * cases, so they're kept merged except for right in the middle
-   * where they diverge.
-   */
-  protected void nameCode(String newName) {
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    // Add the extension here, this simplifies some of the logic below.
-    if (newName.indexOf('.') == -1) {
-      newName += "." + getDefaultExtension();
-    }
-
-    // if renaming to the same thing as before, just ignore.
-    // also ignoring case here, because i don't want to write
-    // a bunch of special stuff for each platform
-    // (osx is case insensitive but preserving, windows insensitive,
-    // *nix is sensitive and preserving.. argh)
-    if (renamingCode) {
-      if (newName.equalsIgnoreCase(current.getCode().getFileName()) && OSUtils.isWindows()) {
-        // exit quietly for the 'rename' case.
-        // if it's a 'new' then an error will occur down below
-        return;
-      }
-    }
-
-    newName = newName.trim();
-    if (newName.equals("")) return;
-
-    int dot = newName.indexOf('.');
-    if (dot == 0) {
-      Base.showWarning(tr("Problem with rename"),
-                       tr("The name cannot start with a period."), null);
-      return;
-    }
-
-    String newExtension = newName.substring(dot+1).toLowerCase();
-    if (!validExtension(newExtension)) {
-      Base.showWarning(tr("Problem with rename"),
-                       I18n.format(
-			 tr("\".{0}\" is not a valid extension."), newExtension
-		       ), null);
-      return;
-    }
-
-    // Don't let the user create the main tab as a .java file instead of .pde
-    if (!isDefaultExtension(newExtension)) {
-      if (renamingCode) {  // If creating a new tab, don't show this error
-        if (current.getCode() == data.getCode(0)) {  // If this is the main tab, disallow
-          Base.showWarning(tr("Problem with rename"),
-                           tr("The main file can't use an extension.\n" +
-                             "(It may be time for your to graduate to a\n" +
-                             "\"real\" programming environment)"), null);
-          return;
-        }
-      }
-    }
-
-    // dots are allowed for the .pde and .java, but not in the name
-    // make sure the user didn't name things poo.time.pde
-    // or something like that (nothing against poo time)
-    String shortName = newName.substring(0, dot);
-    String sanitaryName = BaseNoGui.sanitizeName(shortName);
-    if (!shortName.equals(sanitaryName)) {
-      newName = sanitaryName + "." + newExtension;
-    }
-
-    // In Arduino, we want to allow files with the same name but different
-    // extensions, so compare the full names (including extensions).  This
-    // might cause problems: http://dev.processing.org/bugs/show_bug.cgi?id=543
-    for (SketchCode c : data.getCodes()) {
-      if (newName.equalsIgnoreCase(c.getFileName()) && OSUtils.isWindows()) {
-        Base.showMessage(tr("Error"),
-                         I18n.format(
-			   tr("A file named \"{0}\" already exists in \"{1}\""),
-			   c.getFileName(),
-			   data.getFolder().getAbsolutePath()
-			 ));
-        return;
-      }
-    }
-
-    // In Arduino, don't allow a .cpp file with the same name as the sketch,
-    // because the sketch is concatenated into a file with that name as part
-    // of the build process.  
-    if (newName.equals(getName() + ".cpp")) {
-      Base.showMessage(tr("Error"),
-                       tr("You can't have a .cpp file with the same name as the sketch."));
-      return;
-    }
-
-    if (renamingCode && currentIndex == 0) {
-      for (SketchCode code : data.getCodes()) {
-        if (sanitaryName.equalsIgnoreCase(code.getPrettyName()) &&
-          code.isExtension("cpp")) {
-          Base.showMessage(tr("Error"),
-                           I18n.format(tr("You can't rename the sketch to \"{0}\"\n"
-                                           + "because the sketch already has a .cpp file with that name."),
-                                       sanitaryName));
-          return;
-        }
-      }
-    }
-
-
-    File newFile = new File(data.getFolder(), newName);
-//    if (newFile.exists()) {  // yay! users will try anything
-//      Base.showMessage("Error",
-//                       "A file named \"" + newFile + "\" already exists\n" +
-//                       "in \"" + folder.getAbsolutePath() + "\"");
-//      return;
-//    }
-
-//    File newFileHidden = new File(folder, newName + ".x");
-//    if (newFileHidden.exists()) {
-//      // don't let them get away with it if they try to create something
-//      // with the same name as something hidden
-//      Base.showMessage("No Way",
-//                       "A hidden tab with the same name already exists.\n" +
-//                       "Use \"Unhide\" to bring it back.");
-//      return;
-//    }
-
-    if (renamingCode) {
-      if (currentIndex == 0) {
-        // get the new folder name/location
-        String folderName = newName.substring(0, newName.indexOf('.'));
-        File newFolder = new File(data.getFolder().getParentFile(), folderName);
-        if (newFolder.exists()) {
-          Base.showWarning(tr("Cannot Rename"),
-                           I18n.format(
-			     tr("Sorry, a sketch (or folder) named " +
-                               "\"{0}\" already exists."),
-			     newName
-			   ), null);
-          return;
-        }
-
-        // unfortunately this can't be a "save as" because that
-        // only copies the sketch files and the data folder
-        // however this *will* first save the sketch, then rename
-
-        // first get the contents of the editor text area
-        if (current.getCode().isModified()) {
-          current.getCode().setProgram(editor.getText());
-          try {
-            // save this new SketchCode
-            current.getCode().save();
-          } catch (Exception e) {
-            Base.showWarning(tr("Error"), tr("Could not rename the sketch. (0)"), e);
-            return;
-          }
-        }
-
-        if (!current.getCode().renameTo(newFile)) {
-          Base.showWarning(tr("Error"),
-                           I18n.format(
-			     tr("Could not rename \"{0}\" to \"{1}\""),
-			     current.getCode().getFileName(),
-			     newFile.getName()
-			   ), null);
-          return;
-        }
-
-        // save each of the other tabs because this is gonna be re-opened
-        try {
-          for (SketchCode code : data.getCodes()) {
-            code.save();
-          }
-        } catch (Exception e) {
-          Base.showWarning(tr("Error"), tr("Could not rename the sketch. (1)"), e);
-          return;
-        }
-
-        // now rename the sketch folder and re-open
-        boolean success = data.getFolder().renameTo(newFolder);
-        if (!success) {
-          Base.showWarning(tr("Error"), tr("Could not rename the sketch. (2)"), null);
-          return;
-        }
-        // if successful, set base properties for the sketch
-
-        File newMainFile = new File(newFolder, newName + ".ino");
-
-        // having saved everything and renamed the folder and the main .pde,
-        // use the editor to re-open the sketch to re-init state
-        // (unfortunately this will kill positions for carets etc)
-        editor.handleOpenUnchecked(newMainFile,
-                                   currentIndex,
-                                   editor.getSelectionStart(),
-                                   editor.getSelectionStop(),
-                                   editor.getScrollPosition());
-
-        // get the changes into the sketchbook menu
-        // (re-enabled in 0115 to fix bug #332)
-        editor.base.rebuildSketchbookMenus();
-
-      } else {  // else if something besides code[0]
-        if (!current.getCode().renameTo(newFile)) {
-          Base.showWarning(tr("Error"),
-                           I18n.format(
-			     tr("Could not rename \"{0}\" to \"{1}\""),
-			     current.getCode().getFileName(),
-			     newFile.getName()
-			   ), null);
-          return;
-        }
-      }
-
-    } else {  // creating a new file
-      try {
-        if (!newFile.createNewFile()) {
-          // Already checking for IOException, so make our own.
-          throw new IOException(tr("createNewFile() returned false"));
-        }
-      } catch (IOException e) {
-        Base.showWarning(tr("Error"),
-			 I18n.format(
-                           "Could not create the file \"{0}\" in \"{1}\"",
-			   newFile,
-			   data.getFolder().getAbsolutePath()
-			 ), e);
-        return;
-      }
-      ensureExistence();
-      data.addCode((new SketchCodeDocument(this, newFile)).getCode());
-    }
-
-    // sort the entries
-    data.sortCode();
-
-    // set the new guy as current
-    setCurrentCode(newName);
-
-    // update the tabs
-    editor.header.rebuild();
-  }
-
-
-  /**
-   * Remove a piece of code from the sketch and from the disk.
-   */
-  public void handleDeleteCode() throws IOException {
-    editor.status.clearState();
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    // if read-only, give an error
-    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
-      // if the files are read-only, need to first do a "save as".
-      Base.showMessage(tr("Sketch is Read-Only"),
-                       tr("Some files are marked \"read-only\", so you'll\n" +
-                       "need to re-save the sketch in another location,\n" +
-                       "and try again."));
-      return;
-    }
-
-    // confirm deletion with user, yes/no
-    Object[] options = { tr("OK"), tr("Cancel") };
-    String prompt = (currentIndex == 0) ?
-      tr("Are you sure you want to delete this sketch?") :
-      I18n.format(tr("Are you sure you want to delete \"{0}\"?"), current.getCode().getFileNameWithExtensionIfNotIno());
-    int result = JOptionPane.showOptionDialog(editor,
-                                              prompt,
-                                              tr("Delete"),
-                                              JOptionPane.YES_NO_OPTION,
-                                              JOptionPane.QUESTION_MESSAGE,
-                                              null,
-                                              options,
-                                              options[0]);
-    if (result == JOptionPane.YES_OPTION) {
-      if (currentIndex == 0) {
-        // need to unset all the modified flags, otherwise tries
-        // to do a save on the handleNew()
-
-        // delete the entire sketch
-        Base.removeDir(data.getFolder());
-
-        // get the changes into the sketchbook menu
-        //sketchbook.rebuildMenus();
-
-        // make a new sketch, and i think this will rebuild the sketch menu
-        //editor.handleNewUnchecked();
-        //editor.handleClose2();
-        editor.base.handleClose(editor);
-
-      } else {
-        // delete the file
-        if (!current.getCode().deleteFile(BaseNoGui.getBuildFolder(data).toPath())) {
-          Base.showMessage(tr("Couldn't do it"),
-                           I18n.format(tr("Could not delete \"{0}\"."), current.getCode().getFileName()));
-          return;
-        }
-
-        // remove code from the list
-        data.removeCode(current.getCode());
-
-        // just set current tab to the main tab
-        setCurrentCode(0);
-
-        // update the tabs
-        editor.header.repaint();
-      }
-    }
-  }
-
-
-  /**
-   * Move to the previous tab.
-   */
-  public void handlePrevCode() {
-    int prev = currentIndex - 1;
-    if (prev < 0) prev = data.getCodeCount()-1;
-    setCurrentCode(prev);
-  }
-
-
-  /**
-   * Move to the next tab.
-   */
-  public void handleNextCode() {
-    setCurrentCode((currentIndex + 1) % data.getCodeCount());
-  }
-
-
-  /**
-   * Sets the modified value for the code in the frontmost tab.
-   */
-  public void setModified(boolean state) {
-    //System.out.println("setting modified to " + state);
-    //new Exception().printStackTrace();
-    current.getCode().setModified(state);
-    calcModified();
-  }
-
-
-  private void calcModified() {
-    modified = false;
-    for (SketchCode code : data.getCodes()) {
-      if (code.isModified()) {
-        modified = true;
-        break;
-      }
-    }
-    editor.header.repaint();
-
-    if (OSUtils.isMacOS()) {
-      // http://developer.apple.com/qa/qa2001/qa1146.html
-      Object modifiedParam = modified ? Boolean.TRUE : Boolean.FALSE;
-      editor.getRootPane().putClientProperty("windowModified", modifiedParam);
-      editor.getRootPane().putClientProperty("Window.documentModified", modifiedParam);
-    }
-  }
-
-
-  public boolean isModified() {
-    return modified;
-  }
-
-
-  /**
-   * Save all code in the current sketch.
-   */
-  public boolean save() throws IOException {
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    // first get the contents of the editor text area
-    if (current.getCode().isModified()) {
-      current.getCode().setProgram(editor.getText());
-    }
-
-    // don't do anything if not actually modified
-    //if (!modified) return false;
-
-    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
-      Base.showMessage(tr("Sketch is read-only"),
-        tr("Some files are marked \"read-only\", so you'll\n" +
-          "need to re-save this sketch to another location."));
-      return saveAs();
-    }
-
-    // rename .pde files to .ino
-    File mainFile = new File(getMainFilePath());
-    File mainFolder = mainFile.getParentFile();
-    File[] pdeFiles = mainFolder.listFiles((dir, name) -> {
-      return name.toLowerCase().endsWith(".pde");
-    });
-
-    if (pdeFiles != null && pdeFiles.length > 0) {
-      if (PreferencesData.get("editor.update_extension") == null) {
-        Object[] options = {tr("OK"), tr("Cancel")};
-        int result = JOptionPane.showOptionDialog(editor,
-          tr("In Arduino 1.0, the default file extension has changed\n" +
-            "from .pde to .ino.  New sketches (including those created\n" +
-            "by \"Save-As\") will use the new extension.  The extension\n" +
-            "of existing sketches will be updated on save, but you can\n" +
-            "disable this in the Preferences dialog.\n" +
-            "\n" +
-            "Save sketch and update its extension?"),
-          tr(".pde -> .ino"),
-          JOptionPane.OK_CANCEL_OPTION,
-          JOptionPane.QUESTION_MESSAGE,
-          null,
-          options,
-          options[0]);
-
-        if (result != JOptionPane.OK_OPTION) return false; // save cancelled
-
-        PreferencesData.setBoolean("editor.update_extension", true);
-      }
-
-      if (PreferencesData.getBoolean("editor.update_extension")) {
-        // Do rename of all .pde files to new .ino extension
-        for (File pdeFile : pdeFiles)
-          renameCodeToInoExtension(pdeFile);
-      }
-    }
-
-    data.save();
-    calcModified();
-    return true;
-  }
-
-
-  private boolean renameCodeToInoExtension(File pdeFile) {
-    for (SketchCode c : data.getCodes()) {
-      if (!c.getFile().equals(pdeFile))
-        continue;
-
-      String pdeName = pdeFile.getPath();
-      pdeName = pdeName.substring(0, pdeName.length() - 4) + ".ino";
-      return c.renameTo(new File(pdeName));
-    }
-    return false;
-  }
-
-
-  /**
-   * Handles 'Save As' for a sketch.
-   * <P>
-   * This basically just duplicates the current sketch folder to
-   * a new location, and then calls 'Save'. (needs to take the current
-   * state of the open files and save them to the new folder..
-   * but not save over the old versions for the old sketch..)
-   * <P>
-   * Also removes the previously-generated .class and .jar files,
-   * because they can cause trouble.
-   */
-  protected boolean saveAs() throws IOException {
-    // get new name for folder
-    FileDialog fd = new FileDialog(editor, tr("Save sketch folder as..."), FileDialog.SAVE);
-    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()) || isUntitled()) {
-      // default to the sketchbook folder
-      fd.setDirectory(BaseNoGui.getSketchbookFolder().getAbsolutePath());
-    } else {
-      // default to the parent folder of where this was
-      // on macs a .getParentFile() method is required
-
-      fd.setDirectory(data.getFolder().getParentFile().getAbsolutePath());
-    }
-    String oldName = data.getName();
-    fd.setFile(oldName);
-
-    fd.setVisible(true);
-    String newParentDir = fd.getDirectory();
-    String newName = fd.getFile();
-
-    // user canceled selection
-    if (newName == null) return false;
-    newName = Sketch.checkName(newName);
-
-    File newFolder = new File(newParentDir, newName);
-
-    // make sure there doesn't exist a .cpp file with that name already
-    // but ignore this situation for the first tab, since it's probably being
-    // resaved (with the same name) to another location/folder.
-    for (int i = 1; i < data.getCodeCount(); i++) {
-      SketchCode code = data.getCode(i);
-      if (newName.equalsIgnoreCase(code.getPrettyName())) {
-        Base.showMessage(tr("Error"),
-          I18n.format(tr("You can't save the sketch as \"{0}\"\n" +
-            "because the sketch already has a file with that name."), newName
-          ));
-        return false;
-      }
-    }
-
-    // check if the paths are identical
-    if (newFolder.equals(data.getFolder())) {
-      // just use "save" here instead, because the user will have received a
-      // message (from the operating system) about "do you want to replace?"
-      return save();
-    }
-
-    // check to see if the user is trying to save this sketch inside itself
-    try {
-      String newPath = newFolder.getCanonicalPath() + File.separator;
-      String oldPath = data.getFolder().getCanonicalPath() + File.separator;
-
-      if (newPath.indexOf(oldPath) == 0) {
-        Base.showWarning(tr("How very Borges of you"),
-                tr("You cannot save the sketch into a folder\n" +
-                        "inside itself. This would go on forever."), null);
-        return false;
-      }
-    } catch (IOException e) {
-      //ignore
-    }
-
-    // if the new folder already exists, then need to remove
-    // its contents before copying everything over
-    // (user will have already been warned)
-    if (newFolder.exists()) {
-      Base.removeDir(newFolder);
-    }
-    // in fact, you can't do this on windows because the file dialog
-    // will instead put you inside the folder, but it happens on osx a lot.
-
-    // now make a fresh copy of the folder
-    newFolder.mkdirs();
-
-    // grab the contents of the current tab before saving
-    // first get the contents of the editor text area
-    if (current.getCode().isModified()) {
-      current.getCode().setProgram(editor.getText());
-    }
-
-    // save the other tabs to their new location
-    for (SketchCode code : data.getCodes()) {
-      if (data.indexOfCode(code) == 0) continue;
-      File newFile = new File(newFolder, code.getFileName());
-      code.saveAs(newFile);
-    }
-
-    // re-copy the data folder (this may take a while.. add progress bar?)
-    if (data.getDataFolder().exists()) {
-      File newDataFolder = new File(newFolder, "data");
-      Base.copyDir(data.getDataFolder(), newDataFolder);
-    }
-
-    // re-copy the code folder
-    if (data.getCodeFolder().exists()) {
-      File newCodeFolder = new File(newFolder, "code");
-      Base.copyDir(data.getCodeFolder(), newCodeFolder);
-    }
-
-    // copy custom applet.html file if one exists
-    // http://dev.processing.org/bugs/show_bug.cgi?id=485
-    File customHtml = new File(data.getFolder(), "applet.html");
-    if (customHtml.exists()) {
-      File newHtml = new File(newFolder, "applet.html");
-      Base.copyFile(customHtml, newHtml);
-    }
-
-    // save the main tab with its new name
-    File newFile = new File(newFolder, newName + ".ino");
-    data.getCode(0).saveAs(newFile);
-
-    editor.handleOpenUnchecked(newFile,
-            currentIndex,
-            editor.getSelectionStart(),
-            editor.getSelectionStop(),
-            editor.getScrollPosition());
-
-    // Name changed, rebuild the sketch menus
-    //editor.sketchbook.rebuildMenusAsync();
-    editor.base.rebuildSketchbookMenus();
-
-    // Make sure that it's not an untitled sketch
-    setUntitled(false);
-
-    // let Editor know that the save was successful
-    return true;
-  }
-
-
-  /**
-   * Prompt the user for a new file to the sketch, then call the
-   * other addFile() function to actually add it.
-   */
-  public void handleAddFile() {
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    // if read-only, give an error
-    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
-      // if the files are read-only, need to first do a "save as".
-      Base.showMessage(tr("Sketch is Read-Only"),
-                       tr("Some files are marked \"read-only\", so you'll\n" +
-                         "need to re-save the sketch in another location,\n" +
-                         "and try again."));
-      return;
-    }
-
-    // get a dialog, select a file to add to the sketch
-    FileDialog fd = new FileDialog(editor, tr("Select an image or other data file to copy to your sketch"), FileDialog.LOAD);
-    fd.setVisible(true);
-
-    String directory = fd.getDirectory();
-    String filename = fd.getFile();
-    if (filename == null) return;
-
-    // copy the file into the folder. if people would rather
-    // it move instead of copy, they can do it by hand
-    File sourceFile = new File(directory, filename);
-
-    // now do the work of adding the file
-    boolean result = addFile(sourceFile);
-
-    if (result) {
-      editor.statusNotice(tr("One file added to the sketch."));
-      PreferencesData.set("last.folder", sourceFile.getAbsolutePath());
-    }
-  }
-
-
-  /**
-   * Add a file to the sketch.
-   * <p/>
-   * .pde or .java files will be added to the sketch folder. <br/>
-   * .jar, .class, .dll, .jnilib, and .so files will all
-   * be added to the "code" folder. <br/>
-   * All other files will be added to the "data" folder.
-   * <p/>
-   * If they don't exist already, the "code" or "data" folder
-   * will be created.
-   * <p/>
-   * @return true if successful.
-   */
-  public boolean addFile(File sourceFile) {
-    String filename = sourceFile.getName();
-    File destFile = null;
-    String codeExtension = null;
-    boolean replacement = false;
-
-    // if the file appears to be code related, drop it
-    // into the code folder, instead of the data folder
-    if (filename.toLowerCase().endsWith(".o") ||
-        filename.toLowerCase().endsWith(".a") ||
-        filename.toLowerCase().endsWith(".so")) {
-
-      //if (!codeFolder.exists()) codeFolder.mkdirs();
-      prepareCodeFolder();
-      destFile = new File(data.getCodeFolder(), filename);
-
-    } else {
-      for (String extension : SketchData.EXTENSIONS) {
-        String lower = filename.toLowerCase();
-        if (lower.endsWith("." + extension)) {
-          destFile = new File(data.getFolder(), filename);
-          codeExtension = extension;
-        }
-      }
-      if (codeExtension == null) {
-        prepareDataFolder();
-        destFile = new File(data.getDataFolder(), filename);
-      }
-    }
-
-    // check whether this file already exists
-    if (destFile.exists()) {
-      Object[] options = { tr("OK"), tr("Cancel") };
-      String prompt = I18n.format(tr("Replace the existing version of {0}?"), filename);
-      int result = JOptionPane.showOptionDialog(editor,
-                                                prompt,
-                                                tr("Replace"),
-                                                JOptionPane.YES_NO_OPTION,
-                                                JOptionPane.QUESTION_MESSAGE,
-                                                null,
-                                                options,
-                                                options[0]);
-      if (result == JOptionPane.YES_OPTION) {
-        replacement = true;
-      } else {
-        return false;
-      }
-    }
-
-    // If it's a replacement, delete the old file first,
-    // otherwise case changes will not be preserved.
-    // http://dev.processing.org/bugs/show_bug.cgi?id=969
-    if (replacement) {
-      boolean muchSuccess = destFile.delete();
-      if (!muchSuccess) {
-        Base.showWarning(tr("Error adding file"),
-                         I18n.format(tr("Could not delete the existing ''{0}'' file."), filename),
-			 null);
-        return false;
-      }
-    }
-
-    // make sure they aren't the same file
-    if ((codeExtension == null) && sourceFile.equals(destFile)) {
-      Base.showWarning(tr("You can't fool me"),
-                       tr("This file has already been copied to the\n" +
-                         "location from which where you're trying to add it.\n" +
-                         "I ain't not doin nuthin'."), null);
-      return false;
-    }
-
-    // in case the user is "adding" the code in an attempt
-    // to update the sketch's tabs
-    if (!sourceFile.equals(destFile)) {
-      try {
-        Base.copyFile(sourceFile, destFile);
-
-      } catch (IOException e) {
-        Base.showWarning(tr("Error adding file"),
-                         I18n.format(tr("Could not add ''{0}'' to the sketch."), filename),
-			 e);
-        return false;
-      }
-    }
-
-    if (codeExtension != null) {
-      SketchCode newCode = (new SketchCodeDocument(this, destFile)).getCode();
-
-      if (replacement) {
-        data.replaceCode(newCode);
-
-      } else {
-        ensureExistence();
-        data.addCode(newCode);
-        data.sortCode();
-      }
-      setCurrentCode(filename);
-      editor.header.repaint();
-      if (editor.untitled) {  // TODO probably not necessary? problematic?
-        // Mark the new code as modified so that the sketch is saved
-        current.getCode().setModified(true);
-      }
-
-    } else {
-      if (editor.untitled) {  // TODO probably not necessary? problematic?
-        // If a file has been added, mark the main code as modified so
-        // that the sketch is properly saved.
-        data.getCode(0).setModified(true);
-      }
-    }
-    return true;
-  }
-
-
-  public void importLibrary(UserLibrary lib) throws IOException {
-    importLibrary(lib.getSrcFolder());
-  }
-
-  /**
-   * Add import statements to the current tab for all of packages inside
-   * the specified jar file.
-   */
-  private void importLibrary(File jarPath) throws IOException {
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    String list[] = Base.headerListFromIncludePath(jarPath);
-    if (list == null || list.length == 0) {
-      return;
-    }
-
-    // import statements into the main sketch file (code[0])
-    // if the current code is a .java file, insert into current
-    //if (current.flavor == PDE) {
-    if (hasDefaultExtension(current.getCode())) {
-      setCurrentCode(0);
-    }
-    // could also scan the text in the file to see if each import
-    // statement is already in there, but if the user has the import
-    // commented out, then this will be a problem.
-    StringBuilder buffer = new StringBuilder();
-    for (String aList : list) {
-      buffer.append("#include <");
-      buffer.append(aList);
-      buffer.append(">\n");
-    }
-    buffer.append('\n');
-    buffer.append(editor.getText());
-    editor.setText(buffer.toString());
-    editor.setSelection(0, 0);  // scroll to start
-    setModified(true);
-  }
-
-
-  /**
-   * Change what file is currently being edited. Changes the current tab index.
-   * <OL>
-   * <LI> store the String for the text of the current file.
-   * <LI> retrieve the String for the text of the new file.
-   * <LI> change the text that's visible in the text area
-   * </OL>
-   */
-  public void setCurrentCode(int which) {
-    setCurrentCode(which, false);
-  }
-
-  private void setCurrentCode(int which, boolean forceUpdate) {
-    // if current is null, then this is the first setCurrent(0)
-    if (!forceUpdate && (currentIndex == which) && (current != null)) {
-      return;
-    }
-
-    // get the text currently being edited
-    if (current != null) {
-      current.getCode().setProgram(editor.getText());
-      current.setSelectionStart(editor.getSelectionStart());
-      current.setSelectionStop(editor.getSelectionStop());
-      current.setScrollPosition(editor.getScrollPosition());
-    }
-
-    current = (SketchCodeDocument) data.getCode(which).getMetadata();
-    currentIndex = which;
-
-    if (SwingUtilities.isEventDispatchThread()) {
-      editor.setCode(current);
-    } else {
-      try {
-        SwingUtilities.invokeAndWait(() -> editor.setCode(current));
-      } catch (Exception e) {
-        e.printStackTrace();
-      }
-    }
-
-    editor.header.rebuild();
-  }
-
-
-  /**
-   * Internal helper function to set the current tab based on a name.
-   * @param findName the file name (not pretty name) to be shown
-   */
-  protected void setCurrentCode(String findName) {
-    for (SketchCode code : data.getCodes()) {
-      if (findName.equals(code.getFileName()) ||
-          findName.equals(code.getPrettyName())) {
-        setCurrentCode(data.indexOfCode(code));
-        return;
-      }
-    }
-  }
-
-
-  /**
-   * Preprocess, Compile, and Run the current code.
-   * <P>
-   * There are three main parts to this process:
-   * <PRE>
-   *   (0. if not java, then use another 'engine'.. i.e. python)
-   *
-   *    1. do the p5 language preprocessing
-   *       this creates a working .java file in a specific location
-   *       better yet, just takes a chunk of java code and returns a
-   *       new/better string editor can take care of saving this to a
-   *       file location
-   *
-   *    2. compile the code from that location
-   *       catching errors along the way
-   *       placing it in a ready classpath, or .. ?
-   *
-   *    3. run the code
-   *       needs to communicate location for window
-   *       and maybe setup presentation space as well
-   *       run externally if a code folder exists,
-   *       or if more than one file is in the project
-   *
-   *    X. afterwards, some of these steps need a cleanup function
-   * </PRE>
-   */
-  //protected String compile() throws RunnerException {
-
-  /**
-   * When running from the editor, take care of preparations before running
-   * the build.
-   */
-  public void prepare() throws IOException {
-    // make sure the user didn't hide the sketch folder
-    ensureExistence();
-
-    current.getCode().setProgram(editor.getText());
-
-    // TODO record history here
-    //current.history.record(program, SketchHistory.RUN);
-
-    // if an external editor is being used, need to grab the
-    // latest version of the code from the file.
-    if (PreferencesData.getBoolean("editor.external")) {
-      // history gets screwed by the open..
-      //String historySaved = history.lastRecorded;
-      //handleOpen(sketch);
-      //history.lastRecorded = historySaved;
-
-      // nuke previous files and settings, just get things loaded
-      load(true);
-    }
-
-//    // handle preprocessing the main file's code
-//    return build(tempBuildFolder.getAbsolutePath());
-  }
-
-  /**
-   * Run the build inside the temporary build folder.
-   * @return null if compilation failed, main class name if not
-   * @throws RunnerException
-   */
-  public String build(boolean verbose, boolean save) throws RunnerException, PreferencesMapException, IOException {
-    return build(BaseNoGui.getBuildFolder(data).getAbsolutePath(), verbose, save);
-  }
-
-  /**
-   * Preprocess and compile all the code for this sketch.
-   *
-   * In an advanced program, the returned class name could be different,
-   * which is why the className is set based on the return value.
-   * A compilation error will burp up a RunnerException.
-   *
-   * @return null if compilation failed, main class name if not
-   */
-  private String build(String buildPath, boolean verbose, boolean save) throws RunnerException, PreferencesMapException, IOException {
-    // run the preprocessor
-    editor.status.progressUpdate(20);
-
-    ensureExistence();
-
-    CompilerProgressListener progressListener = editor.status::progressUpdate;
-
-    boolean deleteTemp = false;
-    String pathToSketch = data.getMainFilePath();
-    if (isModified()) {
-      // If any files are modified, make a copy of the sketch with the changes
-      // saved, so arduino-builder will see the modifications.
-      pathToSketch = saveSketchInTempFolder();
-      deleteTemp = true;
-    }
-
-    try {
-      return new Compiler(pathToSketch, data, buildPath).build(progressListener,
-                                                               save);
-    } finally {
-      // Make sure we clean up any temporary sketch copy
-      if (deleteTemp)
-        FileUtils.recursiveDelete(new File(pathToSketch).getParentFile());
-    }
-  }
-
-  private String saveSketchInTempFolder() throws IOException {
-    File tempFolder = FileUtils.createTempFolder("arduino_modified_sketch_");
-    FileUtils.copy(getFolder(), tempFolder);
-
-    for (SketchCode sc : Stream.of(data.getCodes()).filter(SketchCode::isModified).collect(Collectors.toList())) {
-      Files.write(Paths.get(tempFolder.getAbsolutePath(), sc.getFileName()), sc.getProgram().getBytes());
-    }
-
-    return Paths.get(tempFolder.getAbsolutePath(), data.getPrimaryFile().getName()).toString();
-  }
-
-  protected boolean exportApplet(boolean usingProgrammer) throws Exception {
-    return exportApplet(BaseNoGui.getBuildFolder(data).getAbsolutePath(), usingProgrammer);
-  }
-
-
-  /**
-   * Handle export to applet.
-   */
-  private boolean exportApplet(String appletPath, boolean usingProgrammer)
-    throws Exception {
-
-    prepare();
-
-    // build the sketch
-    editor.status.progressNotice(tr("Compiling sketch..."));
-    String foundName = build(appletPath, false, false);
-    // (already reported) error during export, exit this function
-    if (foundName == null) return false;
-
-//    // If name != exportSketchName, then that's weirdness
-//    // BUG unfortunately, that can also be a bug in the preproc :(
-//    if (!name.equals(foundName)) {
-//      Base.showWarning("Error during export",
-//                       "Sketch name is " + name + " but the sketch\n" +
-//                       "name in the code was " + foundName, null);
-//      return false;
-//    }
-
-    editor.status.progressNotice(tr("Uploading..."));
-    boolean success = upload(appletPath, foundName, usingProgrammer);
-    editor.status.progressUpdate(100);
-    return success;
-  }
-
-  private boolean upload(String buildPath, String suggestedClassName, boolean usingProgrammer) throws Exception {
-
-    Uploader uploader = new UploaderUtils().getUploaderByPreferences(false);
-
-    boolean success = false;
-    do {
-      if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) {
-        PasswordAuthorizationDialog dialog = new PasswordAuthorizationDialog(editor, tr("Type board password to upload a new sketch"));
-        dialog.setLocationRelativeTo(editor);
-        dialog.setVisible(true);
-
-        if (dialog.isCancelled()) {
-          editor.statusNotice(tr("Upload cancelled"));
-          return false;
-        }
-
-        PreferencesData.set(uploader.getAuthorizationKey(), dialog.getPassword());
-      }
-
-      List<String> warningsAccumulator = new LinkedList<>();
-      try {
-        success = new UploaderUtils().upload(data, uploader, buildPath, suggestedClassName, usingProgrammer, false, warningsAccumulator);
-      } finally {
-        if (uploader.requiresAuthorization() && !success) {
-          PreferencesData.remove(uploader.getAuthorizationKey());
-        }
-      }
-
-      for (String warning : warningsAccumulator) {
-        System.out.print(tr("Warning"));
-        System.out.print(": ");
-        System.out.println(warning);
-      }
-
-    } while (uploader.requiresAuthorization() && !success);
-
-    return success;
-  }
-
-  /**
-   * Make sure the sketch hasn't been moved or deleted by some
-   * nefarious user. If they did, try to re-create it and save.
-   * Only checks to see if the main folder is still around,
-   * but not its contents.
-   */
-  private void ensureExistence() {
-    if (data.getFolder().exists()) return;
-
-    Base.showWarning(tr("Sketch Disappeared"),
-                     tr("The sketch folder has disappeared.\n " +
-                       "Will attempt to re-save in the same location,\n" +
-                       "but anything besides the code will be lost."), null);
-    try {
-      data.getFolder().mkdirs();
-      modified = true;
-
-      for (SketchCode code : data.getCodes()) {
-        code.save();  // this will force a save
-      }
-      calcModified();
-
-    } catch (Exception e) {
-      Base.showWarning(tr("Could not re-save sketch"),
-                       tr("Could not properly re-save the sketch. " +
-                         "You may be in trouble at this point,\n" +
-                         "and it might be time to copy and paste " +
-                         "your code to another text editor."), e);
-    }
-  }
-
-
-  /**
-   * Returns true if this is a read-only sketch. Used for the
-   * examples directory, or when sketches are loaded from read-only
-   * volumes or folders without appropriate permissions.
-   */
-  public boolean isReadOnly(LibraryList libraries, String examplesPath) {
-    String apath = data.getFolder().getAbsolutePath();
-
-    Optional<UserLibrary> libraryThatIncludesSketch = libraries.stream().filter(lib -> apath.startsWith(lib.getInstalledFolder().getAbsolutePath())).findFirst();
-    if (libraryThatIncludesSketch.isPresent() && !libraryThatIncludesSketch.get().onGoingDevelopment()) {
-      return true;
-    }
-
-    return sketchIsSystemExample(apath, examplesPath) || sketchFilesAreReadOnly();
-  }
-
-  private boolean sketchIsSystemExample(String apath, String examplesPath) {
-    return apath.startsWith(examplesPath);
-  }
-
-  private boolean sketchFilesAreReadOnly() {
-    for (SketchCode code : data.getCodes()) {
-      if (code.isModified() && code.fileReadOnly() && code.fileExists()) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
-
-  // Breaking out extension types in order to clean up the code, and make it
-  // easier for other environments (like Arduino) to incorporate changes.
-
-  /**
-   * True if the specified code has the default file extension.
-   */
-  private boolean hasDefaultExtension(SketchCode code) {
-    return code.isExtension(getDefaultExtension());
-  }
-
-
-  /**
-   * True if the specified extension is the default file extension.
-   */
-  private boolean isDefaultExtension(String what) {
-    return what.equals(getDefaultExtension());
-  }
-
-
-  /**
-   * Check this extension (no dots, please) against the list of valid
-   * extensions.
-   */
-  private boolean validExtension(String what) {
-    return SketchData.EXTENSIONS.contains(what);
-  }
-
-
-  /**
-   * Returns the default extension for this editor setup.
-   */
-  public String getDefaultExtension() {
-    return data.getDefaultExtension();
-  }
-
-  static private final List<String> hiddenExtensions = Arrays.asList("ino", "pde");
-
-  public List<String> getHiddenExtensions() {
-    return hiddenExtensions;
-  }
-
-
-  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
-
-  // Additional accessors added in 0136 because of package work.
-  // These will also be helpful for tool developers.
-
-
-  /**
-   * Returns the name of this sketch. (The pretty name of the main tab.)
-   */
-  public String getName() {
-    return data.getName();
-  }
-
-
-  /**
-   * Returns path to the main .pde file for this sketch.
-   */
-  public String getMainFilePath() {
-    return data.getMainFilePath();
-  }
-
-
-  /**
-   * Returns the sketch folder.
-   */
-  public File getFolder() {
-    return data.getFolder();
-  }
-
-
-  /**
-   * Create the data folder if it does not exist already. As a convenience,
-   * it also returns the data folder, since it's likely about to be used.
-   */
-  private File prepareDataFolder() {
-    if (!data.getDataFolder().exists()) {
-      data.getDataFolder().mkdirs();
-    }
-    return data.getDataFolder();
-  }
-
-
-  /**
-   * Create the code folder if it does not exist already. As a convenience,
-   * it also returns the code folder, since it's likely about to be used.
-   */
-  private File prepareCodeFolder() {
-    if (!data.getCodeFolder().exists()) {
-      data.getCodeFolder().mkdirs();
-    }
-    return data.getCodeFolder();
-  }
-
-
-  public SketchCode[] getCodes() {
-    return data.getCodes();
-  }
-
-
-  public int getCodeCount() {
-    return data.getCodeCount();
-  }
-
-
-  public SketchCode getCode(int index) {
-    return data.getCode(index);
-  }
-
-
-  public int getCodeIndex(SketchCode who) {
-    return data.indexOfCode(who);
-  }
-
-
-  public SketchCode getCurrentCode() {
-    return current.getCode();
-  }
-
-
-  private void setUntitled(boolean u) {
-    editor.untitled = u;
-  }
-
-
-  public boolean isUntitled() {
-    return editor.untitled;
-  }
-
-
-  // .................................................................
-
-
-  /**
-   * Convert to sanitized name and alert the user
-   * if changes were made.
-   */
-  private static String checkName(String origName) {
-    String newName = BaseNoGui.sanitizeName(origName);
-
-    if (!newName.equals(origName)) {
-      String msg =
-        tr("The sketch name had to be modified. Sketch names can only consist\n" +
-          "of ASCII characters and numbers (but cannot start with a number).\n" +
-          "They should also be less than 64 characters long.");
-      System.out.println(msg);
-    }
-    return newName;
-  }
-
-
-}
diff --git a/app/src/processing/app/SketchCodeDocument.java b/app/src/processing/app/SketchCodeDocument.java
deleted file mode 100644
index 681f0af9151..00000000000
--- a/app/src/processing/app/SketchCodeDocument.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package processing.app;
-
-import java.io.File;
-
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
-import javax.swing.text.Document;
-import javax.swing.undo.UndoManager;
-
-public class SketchCodeDocument implements DocumentListener{
-
-  private SketchCode code;
-  private Sketch sketch;
-  private Document document;
-
-  // Undo Manager for this tab, each tab keeps track of their own Editor.undo
-  // will be set to this object when this code is the tab that's currently the
-  // front.
-  private UndoManager undo;
-
-  // saved positions from last time this tab was used
-  private int selectionStart;
-  private int selectionStop;
-  private int scrollPosition;
-
-  public SketchCodeDocument(Sketch sketch, SketchCode code) {
-    this.code = code;
-    this.sketch = sketch;
-    this.code.setMetadata(this);
-  }
-
-  public SketchCodeDocument(Sketch sketch, File file) {
-    this.code = new SketchCode(file, this);
-    this.sketch = sketch;
-  }
-
-  public UndoManager getUndo() {
-    return undo;
-  }
-
-  public void setUndo(UndoManager undo) {
-    this.undo = undo;
-  }
-
-  public int getSelectionStart() {
-    return selectionStart;
-  }
-
-  public void setSelectionStart(int selectionStart) {
-    this.selectionStart = selectionStart;
-  }
-
-  public int getSelectionStop() {
-    return selectionStop;
-  }
-
-  public void setSelectionStop(int selectionStop) {
-    this.selectionStop = selectionStop;
-  }
-
-  public int getScrollPosition() {
-    return scrollPosition;
-  }
-
-  public void setScrollPosition(int scrollPosition) {
-    this.scrollPosition = scrollPosition;
-  }
-
-  public SketchCode getCode() {
-    return code;
-  }
-
-  public void setCode(SketchCode code) {
-    this.code = code;
-  }
-
-  public Document getDocument() {
-    return document;
-  }
-
-  public void setDocument(Document document) {
-    this.document = document;
-    document.addDocumentListener(this);
-  }
-
-  @Override
-  public void insertUpdate(DocumentEvent e) {
-    if(!code.isModified()) sketch.setModified(true);
-  }
-
-
-  @Override
-  public void removeUpdate(DocumentEvent e) {
-    if(!code.isModified()) sketch.setModified(true);
-  }
-
-  @Override
-  public void changedUpdate(DocumentEvent e) {
-     // Callback for when styles in the current document change.
-     // This method is never called.
-  }
-  
-}
diff --git a/app/src/processing/app/SketchController.java b/app/src/processing/app/SketchController.java
new file mode 100644
index 00000000000..8c6dd1c8519
--- /dev/null
+++ b/app/src/processing/app/SketchController.java
@@ -0,0 +1,821 @@
+/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
+
+/*
+  Part of the Processing project - http://processing.org
+
+  Copyright (c) 2004-10 Ben Fry and Casey Reas
+  Copyright (c) 2001-04 Massachusetts Institute of Technology
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software Foundation,
+  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+
+package processing.app;
+
+import cc.arduino.Compiler;
+import cc.arduino.CompilerProgressListener;
+import cc.arduino.UploaderUtils;
+import cc.arduino.packages.Uploader;
+import processing.app.debug.RunnerException;
+import processing.app.forms.PasswordAuthorizationDialog;
+import processing.app.helpers.FileUtils;
+import processing.app.helpers.OSUtils;
+import processing.app.helpers.PreferencesMapException;
+import processing.app.packages.LibraryList;
+import processing.app.packages.UserLibrary;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static processing.app.I18n.tr;
+
+
+/**
+ * Handles various tasks related to a sketch, in response to user inter-action.
+ */
+public class SketchController {
+  private final Editor editor;
+  private final Sketch sketch;
+
+  public SketchController(Editor _editor, Sketch _sketch) {
+    editor = _editor;
+    sketch = _sketch;
+  }
+
+  private boolean renamingCode;
+
+  /**
+   * Handler for the New Code menu option.
+   */
+  public void handleNewCode() {
+    editor.status.clearState();
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    // if read-only, give an error
+    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
+      // if the files are read-only, need to first do a "save as".
+      Base.showMessage(tr("Sketch is Read-Only"),
+                       tr("Some files are marked \"read-only\", so you'll\n" +
+                         "need to re-save the sketch in another location,\n" +
+                         "and try again."));
+      return;
+    }
+
+    renamingCode = false;
+    editor.status.edit(tr("Name for new file:"), "");
+  }
+
+
+  /**
+   * Handler for the Rename Code menu option.
+   */
+  public void handleRenameCode() {
+    SketchFile current = editor.getCurrentTab().getSketchFile();
+
+    editor.status.clearState();
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    if (current.isPrimary() && editor.untitled) {
+      Base.showMessage(tr("Sketch is Untitled"),
+                       tr("How about saving the sketch first \n" +
+                         "before trying to rename it?"));
+      return;
+    }
+
+    // if read-only, give an error
+    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
+      // if the files are read-only, need to first do a "save as".
+      Base.showMessage(tr("Sketch is Read-Only"),
+                       tr("Some files are marked \"read-only\", so you'll\n" +
+                         "need to re-save the sketch in another location,\n" +
+                         "and try again."));
+      return;
+    }
+
+    // ask for new name of file (internal to window)
+    // TODO maybe just popup a text area?
+    renamingCode = true;
+    String prompt = current.isPrimary() ?
+      "New name for sketch:" : "New name for file:";
+    String oldName = current.getPrettyName();
+    editor.status.edit(prompt, oldName);
+  }
+
+
+  /**
+   * This is called upon return from entering a new file name.
+   * (that is, from either newCode or renameCode after the prompt)
+   * This code is almost identical for both the newCode and renameCode
+   * cases, so they're kept merged except for right in the middle
+   * where they diverge.
+   */
+  protected void nameCode(String newName) {
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    newName = newName.trim();
+    if (newName.equals("")) return;
+
+    if (newName.charAt(0) == '.') {
+      Base.showWarning(tr("Problem with rename"),
+                       tr("The name cannot start with a period."), null);
+      return;
+    }
+
+    FileUtils.SplitFile split = FileUtils.splitFilename(newName);
+    if (split.extension.equals(""))
+      split.extension = Sketch.DEFAULT_SKETCH_EXTENSION;
+
+    if (!Sketch.EXTENSIONS.contains(split.extension)) {
+      String msg = I18n.format(tr("\".{0}\" is not a valid extension."),
+                               split.extension);
+      Base.showWarning(tr("Problem with rename"), msg, null);
+      return;
+    }
+
+    // Sanitize name
+    split.basename = BaseNoGui.sanitizeName(split.basename);
+    newName = split.join();
+
+    if (renamingCode) {
+      SketchFile current = editor.getCurrentTab().getSketchFile();
+
+      if (current.isPrimary()) {
+        if (!split.extension.equals(Sketch.DEFAULT_SKETCH_EXTENSION)) {
+          Base.showWarning(tr("Problem with rename"),
+                           tr("The main file cannot use an extension"), null);
+          return;
+        }
+
+        // Primary file, rename the entire sketch
+        final File parent = sketch.getFolder().getParentFile();
+        File newFolder = new File(parent, split.basename);
+        try {
+          sketch.renameTo(newFolder);
+        } catch (IOException e) {
+          // This does not pass on e, to prevent showing a backtrace for
+          // "normal" errors.
+          Base.showWarning(tr("Error"), e.getMessage(), null);
+          return;
+        }
+
+        editor.base.rebuildSketchbookMenus();
+      } else {
+        // Non-primary file, rename just that file
+        try {
+          current.renameTo(newName);
+        } catch (IOException e) {
+          // This does not pass on e, to prevent showing a backtrace for
+          // "normal" errors.
+          Base.showWarning(tr("Error"), e.getMessage(), null);
+          return;
+        }
+      }
+
+    } else {  // creating a new file
+      SketchFile file;
+      try {
+        file = sketch.addFile(newName);
+        editor.addTab(file, "");
+      } catch (IOException e) {
+        // This does not pass on e, to prevent showing a backtrace for
+        // "normal" errors.
+        Base.showWarning(tr("Error"), e.getMessage(), null);
+        return;
+      }
+      editor.selectTab(editor.findTabIndex(file));
+    }
+
+    // update the tabs
+    editor.header.rebuild();
+  }
+
+
+  /**
+   * Remove a piece of code from the sketch and from the disk.
+   */
+  public void handleDeleteCode() throws IOException {
+    SketchFile current = editor.getCurrentTab().getSketchFile();
+    editor.status.clearState();
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    // if read-only, give an error
+    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
+      // if the files are read-only, need to first do a "save as".
+      Base.showMessage(tr("Sketch is Read-Only"),
+                       tr("Some files are marked \"read-only\", so you'll\n" +
+                       "need to re-save the sketch in another location,\n" +
+                       "and try again."));
+      return;
+    }
+
+    // confirm deletion with user, yes/no
+    Object[] options = { tr("OK"), tr("Cancel") };
+    String prompt = current.isPrimary() ?
+      tr("Are you sure you want to delete this sketch?") :
+      I18n.format(tr("Are you sure you want to delete \"{0}\"?"),
+                                                      current.getPrettyName());
+    int result = JOptionPane.showOptionDialog(editor,
+                                              prompt,
+                                              tr("Delete"),
+                                              JOptionPane.YES_NO_OPTION,
+                                              JOptionPane.QUESTION_MESSAGE,
+                                              null,
+                                              options,
+                                              options[0]);
+    if (result == JOptionPane.YES_OPTION) {
+      if (current.isPrimary()) {
+        sketch.delete();
+        editor.base.handleClose(editor);
+      } else {
+        // delete the file
+        if (!current.delete(sketch.getBuildPath().toPath())) {
+          Base.showMessage(tr("Couldn't do it"),
+                           I18n.format(tr("Could not delete \"{0}\"."), current.getFileName()));
+          return;
+        }
+
+        // just set current tab to the main tab
+        editor.selectTab(0);
+
+        // update the tabs
+        editor.header.repaint();
+      }
+    }
+  }
+
+  /**
+   * Called whenever the modification status of one of the tabs changes. TODO:
+   * Move this code into Editor and improve decoupling from EditorTab
+   */
+  public void calcModified() {
+    editor.header.repaint();
+
+    if (OSUtils.isMacOS()) {
+      // http://developer.apple.com/qa/qa2001/qa1146.html
+      Object modifiedParam = sketch.isModified() ? Boolean.TRUE : Boolean.FALSE;
+      editor.getRootPane().putClientProperty("windowModified", modifiedParam);
+      editor.getRootPane().putClientProperty("Window.documentModified", modifiedParam);
+    }
+  }
+
+
+
+  /**
+   * Save all code in the current sketch.
+   */
+  public boolean save() throws IOException {
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
+      Base.showMessage(tr("Sketch is read-only"),
+        tr("Some files are marked \"read-only\", so you'll\n" +
+          "need to re-save this sketch to another location."));
+      return saveAs();
+    }
+
+    // rename .pde files to .ino
+    List<SketchFile> oldFiles = new ArrayList<>();
+    for (SketchFile file : sketch.getFiles()) {
+      if (file.isExtension(Sketch.OLD_SKETCH_EXTENSIONS))
+        oldFiles.add(file);
+    }
+
+    if (oldFiles.size() > 0) {
+      if (PreferencesData.get("editor.update_extension") == null) {
+        Object[] options = {tr("OK"), tr("Cancel")};
+        int result = JOptionPane.showOptionDialog(editor,
+          tr("In Arduino 1.0, the default file extension has changed\n" +
+            "from .pde to .ino.  New sketches (including those created\n" +
+            "by \"Save-As\") will use the new extension.  The extension\n" +
+            "of existing sketches will be updated on save, but you can\n" +
+            "disable this in the Preferences dialog.\n" +
+            "\n" +
+            "Save sketch and update its extension?"),
+          tr(".pde -> .ino"),
+          JOptionPane.OK_CANCEL_OPTION,
+          JOptionPane.QUESTION_MESSAGE,
+          null,
+          options,
+          options[0]);
+
+        if (result != JOptionPane.OK_OPTION) return false; // save cancelled
+
+        PreferencesData.setBoolean("editor.update_extension", true);
+      }
+
+      if (PreferencesData.getBoolean("editor.update_extension")) {
+        // Do rename of all .pde files to new .ino extension
+        for (SketchFile file : oldFiles) {
+          File newName = FileUtils.replaceExtension(file.getFile(), Sketch.DEFAULT_SKETCH_EXTENSION);
+          file.renameTo(newName.getName());
+        }
+      }
+    }
+
+    sketch.save();
+    return true;
+  }
+
+  /**
+   * Handles 'Save As' for a sketch.
+   * <P>
+   * This basically just duplicates the current sketch folder to
+   * a new location, and then calls 'Save'. (needs to take the current
+   * state of the open files and save them to the new folder..
+   * but not save over the old versions for the old sketch..)
+   * <P>
+   * Also removes the previously-generated .class and .jar files,
+   * because they can cause trouble.
+   */
+  protected boolean saveAs() throws IOException {
+    // get new name for folder
+    FileDialog fd = new FileDialog(editor, tr("Save sketch folder as..."), FileDialog.SAVE);
+    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath()) || isUntitled()) {
+      // default to the sketchbook folder
+      fd.setDirectory(BaseNoGui.getSketchbookFolder().getAbsolutePath());
+    } else {
+      // default to the parent folder of where this was
+      // on macs a .getParentFile() method is required
+
+      fd.setDirectory(sketch.getFolder().getParentFile().getAbsolutePath());
+    }
+    String oldName = sketch.getName();
+    fd.setFile(oldName);
+
+    fd.setVisible(true);
+    String newParentDir = fd.getDirectory();
+    String newName = fd.getFile();
+
+    // user canceled selection
+    if (newName == null) return false;
+    newName = SketchController.checkName(newName);
+
+    File newFolder = new File(newParentDir, newName);
+
+    // check if the paths are identical
+    if (newFolder.equals(sketch.getFolder())) {
+      // just use "save" here instead, because the user will have received a
+      // message (from the operating system) about "do you want to replace?"
+      return save();
+    }
+
+    // check to see if the user is trying to save this sketch inside itself
+    try {
+      String newPath = newFolder.getCanonicalPath() + File.separator;
+      String oldPath = sketch.getFolder().getCanonicalPath() + File.separator;
+
+      if (newPath.indexOf(oldPath) == 0) {
+        Base.showWarning(tr("How very Borges of you"),
+                tr("You cannot save the sketch into a folder\n" +
+                        "inside itself. This would go on forever."), null);
+        return false;
+      }
+    } catch (IOException e) {
+      //ignore
+    }
+
+    // if the new folder already exists, then need to remove
+    // its contents before copying everything over
+    // (user will have already been warned)
+    if (newFolder.exists()) {
+      FileUtils.recursiveDelete(newFolder);
+    }
+    // in fact, you can't do this on windows because the file dialog
+    // will instead put you inside the folder, but it happens on osx a lot.
+
+    try {
+      sketch.saveAs(newFolder);
+    } catch (IOException e) {
+      // This does not pass on e, to prevent showing a backtrace for "normal"
+      // errors.
+      Base.showWarning(tr("Error"), e.getMessage(), null);
+    }
+    // Name changed, rebuild the sketch menus
+    //editor.sketchbook.rebuildMenusAsync();
+    editor.base.rebuildSketchbookMenus();
+    editor.header.rebuild();
+
+    // Make sure that it's not an untitled sketch
+    setUntitled(false);
+
+    // let Editor know that the save was successful
+    return true;
+  }
+
+
+  /**
+   * Prompt the user for a new file to the sketch, then call the
+   * other addFile() function to actually add it.
+   */
+  public void handleAddFile() {
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    // if read-only, give an error
+    if (isReadOnly(BaseNoGui.librariesIndexer.getInstalledLibraries(), BaseNoGui.getExamplesPath())) {
+      // if the files are read-only, need to first do a "save as".
+      Base.showMessage(tr("Sketch is Read-Only"),
+                       tr("Some files are marked \"read-only\", so you'll\n" +
+                         "need to re-save the sketch in another location,\n" +
+                         "and try again."));
+      return;
+    }
+
+    // get a dialog, select a file to add to the sketch
+    FileDialog fd = new FileDialog(editor, tr("Select an image or other data file to copy to your sketch"), FileDialog.LOAD);
+    fd.setVisible(true);
+
+    String directory = fd.getDirectory();
+    String filename = fd.getFile();
+    if (filename == null) return;
+
+    // copy the file into the folder. if people would rather
+    // it move instead of copy, they can do it by hand
+    File sourceFile = new File(directory, filename);
+
+    // now do the work of adding the file
+    boolean result = addFile(sourceFile);
+
+    if (result) {
+      editor.statusNotice(tr("One file added to the sketch."));
+      PreferencesData.set("last.folder", sourceFile.getAbsolutePath());
+    }
+  }
+
+
+  /**
+   * Add a file to the sketch.
+   *
+   * Supported code files will be copied to the sketch folder. All other files
+   * will be copied to the "data" folder (which is created if it does not exist
+   * yet).
+   * 
+   * @return true if successful.
+   */
+  public boolean addFile(File sourceFile) {
+    String filename = sourceFile.getName();
+    File destFile = null;
+    boolean isData = false;
+    boolean replacement = false;
+
+    if (FileUtils.hasExtension(sourceFile, Sketch.EXTENSIONS)) {
+      destFile = new File(sketch.getFolder(), filename);
+    } else {
+      sketch.prepareDataFolder();
+      destFile = new File(sketch.getDataFolder(), filename);
+      isData = true;
+    }
+
+    // check whether this file already exists
+    if (destFile.exists()) {
+      Object[] options = { tr("OK"), tr("Cancel") };
+      String prompt = I18n.format(tr("Replace the existing version of {0}?"), filename);
+      int result = JOptionPane.showOptionDialog(editor,
+                                                prompt,
+                                                tr("Replace"),
+                                                JOptionPane.YES_NO_OPTION,
+                                                JOptionPane.QUESTION_MESSAGE,
+                                                null,
+                                                options,
+                                                options[0]);
+      if (result == JOptionPane.YES_OPTION) {
+        replacement = true;
+      } else {
+        return false;
+      }
+    }
+
+    // If it's a replacement, delete the old file first,
+    // otherwise case changes will not be preserved.
+    // http://dev.processing.org/bugs/show_bug.cgi?id=969
+    if (replacement) {
+      boolean muchSuccess = destFile.delete();
+      if (!muchSuccess) {
+        Base.showWarning(tr("Error adding file"),
+                         I18n.format(tr("Could not delete the existing ''{0}'' file."), filename),
+			 null);
+        return false;
+      }
+    }
+
+    // make sure they aren't the same file
+    if (isData && sourceFile.equals(destFile)) {
+      Base.showWarning(tr("You can't fool me"),
+                       tr("This file has already been copied to the\n" +
+                         "location from which where you're trying to add it.\n" +
+                         "I ain't not doin nuthin'."), null);
+      return false;
+    }
+
+    // in case the user is "adding" the code in an attempt
+    // to update the sketch's tabs
+    if (!sourceFile.equals(destFile)) {
+      try {
+        Base.copyFile(sourceFile, destFile);
+
+      } catch (IOException e) {
+        Base.showWarning(tr("Error adding file"),
+                         I18n.format(tr("Could not add ''{0}'' to the sketch."), filename),
+			 e);
+        return false;
+      }
+    }
+
+    if (!isData) {
+      int tabIndex;
+      if (replacement) {
+        tabIndex = editor.findTabIndex(destFile);
+        editor.getTabs().get(tabIndex).reload();
+      } else {
+        SketchFile sketchFile;
+        try {
+          sketchFile = sketch.addFile(destFile.getName());
+          editor.addTab(sketchFile, null);
+        } catch (IOException e) {
+          // This does not pass on e, to prevent showing a backtrace for
+          // "normal" errors.
+          Base.showWarning(tr("Error"), e.getMessage(), null);
+          return false;
+        }
+        tabIndex = editor.findTabIndex(sketchFile);
+      }
+      editor.selectTab(tabIndex);
+    }
+    return true;
+  }
+
+
+  public void importLibrary(UserLibrary lib) throws IOException {
+    importLibrary(lib.getSrcFolder());
+  }
+
+  /**
+   * Add import statements to the current tab for all of packages inside
+   * the specified jar file.
+   */
+  private void importLibrary(File jarPath) throws IOException {
+    // make sure the user didn't hide the sketch folder
+    ensureExistence();
+
+    String list[] = Base.headerListFromIncludePath(jarPath);
+    if (list == null || list.length == 0) {
+      return;
+    }
+
+    // import statements into the main sketch file (code[0])
+    // if the current code is a .java file, insert into current
+    //if (current.flavor == PDE) {
+    SketchFile file = editor.getCurrentTab().getSketchFile();
+    if (file.isExtension(Sketch.SKETCH_EXTENSIONS))
+      editor.selectTab(0);
+
+    // could also scan the text in the file to see if each import
+    // statement is already in there, but if the user has the import
+    // commented out, then this will be a problem.
+    StringBuilder buffer = new StringBuilder();
+    for (String aList : list) {
+      buffer.append("#include <");
+      buffer.append(aList);
+      buffer.append(">\n");
+    }
+    buffer.append('\n');
+    buffer.append(editor.getCurrentTab().getText());
+    editor.getCurrentTab().setText(buffer.toString());
+    editor.getCurrentTab().setSelection(0, 0);  // scroll to start
+  }
+
+  /**
+   * Preprocess and compile all the code for this sketch.
+   *
+   * In an advanced program, the returned class name could be different,
+   * which is why the className is set based on the return value.
+   * A compilation error will burp up a RunnerException.
+   *
+   * @return null if compilation failed, main class name if not
+   */
+  public String build(boolean verbose, boolean save) throws RunnerException, PreferencesMapException, IOException {
+    // run the preprocessor
+    editor.status.progressUpdate(20);
+
+    ensureExistence();
+
+    CompilerProgressListener progressListener = editor.status::progressUpdate;
+
+    boolean deleteTemp = false;
+    File pathToSketch = sketch.getPrimaryFile().getFile();
+    if (sketch.isModified()) {
+      // If any files are modified, make a copy of the sketch with the changes
+      // saved, so arduino-builder will see the modifications.
+      pathToSketch = saveSketchInTempFolder();
+      deleteTemp = true;
+    }
+
+    try {
+      return new Compiler(pathToSketch, sketch).build(progressListener, save);
+    } finally {
+      // Make sure we clean up any temporary sketch copy
+      if (deleteTemp)
+        FileUtils.recursiveDelete(pathToSketch.getParentFile());
+    }
+  }
+
+  private File saveSketchInTempFolder() throws IOException {
+    File tempFolder = FileUtils.createTempFolder("arduino_modified_sketch_");
+    FileUtils.copy(sketch.getFolder(), tempFolder);
+
+    for (SketchFile file : Stream.of(sketch.getFiles()).filter(SketchFile::isModified).collect(Collectors.toList())) {
+      Files.write(Paths.get(tempFolder.getAbsolutePath(), file.getFileName()), file.getProgram().getBytes());
+    }
+
+    return Paths.get(tempFolder.getAbsolutePath(), sketch.getPrimaryFile().getFileName()).toFile();
+  }
+
+  /**
+   * Handle export to applet.
+   */
+  protected boolean exportApplet(boolean usingProgrammer) throws Exception {
+    // build the sketch
+    editor.status.progressNotice(tr("Compiling sketch..."));
+    String foundName = build(false, false);
+    // (already reported) error during export, exit this function
+    if (foundName == null) return false;
+
+//    // If name != exportSketchName, then that's weirdness
+//    // BUG unfortunately, that can also be a bug in the preproc :(
+//    if (!name.equals(foundName)) {
+//      Base.showWarning("Error during export",
+//                       "Sketch name is " + name + " but the sketch\n" +
+//                       "name in the code was " + foundName, null);
+//      return false;
+//    }
+
+    editor.status.progressNotice(tr("Uploading..."));
+    boolean success = upload(foundName, usingProgrammer);
+    editor.status.progressUpdate(100);
+    return success;
+  }
+
+  private boolean upload(String suggestedClassName, boolean usingProgrammer) throws Exception {
+
+    Uploader uploader = new UploaderUtils().getUploaderByPreferences(false);
+
+    boolean success = false;
+    do {
+      if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) {
+        PasswordAuthorizationDialog dialog = new PasswordAuthorizationDialog(editor, tr("Type board password to upload a new sketch"));
+        dialog.setLocationRelativeTo(editor);
+        dialog.setVisible(true);
+
+        if (dialog.isCancelled()) {
+          editor.statusNotice(tr("Upload cancelled"));
+          return false;
+        }
+
+        PreferencesData.set(uploader.getAuthorizationKey(), dialog.getPassword());
+      }
+
+      List<String> warningsAccumulator = new LinkedList<>();
+      try {
+        success = new UploaderUtils().upload(sketch, uploader, suggestedClassName, usingProgrammer, false, warningsAccumulator);
+      } finally {
+        if (uploader.requiresAuthorization() && !success) {
+          PreferencesData.remove(uploader.getAuthorizationKey());
+        }
+      }
+
+      for (String warning : warningsAccumulator) {
+        System.out.print(tr("Warning"));
+        System.out.print(": ");
+        System.out.println(warning);
+      }
+
+    } while (uploader.requiresAuthorization() && !success);
+
+    return success;
+  }
+
+  /**
+   * Make sure the sketch hasn't been moved or deleted by some
+   * nefarious user. If they did, try to re-create it and save.
+   * Only checks to see if the main folder is still around,
+   * but not its contents.
+   */
+  private void ensureExistence() {
+    if (sketch.getFolder().exists()) return;
+
+    Base.showWarning(tr("Sketch Disappeared"),
+                     tr("The sketch folder has disappeared.\n " +
+                       "Will attempt to re-save in the same location,\n" +
+                       "but anything besides the code will be lost."), null);
+    try {
+      sketch.getFolder().mkdirs();
+
+      for (SketchFile file : sketch.getFiles()) {
+        file.save();  // this will force a save
+      }
+      calcModified();
+
+    } catch (Exception e) {
+      Base.showWarning(tr("Could not re-save sketch"),
+                       tr("Could not properly re-save the sketch. " +
+                         "You may be in trouble at this point,\n" +
+                         "and it might be time to copy and paste " +
+                         "your code to another text editor."), e);
+    }
+  }
+
+
+  /**
+   * Returns true if this is a read-only sketch. Used for the
+   * examples directory, or when sketches are loaded from read-only
+   * volumes or folders without appropriate permissions.
+   */
+  public boolean isReadOnly(LibraryList libraries, String examplesPath) {
+    String apath = sketch.getFolder().getAbsolutePath();
+
+    Optional<UserLibrary> libraryThatIncludesSketch = libraries.stream().filter(lib -> apath.startsWith(lib.getInstalledFolder().getAbsolutePath())).findFirst();
+    if (libraryThatIncludesSketch.isPresent() && !libraryThatIncludesSketch.get().onGoingDevelopment()) {
+      return true;
+    }
+
+    return sketchIsSystemExample(apath, examplesPath) || sketchFilesAreReadOnly();
+  }
+
+  private boolean sketchIsSystemExample(String apath, String examplesPath) {
+    return apath.startsWith(examplesPath);
+  }
+
+  private boolean sketchFilesAreReadOnly() {
+    for (SketchFile file : sketch.getFiles()) {
+      if (file.isModified() && file.fileReadOnly() && file.fileExists()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+
+
+
+  private void setUntitled(boolean u) {
+    editor.untitled = u;
+  }
+
+
+  public boolean isUntitled() {
+    return editor.untitled;
+  }
+
+  public Sketch getSketch() {
+    return sketch;
+  }
+
+  // .................................................................
+
+
+  /**
+   * Convert to sanitized name and alert the user
+   * if changes were made.
+   */
+  private static String checkName(String origName) {
+    String newName = BaseNoGui.sanitizeName(origName);
+
+    if (!newName.equals(origName)) {
+      String msg =
+        tr("The sketch name had to be modified. Sketch names can only consist\n" +
+          "of ASCII characters and numbers (but cannot start with a number).\n" +
+          "They should also be less than 64 characters long.");
+      System.out.println(msg);
+    }
+    return newName;
+  }
+
+
+}
diff --git a/app/src/processing/app/helpers/DocumentTextChangeListener.java b/app/src/processing/app/helpers/DocumentTextChangeListener.java
new file mode 100644
index 00000000000..290275e3434
--- /dev/null
+++ b/app/src/processing/app/helpers/DocumentTextChangeListener.java
@@ -0,0 +1,39 @@
+package processing.app.helpers;
+
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+/**
+ * Helper class that create a document listener that calls the given
+ * TextChangeListener on any change to the document text (but not changes to
+ * document attributes).
+ * 
+ * The TextChangeListener to be passed is intended to be a lambda function, for
+ * easy definition of a callback.
+ */
+public class DocumentTextChangeListener implements DocumentListener {
+  public interface TextChangeListener {
+    public void textChanged();
+  }
+
+  private TextChangeListener onChange;
+
+  public DocumentTextChangeListener(TextChangeListener onChange) {
+    this.onChange = onChange;
+  }
+
+  @Override
+  public void changedUpdate(DocumentEvent arg0) {
+    /* Attributes changed, do nothing */
+  }
+
+  @Override
+  public void insertUpdate(DocumentEvent arg0) {
+    onChange.textChanged();
+  }
+
+  @Override
+  public void removeUpdate(DocumentEvent arg0) {
+    onChange.textChanged();
+  }
+}
diff --git a/app/src/processing/app/macosx/ThinkDifferent.java b/app/src/processing/app/macosx/ThinkDifferent.java
index 7436591cb2c..e946bdc0fd7 100644
--- a/app/src/processing/app/macosx/ThinkDifferent.java
+++ b/app/src/processing/app/macosx/ThinkDifferent.java
@@ -75,7 +75,7 @@ public void openFiles(final AppEvent.OpenFilesEvent openFilesEvent) {
               try {
                 Base.INSTANCE.handleOpen(file);
                 List<Editor> editors = Base.INSTANCE.getEditors();
-                if (editors.size() == 2 && editors.get(0).getSketch().isUntitled()) {
+                if (editors.size() == 2 && editors.get(0).getSketchController().isUntitled()) {
                   Base.INSTANCE.handleClose(editors.get(0));
                 }
               } catch (Exception e) {
diff --git a/app/src/processing/app/syntax/SketchTextArea.java b/app/src/processing/app/syntax/SketchTextArea.java
index ac50a2dc2a6..bc71817fbaf 100644
--- a/app/src/processing/app/syntax/SketchTextArea.java
+++ b/app/src/processing/app/syntax/SketchTextArea.java
@@ -35,7 +35,6 @@
 import org.fife.ui.rsyntaxtextarea.Token;
 import org.fife.ui.rtextarea.RTextArea;
 import org.fife.ui.rtextarea.RTextAreaUI;
-import org.fife.ui.rtextarea.RUndoManager;
 import processing.app.Base;
 import processing.app.BaseNoGui;
 import processing.app.PreferencesData;
@@ -44,9 +43,7 @@
 import javax.swing.event.HyperlinkEvent;
 import javax.swing.event.HyperlinkListener;
 import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
 import javax.swing.text.Segment;
-import javax.swing.undo.UndoManager;
 import java.awt.*;
 import java.awt.event.MouseEvent;
 import java.io.File;
@@ -69,7 +66,8 @@ public class SketchTextArea extends RSyntaxTextArea {
 
   private PdeKeywords pdeKeywords;
 
-  public SketchTextArea(PdeKeywords pdeKeywords) throws IOException {
+  public SketchTextArea(RSyntaxDocument document, PdeKeywords pdeKeywords) throws IOException {
+    super(document);
     this.pdeKeywords = pdeKeywords;
     installFeatures();
   }
@@ -148,25 +146,6 @@ public boolean isSelectionActive() {
     return this.getSelectedText() != null;
   }
 
-  public void switchDocument(Document document, UndoManager newUndo) {
-
-    // HACK: Dont discard changes on curret UndoManager.
-    // BUG: https://github.com/bobbylight/RSyntaxTextArea/issues/84
-    setUndoManager(null); // bypass reset current undo manager...
-
-    super.setDocument(document);
-
-    setUndoManager((RUndoManager) newUndo);
-
-    // HACK: Complement previous hack (hide code folding on switch) | Drawback: Lose folding state
-//  if(sketch.getCodeCount() > 1 && textarea.isCodeFoldingEnabled()){
-//    textarea.setCodeFoldingEnabled(false);
-//    textarea.setCodeFoldingEnabled(true);
-//  }
-
-
-  }
-
   @Override
   protected RTAMouseListener createMouseListener() {
     return new SketchTextAreaMouseListener(this);
diff --git a/app/src/processing/app/tools/Archiver.java b/app/src/processing/app/tools/Archiver.java
index 34cf4efa89f..7308a4d6de7 100644
--- a/app/src/processing/app/tools/Archiver.java
+++ b/app/src/processing/app/tools/Archiver.java
@@ -26,7 +26,7 @@
 import org.apache.commons.compress.utils.IOUtils;
 import processing.app.Base;
 import processing.app.Editor;
-import processing.app.Sketch;
+import processing.app.SketchController;
 
 import java.awt.*;
 import java.io.File;
@@ -69,7 +69,7 @@ public void init(Editor editor) {
 
 
   public void run() {
-    Sketch sketch = editor.getSketch();
+    SketchController sketch = editor.getSketchController();
     
     // first save the sketch so that things don't archive strangely
     boolean success = false;
@@ -84,7 +84,7 @@ public void run() {
       return;
     }
 
-    File location = sketch.getFolder();
+    File location = sketch.getSketch().getFolder();
     String name = location.getName();
     File parent = new File(location.getParent());
 
diff --git a/app/src/processing/app/tools/DiscourseFormat.java b/app/src/processing/app/tools/DiscourseFormat.java
index c631df8bd2e..c79f7d11077 100644
--- a/app/src/processing/app/tools/DiscourseFormat.java
+++ b/app/src/processing/app/tools/DiscourseFormat.java
@@ -25,6 +25,7 @@
 
 import org.fife.ui.rsyntaxtextarea.Token;
 import processing.app.Editor;
+import processing.app.EditorTab;
 import processing.app.syntax.SketchTextArea;
 
 import javax.swing.text.BadLocationException;
@@ -65,9 +66,9 @@ public class DiscourseFormat {
    * from the actual Processing Tab ready to send to the processing discourse
    * web (copy & paste)
    */
-  public DiscourseFormat(Editor editor, boolean html) {
+  public DiscourseFormat(Editor editor, EditorTab tab, boolean html) {
     this.editor = editor;
-    this.textarea = editor.getTextArea();
+    this.textarea = tab.getTextArea();
     this.html = html;
   }
 
diff --git a/app/src/processing/app/tools/FixEncoding.java b/app/src/processing/app/tools/FixEncoding.java
index d76d9b1cb2a..fa91f11c294 100644
--- a/app/src/processing/app/tools/FixEncoding.java
+++ b/app/src/processing/app/tools/FixEncoding.java
@@ -66,13 +66,9 @@ public void run() {
     }
     try {
       for (int i = 0; i < sketch.getCodeCount(); i++) {
-        SketchCode code = sketch.getCode(i);
-        code.setProgram(loadWithLocalEncoding(code.getFile()));
-        code.setModified(true);  // yes, because we want them to save this
+        SketchFile file = sketch.getFile(i);
+        editor.findTab(file).setText(loadWithLocalEncoding(file.getFile()));
       }
-      // Update the currently visible program with its code
-      editor.setText(sketch.getCurrentCode().getProgram());
-      
     } catch (IOException e) {
       String msg = 
         tr("An error occurred while trying to fix the file encoding.\nDo not attempt to save this sketch as it may overwrite\nthe old version. Use Open to re-open the sketch and try again.\n") +
diff --git a/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java b/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java
index 411cb5de6ea..1a213eb1e57 100644
--- a/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java
+++ b/app/test/processing/app/BlockCommentGeneratesOneUndoActionTest.java
@@ -55,7 +55,7 @@ public void shouldUndoAndRedo() throws Exception {
     GuiActionRunner.execute(new GuiQuery<Frame>() {
 
       protected Frame executeInEDT() {
-        window.getEditor().handleCommentUncomment();
+        window.getEditor().getCurrentTab().handleCommentUncomment();
         return window.getEditor();
       }
 
diff --git a/arduino-core/src/cc/arduino/Compiler.java b/arduino-core/src/cc/arduino/Compiler.java
index bb815575cfc..e0788cf7b6a 100644
--- a/arduino-core/src/cc/arduino/Compiler.java
+++ b/arduino-core/src/cc/arduino/Compiler.java
@@ -108,24 +108,25 @@ enum BuilderAction {
 
   private static final Pattern ERROR_FORMAT = Pattern.compile("(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*error:\\s*(.*)\\s*", Pattern.MULTILINE | Pattern.DOTALL);
 
-  private final String pathToSketch;
-  private final SketchData sketch;
-  private final String buildPath;
+  private final File pathToSketch;
+  private final Sketch sketch;
+  private String buildPath;
   private final boolean verbose;
   private RunnerException exception;
 
-  public Compiler(SketchData data, String buildPath) {
-    this(data.getMainFilePath(), data, buildPath);
+  public Compiler(Sketch data) {
+    this(data.getPrimaryFile().getFile(), data);
   }
 
-  public Compiler(String pathToSketch, SketchData sketch, String buildPath) {
+  public Compiler(File pathToSketch, Sketch sketch) {
     this.pathToSketch = pathToSketch;
     this.sketch = sketch;
-    this.buildPath = buildPath;
     this.verbose = PreferencesData.getBoolean("build.verbose");
   }
 
   public String build(CompilerProgressListener progListener, boolean exportHex) throws RunnerException, PreferencesMapException, IOException {
+    this.buildPath = sketch.getBuildPath().getAbsolutePath();
+
     TargetBoard board = BaseNoGui.getTargetBoard();
     if (board == null) {
       throw new RunnerException("Board is not selected");
@@ -155,7 +156,7 @@ public String build(CompilerProgressListener progListener, boolean exportHex) th
 
     size(prefs);
 
-    return sketch.getPrimaryFile().getName();
+    return sketch.getPrimaryFile().getFileName();
   }
 
   private String VIDPID() {
@@ -241,7 +242,7 @@ private void callArduinoBuilder(TargetBoard board, TargetPlatform platform, Targ
       commandLine.addArgument("-verbose", false);
     }
 
-    commandLine.addArgument("\"" + pathToSketch + "\"", false);
+    commandLine.addArgument("\"" + pathToSketch.getAbsolutePath() + "\"", false);
 
     if (verbose) {
       System.out.println(commandLine);
@@ -565,8 +566,7 @@ public void message(String s) {
       RunnerException exception = placeException(error, pieces[1], PApplet.parseInt(pieces[2]) - 1);
 
       if (exception != null) {
-        SketchCode code = sketch.getCode(exception.getCodeIndex());
-        String fileName = (code.isExtension("ino") || code.isExtension("pde")) ? code.getPrettyName() : code.getFileName();
+        String fileName = exception.getCodeFile().getPrettyName();
         int lineNum = exception.getCodeLine() + 1;
         s = fileName + ":" + lineNum + ": error: " + error + msg;
       }
@@ -595,9 +595,9 @@ public void message(String s) {
   }
 
   private RunnerException placeException(String message, String fileName, int line) {
-    for (SketchCode code : sketch.getCodes()) {
-      if (new File(fileName).getName().equals(code.getFileName())) {
-        return new RunnerException(message, sketch.indexOfCode(code), line);
+    for (SketchFile file : sketch.getFiles()) {
+      if (new File(fileName).getName().equals(file.getFileName())) {
+        return new RunnerException(message, file, line);
       }
     }
     return null;
diff --git a/arduino-core/src/cc/arduino/UploaderUtils.java b/arduino-core/src/cc/arduino/UploaderUtils.java
index b243c30d216..a80eaf5065c 100644
--- a/arduino-core/src/cc/arduino/UploaderUtils.java
+++ b/arduino-core/src/cc/arduino/UploaderUtils.java
@@ -34,7 +34,7 @@
 import cc.arduino.packages.UploaderFactory;
 import processing.app.BaseNoGui;
 import processing.app.PreferencesData;
-import processing.app.SketchData;
+import processing.app.Sketch;
 import processing.app.debug.TargetPlatform;
 
 import java.util.LinkedList;
@@ -56,7 +56,7 @@ public Uploader getUploaderByPreferences(boolean noUploadPort) {
     return new UploaderFactory().newUploader(target.getBoards().get(board), boardPort, noUploadPort);
   }
 
-  public boolean upload(SketchData data, Uploader uploader, String buildPath, String suggestedClassName, boolean usingProgrammer, boolean noUploadPort, List<String> warningsAccumulator) throws Exception {
+  public boolean upload(Sketch data, Uploader uploader, String suggestedClassName, boolean usingProgrammer, boolean noUploadPort, List<String> warningsAccumulator) throws Exception {
 
     if (uploader == null)
       uploader = getUploaderByPreferences(noUploadPort);
@@ -75,7 +75,7 @@ public boolean upload(SketchData data, Uploader uploader, String buildPath, Stri
     }
 
     try {
-      success = uploader.uploadUsingPreferences(data.getFolder(), buildPath, suggestedClassName, usingProgrammer, warningsAccumulator);
+      success = uploader.uploadUsingPreferences(data.getFolder(), data.getBuildPath().getAbsolutePath(), suggestedClassName, usingProgrammer, warningsAccumulator);
     } finally {
       if (uploader.requiresAuthorization() && !success) {
         PreferencesData.remove(uploader.getAuthorizationKey());
diff --git a/arduino-core/src/processing/app/BaseNoGui.java b/arduino-core/src/processing/app/BaseNoGui.java
index 11ec4dd46a1..cb71cc6a36c 100644
--- a/arduino-core/src/processing/app/BaseNoGui.java
+++ b/arduino-core/src/processing/app/BaseNoGui.java
@@ -12,7 +12,6 @@
 import cc.arduino.packages.DiscoveryManager;
 import cc.arduino.packages.Uploader;
 import com.fasterxml.jackson.core.JsonProcessingException;
-import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.compress.utils.IOUtils;
 import org.apache.commons.logging.impl.LogFactoryImpl;
 import org.apache.commons.logging.impl.NoOpLog;
@@ -28,7 +27,6 @@
 import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.nio.file.Files;
 import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -121,18 +119,6 @@ static public String getAvrBasePath() {
     return path;
   }
 
-  static public File getBuildFolder(SketchData data) throws IOException {
-    File buildFolder;
-    if (PreferencesData.get("build.path") != null) {
-      buildFolder = absoluteFile(PreferencesData.get("build.path"));
-      Files.createDirectories(buildFolder.toPath());
-    } else {
-      buildFolder = FileUtils.createTempFolder("build", DigestUtils.md5Hex(data.getMainFilePath()) + ".tmp");
-      DeleteFilesOnShutdown.add(buildFolder);
-    }
-    return buildFolder;
-  }
-
   static public PreferencesMap getBoardPreferences() {
     TargetBoard board = getTargetBoard();
     if (board == null)
@@ -466,14 +452,12 @@ static public void init(String[] args) throws Exception {
         boolean success = false;
         try {
           // Editor constructor loads the sketch with handleOpenInternal() that
-          // creates a new Sketch that, in trun, calls load() inside its constructor
+          // creates a new Sketch that, in turn, builds a SketchData
+          // inside its constructor.
           // This translates here as:
           //   SketchData data = new SketchData(file);
           //   File tempBuildFolder = getBuildFolder();
-          //   data.load();
-          SketchData data = new SketchData(absoluteFile(parser.getFilenames().get(0)));
-          File tempBuildFolder = getBuildFolder(data);
-          data.load();
+          Sketch data = new Sketch(absoluteFile(parser.getFilenames().get(0)));
 
           // Sketch.exportApplet()
           //  - calls Sketch.prepare() that calls Sketch.ensureExistence()
@@ -482,7 +466,7 @@ static public void init(String[] args) throws Exception {
           if (!data.getFolder().exists()) {
             showError(tr("No sketch"), tr("Can't find the sketch in the specified path"), null);
           }
-          String suggestedClassName = new Compiler(data, tempBuildFolder.getAbsolutePath()).build(null, false);
+          String suggestedClassName = new Compiler(data).build(null, false);
           if (suggestedClassName == null) {
             showError(tr("Error while verifying"), tr("An error occurred while verifying the sketch"), null);
           }
@@ -491,7 +475,7 @@ static public void init(String[] args) throws Exception {
           Uploader uploader = new UploaderUtils().getUploaderByPreferences(parser.isNoUploadPort());
           if (uploader.requiresAuthorization() && !PreferencesData.has(uploader.getAuthorizationKey())) showError("...", "...", null);
           try {
-            success = new UploaderUtils().upload(data, uploader, tempBuildFolder.getAbsolutePath(), suggestedClassName, parser.isDoUseProgrammer(), parser.isNoUploadPort(), warningsAccumulator);
+            success = new UploaderUtils().upload(data, uploader, suggestedClassName, parser.isDoUseProgrammer(), parser.isNoUploadPort(), warningsAccumulator);
             showMessage(tr("Done uploading"), tr("Done uploading"));
           } finally {
             if (uploader.requiresAuthorization() && !success) {
@@ -518,9 +502,7 @@ static public void init(String[] args) throws Exception {
             //   SketchData data = new SketchData(file);
             //   File tempBuildFolder = getBuildFolder();
             //   data.load();
-            SketchData data = new SketchData(absoluteFile(path));
-            File tempBuildFolder = getBuildFolder(data);
-            data.load();
+            Sketch data = new Sketch(absoluteFile(path));
 
             // Sketch.prepare() calls Sketch.ensureExistence()
             // Sketch.build(verbose) calls Sketch.ensureExistence() and set progressListener and, finally, calls Compiler.build()
@@ -528,7 +510,7 @@ static public void init(String[] args) throws Exception {
             //    if (!data.getFolder().exists()) showError(...);
             //    String ... = Compiler.build(data, tempBuildFolder.getAbsolutePath(), tempBuildFolder, null, verbose);
             if (!data.getFolder().exists()) showError(tr("No sketch"), tr("Can't find the sketch in the specified path"), null);
-            String suggestedClassName = new Compiler(data, tempBuildFolder.getAbsolutePath()).build(null, false);
+            String suggestedClassName = new Compiler(data).build(null, false);
             if (suggestedClassName == null) showError(tr("Error while verifying"), tr("An error occurred while verifying the sketch"), null);
             showMessage(tr("Done compiling"), tr("Done compiling"));
           } catch (Exception e) {
@@ -993,49 +975,6 @@ static public void initParameters(String args[]) throws Exception {
     PreferencesData.init(absoluteFile(preferencesFile));
   }
 
-  /**
-   * Recursively remove all files within a directory,
-   * used with removeDir(), or when the contents of a dir
-   * should be removed, but not the directory itself.
-   * (i.e. when cleaning temp files from lib/build)
-   */
-  static public void removeDescendants(File dir) {
-    if (!dir.exists()) return;
-
-    String files[] = dir.list();
-    if (files == null) {
-      return;
-    }
-
-    for (String file : files) {
-      if (file.equals(".") || file.equals("..")) continue;
-      File dead = new File(dir, file);
-      if (!dead.isDirectory()) {
-        if (!PreferencesData.getBoolean("compiler.save_build_files")) {
-          if (!dead.delete()) {
-            // temporarily disabled
-            System.err.println(I18n.format(tr("Could not delete {0}"), dead));
-          }
-        }
-      } else {
-        removeDir(dead);
-        //dead.delete();
-      }
-    }
-  }
-
-  /**
-   * Remove all files in a directory and the directory itself.
-   */
-  static public void removeDir(File dir) {
-    if (dir.exists()) {
-      removeDescendants(dir);
-      if (!dir.delete()) {
-        System.err.println(I18n.format(tr("Could not delete {0}"), dir));
-      }
-    }
-  }
-
   /**
    * Produce a sanitized name that fits our standards for likely to work.
    * <p/>
diff --git a/arduino-core/src/processing/app/Sketch.java b/arduino-core/src/processing/app/Sketch.java
new file mode 100644
index 00000000000..41e6484b1b2
--- /dev/null
+++ b/arduino-core/src/processing/app/Sketch.java
@@ -0,0 +1,369 @@
+package processing.app;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import cc.arduino.files.DeleteFilesOnShutdown;
+import processing.app.helpers.FileUtils;
+
+import static processing.app.I18n.tr;
+
+/**
+ * This represents a single sketch, consisting of one or more files.
+ */
+public class Sketch {
+  public static final String DEFAULT_SKETCH_EXTENSION = "ino";
+  public static final List<String> OLD_SKETCH_EXTENSIONS = Arrays.asList("pde");
+  public static final List<String> SKETCH_EXTENSIONS = Stream.concat(Stream.of(DEFAULT_SKETCH_EXTENSION), OLD_SKETCH_EXTENSIONS.stream()).collect(Collectors.toList());
+  public static final List<String> OTHER_ALLOWED_EXTENSIONS = Arrays.asList("c", "cpp", "h", "hh", "hpp", "s");
+  public static final List<String> EXTENSIONS = Stream.concat(SKETCH_EXTENSIONS.stream(), OTHER_ALLOWED_EXTENSIONS.stream()).collect(Collectors.toList());
+
+  /**
+   * folder that contains this sketch
+   */
+  private File folder;
+
+  private List<SketchFile> files = new ArrayList<SketchFile>();
+
+  private File buildPath;
+
+  private static final Comparator<SketchFile> CODE_DOCS_COMPARATOR = new Comparator<SketchFile>() {
+    @Override
+    public int compare(SketchFile x, SketchFile y) {
+      if (x.isPrimary() && !y.isPrimary())
+        return -1;
+      if (y.isPrimary() && !x.isPrimary())
+        return 1;
+      return x.getFileName().compareTo(y.getFileName());
+    }
+  };
+
+  /**
+   * Create a new SketchData object, and looks at the sketch directory
+   * on disk to get populate the list of files in this sketch.
+   *
+   * @param file
+   *          Any file inside the sketch directory.
+   */
+  Sketch(File file) throws IOException {
+    folder = file.getParentFile();
+    files = listSketchFiles(true);
+  }
+
+  static public File checkSketchFile(File file) {
+    // check to make sure that this .pde file is
+    // in a folder of the same name
+    String fileName = file.getName();
+    File parent = file.getParentFile();
+    String parentName = parent.getName();
+    String pdeName = parentName + ".pde";
+    File altPdeFile = new File(parent, pdeName);
+    String inoName = parentName + ".ino";
+    File altInoFile = new File(parent, inoName);
+
+    if (pdeName.equals(fileName) || inoName.equals(fileName))
+      return file;
+
+    if (altPdeFile.exists())
+      return altPdeFile;
+
+    if (altInoFile.exists())
+      return altInoFile;
+
+    return null;
+  }
+
+  /**
+   * Reload the list of files. This checks the sketch directory on disk,
+   * to see if any files were added or removed. This does *not* check
+   * the contents of the files, just their presence.
+   *
+   * @return true when the list of files was changed, false when it was
+   *         not.
+   */
+  public boolean reload() throws IOException {
+    List<SketchFile> reloaded = listSketchFiles(false);
+    if (!reloaded.equals(files)) {
+      files = reloaded;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Scan this sketch's directory for files that should be loaded as
+   * part of this sketch. Doesn't modify this SketchData instance, just
+   * returns a filtered and sorted list of File objects ready to be
+   * passed to the SketchFile constructor.
+   *
+   * @param showWarnings
+   *          When true, any invalid filenames will show a warning.
+   */
+  private List<SketchFile> listSketchFiles(boolean showWarnings) throws IOException {
+    Set<SketchFile> result = new TreeSet<>(CODE_DOCS_COMPARATOR);
+    for (File file : FileUtils.listFiles(folder, false, EXTENSIONS)) {
+      if (BaseNoGui.isSanitaryName(file.getName())) {
+        result.add(new SketchFile(this, file));
+      } else if (showWarnings) {
+        System.err.println(I18n.format(tr("File name {0} is invalid: ignored"), file.getName()));
+      }
+    }
+
+    if (result.size() == 0)
+      throw new IOException(tr("No valid code files found"));
+
+    return new ArrayList<>(result);
+  }
+
+  /**
+   * Create the data folder if it does not exist already. As a
+   * convenience, it also returns the data folder, since it's likely
+   * about to be used.
+   */
+  public File prepareDataFolder() {
+    File dataFolder = getDataFolder();
+    if (!dataFolder.exists()) {
+      dataFolder.mkdirs();
+    }
+    return dataFolder;
+  }
+
+  public void save() throws IOException {
+    for (SketchFile file : getFiles()) {
+      if (file.isModified())
+        file.save();
+    }
+  }
+
+  public int getCodeCount() {
+    return files.size();
+  }
+
+  public SketchFile[] getFiles() {
+    return files.toArray(new SketchFile[0]);
+  }
+
+  /**
+   * Returns a file object for the primary .pde of this sketch.
+   */
+  public SketchFile getPrimaryFile() {
+    return files.get(0);
+  }
+
+  /**
+   * Returns path to the main .pde file for this sketch.
+   */
+  public String getMainFilePath() {
+    return getPrimaryFile().getFile().getAbsolutePath();
+  }
+
+  public SketchFile getFile(int i) {
+    return files.get(i);
+  }
+
+  /**
+   * Gets the build path for this sketch. The first time this is called,
+   * a build path is generated and created and the same path is returned
+   * on all subsequent calls.
+   *
+   * This takes into account the build.path preference. If it is set,
+   * that path is always returned, and the directory is *not* deleted on
+   * shutdown. If the preference is not set, a random pathname in a
+   * temporary directory is generated, which is automatically deleted on
+   * shutdown.
+   */
+  public File getBuildPath() throws IOException {
+    if (buildPath == null) {
+      if (PreferencesData.get("build.path") != null) {
+        buildPath = BaseNoGui.absoluteFile(PreferencesData.get("build.path"));
+        Files.createDirectories(buildPath.toPath());
+      } else {
+        buildPath = FileUtils.createTempFolder("arduino_build_");
+        DeleteFilesOnShutdown.add(buildPath);
+      }
+    }
+
+    return buildPath;
+  }
+
+  protected void removeFile(SketchFile which) {
+    if (!files.remove(which))
+      System.err.println("removeCode: internal error.. could not find code");
+  }
+
+  public String getName() {
+    return folder.getName();
+  }
+
+  public File getFolder() {
+    return folder;
+  }
+
+  public File getDataFolder() {
+    return new File(folder, "data");
+  }
+
+  /**
+   * Is any of the files in this sketch modified?
+   */
+  public boolean isModified() {
+    for (SketchFile file : files) {
+      if (file.isModified())
+        return true;
+    }
+    return false;
+  }
+
+  /**
+   * Finds the file with the given filename and returns its index.
+   * Returns -1 when the file was not found.
+   */
+  public int findFileIndex(File filename) {
+    int i = 0;
+    for (SketchFile file : files) {
+      if (file.getFile().equals(filename))
+        return i;
+      i++;
+    }
+    return -1;
+  }
+
+  /**
+   * Check if renaming/saving this sketch to the given folder would
+   * cause a problem because: 1. The new folder already exists 2.
+   * Renaming the primary file would cause a conflict with an existing
+   * file. If so, an IOEXception is thrown. If not, the name of the new
+   * primary file is returned.
+   */
+  protected File checkNewFoldername(File newFolder) throws IOException {
+    String newPrimary = FileUtils.addExtension(newFolder.getName(), DEFAULT_SKETCH_EXTENSION);
+    // Verify the new folder does not exist yet
+    if (newFolder.exists()) {
+      String msg = I18n.format(tr("Sorry, the folder \"{0}\" already exists."), newFolder.getAbsoluteFile());
+      throw new IOException(msg);
+    }
+
+    // If the folder is actually renamed (as opposed to moved somewhere
+    // else), check for conflicts using the new filename, but the
+    // existing folder name.
+    if(newFolder.getName() != folder.getName())
+      checkNewFilename(new File(folder, newPrimary));
+
+    return new File(newFolder, newPrimary);
+  }
+
+  /**
+   * Check if renaming or adding a file would cause a problem because
+   * the file already exists in this sketch. If so, an IOEXception is
+   * thrown.
+   *
+   * @param newFile
+   *          The filename of the new file, or the new name for an
+   *          existing file.
+   */
+  protected void checkNewFilename(File newFile) throws IOException {
+    // Verify that the sketch doesn't have a filem with the new name
+    // already, other than the current primary (index 0)
+    if (findFileIndex(newFile) >= 0) {
+      String msg = I18n.format(tr("The sketch already contains a file named \"{0}\""), newFile.getName());
+      throw new IOException(msg);
+    }
+
+  }
+
+  /**
+   * Rename this sketch' folder to the given name. Unlike saveAs(), this
+   * moves the sketch directory, not leaving anything in the old place.
+   * This operation does not *save* the sketch, so the files on disk are
+   * moved, but not modified.
+   *
+   * @param newFolder
+   *          The new folder name for this sketch. The new primary
+   *          file's name will be derived from this.
+   *
+   * @throws IOException
+   *           When a problem occurs. The error message should be
+   *           already translated.
+   */
+  public void renameTo(File newFolder) throws IOException {
+    // Check intended rename (throws if there is a problem)
+    File newPrimary = checkNewFoldername(newFolder);
+
+    // Rename the sketch folder
+    if (!getFolder().renameTo(newFolder))
+      throw new IOException(tr("Failed to rename sketch folder"));
+
+    folder = newFolder;
+
+    // Tell each file about its new name
+    for (SketchFile file : files)
+      file.renamedTo(new File(newFolder, file.getFileName()));
+
+    // And finally, rename the primary file
+    getPrimaryFile().renameTo(newPrimary.getName());
+  }
+
+
+  public SketchFile addFile(String newName) throws IOException {
+    // Check the name will not cause any conflicts
+    File newFile = new File(folder, newName);
+    checkNewFilename(newFile);
+
+    // Add a new sketchFile
+    SketchFile sketchFile = new SketchFile(this, newFile);
+    files.add(sketchFile);
+    Collections.sort(files, CODE_DOCS_COMPARATOR);
+
+    return sketchFile;
+  }
+
+  /**
+   * Save this sketch under the new name given. Unlike renameTo(), this
+   * leaves the existing sketch in place.
+   *
+   * @param newFolder
+   *          The new folder name for this sketch. The new primary
+   *          file's name will be derived from this.
+   *
+   * @throws IOException
+   *           When a problem occurs. The error message should be
+   *           already translated.
+   */
+  public void saveAs(File newFolder) throws IOException {
+    // Check intented rename (throws if there is a problem)
+    File newPrimary = checkNewFoldername(newFolder);
+
+    // Create the folder
+    if (!newFolder.mkdirs()) {
+      String msg = I18n.format(tr("Could not create directory \"{0}\""), newFolder.getAbsolutePath());
+      throw new IOException(msg);
+    }
+
+    // Save the files to their new location
+    for (SketchFile file : files) {
+      if (file.isPrimary())
+        file.saveAs(newPrimary);
+      else
+        file.saveAs(new File(newFolder, file.getFileName()));
+    }
+
+    folder = newFolder;
+
+    // Copy the data folder (this may take a while.. add progress bar?)
+    if (getDataFolder().exists()) {
+      File newDataFolder = new File(newFolder, "data");
+      FileUtils.copy(getDataFolder(), newDataFolder);
+    }
+  }
+
+  /**
+   * Deletes this entire sketch from disk.
+   */
+  void delete() {
+    FileUtils.recursiveDelete(folder);
+  }
+}
diff --git a/arduino-core/src/processing/app/SketchCode.java b/arduino-core/src/processing/app/SketchCode.java
deleted file mode 100644
index 3e185867641..00000000000
--- a/arduino-core/src/processing/app/SketchCode.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
-  SketchCode - data class for a single file inside a sketch
-  Part of the Processing project - http://processing.org
-
-  Copyright (c) 2004-08 Ben Fry and Casey Reas
-  Copyright (c) 2001-04 Massachusetts Institute of Technology
-
-  This program is free software; you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation; either version 2 of the License, or
-  (at your option) any later version.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software Foundation,
-  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-*/
-
-package processing.app;
-
-import processing.app.helpers.FileUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static processing.app.I18n.tr;
-
-/**
- * Represents a single tab of a sketch.
- */
-public class SketchCode {
-
-  /**
-   * File object for where this code is located
-   */
-  private File file;
-
-  /**
-   * Text of the program text for this tab
-   */
-  private String program;
-
-  private boolean modified;
-
-  private Object metadata;
-
-  public SketchCode(File file) {
-    init(file, null);
-  }
-
-  public SketchCode(File file, Object metadata) {
-    init(file, metadata);
-  }
-
-  private void init(File file, Object metadata) {
-    this.file = file;
-    this.metadata = metadata;
-
-    try {
-      load();
-    } catch (IOException e) {
-      System.err.println(
-        I18n.format(tr("Error while loading code {0}"), file.getName()));
-    }
-  }
-
-
-  public File getFile() {
-    return file;
-  }
-
-
-  protected boolean fileExists() {
-    return file.exists();
-  }
-
-
-  protected boolean fileReadOnly() {
-    return !file.canWrite();
-  }
-
-
-  protected boolean deleteFile(Path tempBuildFolder) throws IOException {
-    if (!file.delete()) {
-      return false;
-    }
-
-    List<Path> tempBuildFolders = Stream.of(tempBuildFolder, tempBuildFolder.resolve("sketch"))
-        .filter(path -> Files.exists(path)).collect(Collectors.toList());
-
-    for (Path folder : tempBuildFolders) {
-      if (!deleteCompiledFilesFrom(folder)) {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-  private boolean deleteCompiledFilesFrom(Path tempBuildFolder) throws IOException {
-    List<Path> compiledFiles = Files.list(tempBuildFolder)
-      .filter(pathname -> pathname.getFileName().toString().startsWith(getFileName()))
-      .collect(Collectors.toList());
-
-    for (Path compiledFile : compiledFiles) {
-      try {
-        Files.delete(compiledFile);
-      } catch (IOException e) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  protected boolean renameTo(File what) {
-    boolean success = file.renameTo(what);
-    if (success) {
-      file = what;
-    }
-    return success;
-  }
-
-
-  public String getFileName() {
-    return file.getName();
-  }
-
-
-  public String getPrettyName() {
-    String prettyName = getFileName();
-    int dot = prettyName.lastIndexOf('.');
-    return prettyName.substring(0, dot);
-  }
-
-  public String getFileNameWithExtensionIfNotIno() {
-    if (getFileName().endsWith(".ino")) {
-      return getPrettyName();
-    }
-    return getFileName();
-  }
-
-  public boolean isExtension(String... extensions) {
-    return isExtension(Arrays.asList(extensions));
-  }
-
-  public boolean isExtension(List<String> extensions) {
-    return FileUtils.hasExtension(file, extensions);
-  }
-
-
-  public String getProgram() {
-    return program;
-  }
-
-
-  public void setProgram(String replacement) {
-    program = replacement;
-  }
-
-
-  public int getLineCount() {
-    return BaseNoGui.countLines(program);
-  }
-
-
-  public void setModified(boolean modified) {
-    this.modified = modified;
-  }
-
-
-  public boolean isModified() {
-    return modified;
-  }
-
-
-  /**
-   * Load this piece of code from a file.
-   */
-  private void load() throws IOException {
-    program = BaseNoGui.loadFile(file);
-
-    if (program == null) {
-      throw new IOException();
-    }
-
-    if (program.indexOf('\uFFFD') != -1) {
-      System.err.println(
-        I18n.format(
-          tr("\"{0}\" contains unrecognized characters. " +
-            "If this code was created with an older version of Arduino, " +
-            "you may need to use Tools -> Fix Encoding & Reload to update " +
-            "the sketch to use UTF-8 encoding. If not, you may need to " +
-            "delete the bad characters to get rid of this warning."),
-          file.getName()
-        )
-      );
-      System.err.println();
-    }
-
-    setModified(false);
-  }
-
-
-  /**
-   * Save this piece of code, regardless of whether the modified
-   * flag is set or not.
-   */
-  public void save() throws IOException {
-    // TODO re-enable history
-    //history.record(s, SketchHistory.SAVE);
-
-    BaseNoGui.saveFile(program, file);
-    setModified(false);
-  }
-
-
-  /**
-   * Save this file to another location, used by Sketch.saveAs()
-   */
-  public void saveAs(File newFile) throws IOException {
-    BaseNoGui.saveFile(program, newFile);
-  }
-
-
-  public Object getMetadata() {
-    return metadata;
-  }
-
-
-  public void setMetadata(Object metadata) {
-    this.metadata = metadata;
-  }
-}
diff --git a/arduino-core/src/processing/app/SketchData.java b/arduino-core/src/processing/app/SketchData.java
deleted file mode 100644
index 21aeecb4822..00000000000
--- a/arduino-core/src/processing/app/SketchData.java
+++ /dev/null
@@ -1,270 +0,0 @@
-package processing.app;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.*;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static processing.app.I18n.tr;
-
-public class SketchData {
-
-  public static final List<String> SKETCH_EXTENSIONS = Arrays.asList("ino", "pde");
-  public static final List<String> OTHER_ALLOWED_EXTENSIONS = Arrays.asList("c", "cpp", "h", "hh", "hpp", "s");
-  public static final List<String> EXTENSIONS = Stream.concat(SKETCH_EXTENSIONS.stream(), OTHER_ALLOWED_EXTENSIONS.stream()).collect(Collectors.toList());
-
-  /**
-   * main pde file for this sketch.
-   */
-  private File primaryFile;
-
-  /**
-   * folder that contains this sketch
-   */
-  private File folder;
-
-  /**
-   * data folder location for this sketch (may not exist yet)
-   */
-  private File dataFolder;
-
-  /**
-   * code folder location for this sketch (may not exist yet)
-   */
-  private File codeFolder;
-
-  /**
-   * Name of sketch, which is the name of main file (without .pde or .java
-   * extension)
-   */
-  private String name;
-
-  private List<SketchCode> codes = new ArrayList<SketchCode>();
-
-  private static final Comparator<SketchCode> CODE_DOCS_COMPARATOR = new Comparator<SketchCode>() {
-    @Override
-    public int compare(SketchCode x, SketchCode y) {
-      return x.getFileName().compareTo(y.getFileName());
-    }
-  };
-
-  SketchData(File file) {
-    primaryFile = file;
-
-    // get the name of the sketch by chopping .pde or .java
-    // off of the main file name
-    String mainFilename = primaryFile.getName();
-    int suffixLength = getDefaultExtension().length() + 1;
-    name = mainFilename.substring(0, mainFilename.length() - suffixLength);
-
-    folder = new File(file.getParent());
-    //System.out.println("sketch dir is " + folder);
-  }
-
-  static public File checkSketchFile(File file) {
-    // check to make sure that this .pde file is
-    // in a folder of the same name
-    String fileName = file.getName();
-    File parent = file.getParentFile();
-    String parentName = parent.getName();
-    String pdeName = parentName + ".pde";
-    File altPdeFile = new File(parent, pdeName);
-    String inoName = parentName + ".ino";
-    File altInoFile = new File(parent, inoName);
-
-    if (pdeName.equals(fileName) || inoName.equals(fileName))
-      return file;
-
-    if (altPdeFile.exists())
-      return altPdeFile;
-
-    if (altInoFile.exists())
-      return altInoFile;
-
-    return null;
-  }
-
-  /**
-   * Build the list of files.
-   * <p>
-   * Generally this is only done once, rather than
-   * each time a change is made, because otherwise it gets to be
-   * a nightmare to keep track of what files went where, because
-   * not all the data will be saved to disk.
-   * <p>
-   * This also gets called when the main sketch file is renamed,
-   * because the sketch has to be reloaded from a different folder.
-   * <p>
-   * Another exception is when an external editor is in use,
-   * in which case the load happens each time "run" is hit.
-   */
-  protected void load() throws IOException {
-    codeFolder = new File(folder, "code");
-    dataFolder = new File(folder, "data");
-
-    // get list of files in the sketch folder
-    String list[] = folder.list();
-    if (list == null) {
-      throw new IOException("Unable to list files from " + folder);
-    }
-
-    // reset these because load() may be called after an
-    // external editor event. (fix for 0099)
-//    codeDocs = new SketchCodeDoc[list.length];
-    clearCodeDocs();
-//    data.setCodeDocs(codeDocs);
-
-    for (String filename : list) {
-      // Ignoring the dot prefix files is especially important to avoid files
-      // with the ._ prefix on Mac OS X. (You'll see this with Mac files on
-      // non-HFS drives, i.e. a thumb drive formatted FAT32.)
-      if (filename.startsWith(".")) continue;
-
-      // Don't let some wacko name a directory blah.pde or bling.java.
-      if (new File(folder, filename).isDirectory()) continue;
-
-      // figure out the name without any extension
-      String base = filename;
-      // now strip off the .pde and .java extensions
-      for (String extension : EXTENSIONS) {
-        if (base.toLowerCase().endsWith("." + extension)) {
-          base = base.substring(0, base.length() - (extension.length() + 1));
-
-          // Don't allow people to use files with invalid names, since on load,
-          // it would be otherwise possible to sneak in nasty filenames. [0116]
-          if (BaseNoGui.isSanitaryName(base)) {
-            addCode(new SketchCode(new File(folder, filename)));
-          } else {
-            System.err.println(I18n.format(tr("File name {0} is invalid: ignored"), filename));
-          }
-        }
-      }
-    }
-
-    if (getCodeCount() == 0)
-      throw new IOException(tr("No valid code files found"));
-
-    // move the main class to the first tab
-    // start at 1, if it's at zero, don't bother
-    for (SketchCode code : getCodes()) {
-      //if (code[i].file.getName().equals(mainFilename)) {
-      if (code.getFile().equals(primaryFile)) {
-        moveCodeToFront(code);
-        break;
-      }
-    }
-
-    // sort the entries at the top
-    sortCode();
-  }
-
-  public void save() throws IOException {
-    for (SketchCode code : getCodes()) {
-      if (code.isModified())
-        code.save();
-    }
-  }
-
-  public int getCodeCount() {
-    return codes.size();
-  }
-
-  public SketchCode[] getCodes() {
-    return codes.toArray(new SketchCode[0]);
-  }
-
-  /**
-   * Returns the default extension for this editor setup.
-   */
-  public String getDefaultExtension() {
-    return "ino";
-  }
-
-  /**
-   * Returns a file object for the primary .pde of this sketch.
-   */
-  public File getPrimaryFile() {
-    return primaryFile;
-  }
-
-  /**
-   * Returns path to the main .pde file for this sketch.
-   */
-  public String getMainFilePath() {
-    return primaryFile.getAbsolutePath();
-    //return code[0].file.getAbsolutePath();
-  }
-
-  public void addCode(SketchCode sketchCode) {
-    codes.add(sketchCode);
-  }
-
-  public void moveCodeToFront(SketchCode codeDoc) {
-    codes.remove(codeDoc);
-    codes.add(0, codeDoc);
-  }
-
-  protected void replaceCode(SketchCode newCode) {
-    for (SketchCode code : codes) {
-      if (code.getFileName().equals(newCode.getFileName())) {
-        codes.set(codes.indexOf(code), newCode);
-        return;
-      }
-    }
-  }
-
-  protected void sortCode() {
-    if (codes.size() < 2)
-      return;
-    SketchCode first = codes.remove(0);
-    Collections.sort(codes, CODE_DOCS_COMPARATOR);
-    codes.add(0, first);
-  }
-
-  public SketchCode getCode(int i) {
-    return codes.get(i);
-  }
-
-  protected void removeCode(SketchCode which) {
-    for (SketchCode code : codes) {
-      if (code == which) {
-        codes.remove(code);
-        return;
-      }
-    }
-    System.err.println("removeCode: internal error.. could not find code");
-  }
-
-  public int indexOfCode(SketchCode who) {
-    for (SketchCode code : codes) {
-      if (code == who)
-        return codes.indexOf(code);
-    }
-    return -1;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(String name) {
-    this.name = name;
-  }
-
-  public void clearCodeDocs() {
-    codes.clear();
-  }
-
-  public File getFolder() {
-    return folder;
-  }
-
-  public File getDataFolder() {
-    return dataFolder;
-  }
-
-  public File getCodeFolder() {
-    return codeFolder;
-  }
-}
diff --git a/arduino-core/src/processing/app/SketchFile.java b/arduino-core/src/processing/app/SketchFile.java
new file mode 100644
index 00000000000..19d30006533
--- /dev/null
+++ b/arduino-core/src/processing/app/SketchFile.java
@@ -0,0 +1,301 @@
+/*
+  SketchFile - data class for a single file inside a sketch
+  Part of the Processing project - http://processing.org
+
+  Copyright (c) 2004-08 Ben Fry and Casey Reas
+  Copyright (c) 2001-04 Massachusetts Institute of Technology
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software Foundation,
+  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+
+package processing.app;
+
+import processing.app.helpers.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static processing.app.I18n.tr;
+
+/**
+ * Represents a file within a sketch.
+ */
+public class SketchFile {
+
+  /**
+   * File object for where this code is located
+   */
+  private File file;
+
+  /**
+   * The sketch this file belongs to.
+   */
+  private Sketch sketch;
+
+  /**
+   * Is this the primary file in the sketch?
+   */
+  private boolean primary;
+
+  /**
+   * Interface for an in-memory storage of text file contents. This is
+   * intended to allow a GUI to keep modified text in memory, and allow
+   * SketchFile to check for changes when needed.
+   */
+  public static interface TextStorage {
+    /** Get the current text */
+    public String getText();
+
+    /**
+     * Is the text modified externally, after the last call to
+     * clearModified() or setText()?
+     */
+    public boolean isModified();
+
+    /** Clear the isModified() result value */
+    public void clearModified();
+  };
+
+  /**
+   * A storage for this file's text. This can be set by a GUI, so we can
+   * have access to any modified version of the file. This can be null,
+   * in which case the file is never modified, and saving is a no-op.
+   */
+  private TextStorage storage;
+
+  /**
+   * Create a new SketchFile
+   *
+   * @param sketch
+   *          The sketch this file belongs to
+   * @param file
+   *          The file this SketchFile represents
+   * @param primary
+   *          Whether this file is the primary file of the sketch
+   */
+  public SketchFile(Sketch sketch, File file) {
+    this.sketch = sketch;
+    this.file = file;
+    FileUtils.SplitFile split = FileUtils.splitFilename(file);
+    this.primary = split.basename.equals(sketch.getFolder().getName())
+        && Sketch.SKETCH_EXTENSIONS.contains(split.extension);
+  }
+
+  /**
+   * Set an in-memory storage for this file's text, that will be queried
+   * on compile, save, and whenever the text is needed. null can be
+   * passed to detach any attached storage.
+   */
+  public void setStorage(TextStorage text) {
+    this.storage = text;
+  }
+
+
+  public File getFile() {
+    return file;
+  }
+
+  /**
+   * Is this the primary file in the sketch?
+   */
+  public boolean isPrimary() {
+    return primary;
+  }
+
+  protected boolean fileExists() {
+    return file.exists();
+  }
+
+
+  protected boolean fileReadOnly() {
+    return !file.canWrite();
+  }
+
+
+  protected boolean delete(Path tempBuildFolder) throws IOException {
+    if (!file.delete()) {
+      return false;
+    }
+
+    List<Path> tempBuildFolders = Stream.of(tempBuildFolder, tempBuildFolder.resolve("sketch"))
+        .filter(path -> Files.exists(path)).collect(Collectors.toList());
+
+    for (Path folder : tempBuildFolders) {
+      if (!deleteCompiledFilesFrom(folder)) {
+        return false;
+      }
+    }
+
+    sketch.removeFile(this);
+
+    return true;
+  }
+
+  private boolean deleteCompiledFilesFrom(Path tempBuildFolder) throws IOException {
+    List<Path> compiledFiles = Files.list(tempBuildFolder)
+      .filter(pathname -> pathname.getFileName().toString().startsWith(getFileName()))
+      .collect(Collectors.toList());
+
+    for (Path compiledFile : compiledFiles) {
+      try {
+        Files.delete(compiledFile);
+      } catch (IOException e) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Rename the given file to get the given name.
+   *
+   * @param newName
+   *          The new name, including extension, excluding directory
+   *          name.
+   * @throws IOException
+   *           When a problem occurs, or is expected to occur. The error
+   *           message should be already translated.
+   */
+  public void renameTo(String newName) throws IOException {
+    File newFile = new File(file.getParentFile(), newName);
+    sketch.checkNewFilename(newFile);
+    if (file.renameTo(newFile)) {
+      renamedTo(newFile);
+    } else {
+      String msg = I18n.format(tr("Failed to rename \"{0}\" to \"{1}\""), file.getName(), newName);
+      throw new IOException(msg);
+    }
+  }
+
+  /**
+   * Should be called when this file was renamed and renameTo could not
+   * be used (e.g. when renaming the entire sketch directory).
+   */
+  protected void renamedTo(File what) {
+    file = what;
+  }
+
+  /*
+   * Returns the filename include extension.
+   */
+  public String getFileName() {
+    return file.getName();
+  }
+
+  /**
+   * Returns the filename without extension for normal sketch files
+   * (Sketch.SKETCH_EXTENSIONS) and the filename with extension for all
+   * others.
+   */
+  public String getPrettyName() {
+    if (isExtension(Sketch.SKETCH_EXTENSIONS))
+      return getBaseName();
+    else
+      return getFileName();
+  }
+
+  /**
+   * Returns the filename without extension
+   */
+  public String getBaseName() {
+    return FileUtils.splitFilename(file).basename;
+  }
+
+  public boolean isExtension(String... extensions) {
+    return isExtension(Arrays.asList(extensions));
+  }
+
+  public boolean isExtension(List<String> extensions) {
+    return FileUtils.hasExtension(file, extensions);
+  }
+
+
+  public String getProgram() {
+    if (storage != null)
+      return storage.getText();
+
+    return null;
+  }
+
+
+  public boolean isModified() {
+    if (storage != null)
+      return storage.isModified();
+    return false;
+  }
+
+  public boolean equals(Object o) {
+    return (o instanceof SketchFile) && file.equals(((SketchFile) o).file);
+  }
+
+  /**
+   * Load this piece of code from a file and return the contents. This
+   * completely ignores any changes in the linked storage, if any, and
+   * just directly reads the file.
+   */
+  public String load() throws IOException {
+    String text = BaseNoGui.loadFile(file);
+
+    if (text == null) {
+      throw new IOException();
+    }
+
+    if (text.indexOf('\uFFFD') != -1) {
+      System.err.println(
+        I18n.format(
+          tr("\"{0}\" contains unrecognized characters. " +
+            "If this code was created with an older version of Arduino, " +
+            "you may need to use Tools -> Fix Encoding & Reload to update " +
+            "the sketch to use UTF-8 encoding. If not, you may need to " +
+            "delete the bad characters to get rid of this warning."),
+          file.getName()
+        )
+      );
+      System.err.println();
+    }
+    return text;
+  }
+
+
+  /**
+   * Save this piece of code, regardless of whether the modified
+   * flag is set or not.
+   */
+  public void save() throws IOException {
+    if (storage == null)
+      return; /* Nothing to do */
+
+    BaseNoGui.saveFile(storage.getText(), file);
+    storage.clearModified();
+  }
+
+
+  /**
+   * Save this file to another location, used by Sketch.saveAs()
+   */
+  public void saveAs(File newFile) throws IOException {
+    if (storage == null)
+      return; /* Nothing to do */
+
+    BaseNoGui.saveFile(storage.getText(), newFile);
+    renamedTo(newFile);
+  }
+}
diff --git a/arduino-core/src/processing/app/debug/RunnerException.java b/arduino-core/src/processing/app/debug/RunnerException.java
index 0a67d1e80ef..5a60ac5a48b 100644
--- a/arduino-core/src/processing/app/debug/RunnerException.java
+++ b/arduino-core/src/processing/app/debug/RunnerException.java
@@ -23,6 +23,7 @@
 
 package processing.app.debug;
 
+import processing.app.SketchFile;
 
 /**
  * An exception with a line number attached that occurs
@@ -31,7 +32,7 @@
 @SuppressWarnings("serial")
 public class RunnerException extends Exception {
   protected String message;
-  protected int codeIndex;
+  protected SketchFile codeFile;
   protected int codeLine;
   protected int codeColumn;
   protected boolean showStackTrace;
@@ -42,23 +43,23 @@ public RunnerException(String message) {
   }
 
   public RunnerException(String message, boolean showStackTrace) {
-    this(message, -1, -1, -1, showStackTrace);
+    this(message, null, -1, -1, showStackTrace);
   }
 
-  public RunnerException(String message, int file, int line) {
+  public RunnerException(String message, SketchFile file, int line) {
     this(message, file, line, -1, true);
   }
 
   
-  public RunnerException(String message, int file, int line, int column) {
+  public RunnerException(String message, SketchFile file, int line, int column) {
     this(message, file, line, column, true);
   }
   
   
-  public RunnerException(String message, int file, int line, int column, 
+  public RunnerException(String message, SketchFile file, int line, int column, 
                          boolean showStackTrace) {
     this.message = message;
-    this.codeIndex = file;
+    this.codeFile = file;
     this.codeLine = line;
     this.codeColumn = column;
     this.showStackTrace = showStackTrace;
@@ -84,18 +85,17 @@ public void setMessage(String message) {
   }
   
   
-  public int getCodeIndex() {
-    return codeIndex;
+  public SketchFile getCodeFile() {
+    return codeFile;
   }
   
   
-  public void setCodeIndex(int index) {
-    codeIndex = index;
+  public void setCodeFile(SketchFile file) {
+    codeFile = file;
   }
-  
-  
-  public boolean hasCodeIndex() {
-    return codeIndex != -1;
+
+  public boolean hasCodeFile() {
+    return codeFile != null;
   }
   
   
@@ -107,8 +107,7 @@ public int getCodeLine() {
   public void setCodeLine(int line) {
     this.codeLine = line;
   }
-  
-  
+
   public boolean hasCodeLine() {
     return codeLine != -1;
   }
@@ -117,8 +116,7 @@ public boolean hasCodeLine() {
   public void setCodeColumn(int column) {
     this.codeColumn = column;
   }
-  
-  
+
   public int getCodeColumn() {
     return codeColumn;
   }
diff --git a/arduino-core/src/processing/app/helpers/FileUtils.java b/arduino-core/src/processing/app/helpers/FileUtils.java
index 4083a0a69de..fdfdf313428 100644
--- a/arduino-core/src/processing/app/helpers/FileUtils.java
+++ b/arduino-core/src/processing/app/helpers/FileUtils.java
@@ -245,6 +245,44 @@ public static boolean hasExtension(File file, List<String> extensions) {
     return extensions.contains(extension.toLowerCase());
   }
 
+  /**
+   * Returns the given filename with the extension replaced by the one
+   * given. If the filename does not have an extension yet, one is
+   * added.
+   */
+  public static String replaceExtension(String filename, String extension) {
+    SplitFile split = splitFilename(filename);
+    split.extension = extension;
+    return split.join();
+  }
+
+  /**
+   * Returns the given filename with the extension replaced by the one
+   * given. If the filename does not have an extension yet, one is
+   * added.
+   */
+  public static File replaceExtension(File file, String extension) {
+    return new File(file.getParentFile(), replaceExtension(file.getName(), extension));
+  }
+
+  /**
+   * Adds an extension to the given filename. If it already contains
+   * one, an additional extension is added. If the extension is the
+   * empty string, the file is returned unmodified.
+   */
+  public static String addExtension(String filename, String extension) {
+    return extension.equals("") ? filename : (filename + "." + extension);
+  }
+
+  /**
+   * Adds an extension to the given filename. If it already contains
+   * one, an additional extension is added. If the extension is the
+   * empty string, the file is returned unmodified.
+   */
+  public static File addExtension(File file, String extension) {
+    return new File(file.getParentFile(), addExtension(file.getName(), extension));
+  }
+
   /**
    * The result of a splitFilename call.
    */
@@ -256,6 +294,10 @@ public SplitFile(String basename, String extension) {
 
     public String basename;
     public String extension;
+
+    public String join() {
+      return addExtension(basename, extension);
+    }
   }
 
   /**