diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java
index e68ca47783b..60ab994e62e 100644
--- a/app/src/processing/app/Editor.java
+++ b/app/src/processing/app/Editor.java
@@ -44,6 +44,7 @@
 import processing.app.syntax.SketchTextArea;
 import processing.app.tools.MenuScroller;
 import processing.app.tools.Tool;
+import processing.app.tools.WatchDir;
 
 import javax.swing.*;
 import javax.swing.event.*;
@@ -72,11 +73,19 @@
 import static processing.app.I18n.tr;
 import static processing.app.Theme.scale;
 
+import static java.nio.file.StandardWatchEventKinds.*;
+import java.nio.file.WatchService;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchEvent;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.io.File;
+
 /**
  * Main editor panel for the Processing Development Environment.
  */
 @SuppressWarnings("serial")
-public class Editor extends JFrame implements RunnerListener {
+public class Editor extends JFrame implements RunnerListener, FocusListener {
 
   public static final int MAX_TIME_AWAITING_FOR_RESUMING_SERIAL_MONITOR = 10000;
 
@@ -198,6 +207,9 @@ public boolean test(SketchController sketch) {
   private Runnable exportAppHandler;
   private Runnable timeoutUploadHandler;
 
+  protected Thread watcher = null;
+  protected Runnable task = null;
+
   public Editor(Base ibase, File file, int[] storedLocation, int[] defaultLocation, Platform platform) throws Exception {
     super("Arduino");
     this.base = ibase;
@@ -341,6 +353,21 @@ public void windowDeactivated(WindowEvent e) {
     if (!loaded) sketchController = null;
   }
 
+  @Override
+  public void focusGained(FocusEvent fe){
+    if (watcher != null) {
+      watcher.interrupt();
+      watcher = null;
+    }
+  }
+
+  @Override
+  public void focusLost(FocusEvent fe){
+    if (watcher == null) {
+      watcher = new Thread(task);
+      watcher.start();
+    }
+  }
 
   /**
    * Handles files dragged & dropped from the desktop and into the editor
@@ -1649,7 +1676,7 @@ public void reorderTabs() {
    *          the given file.
    * @throws IOException
    */
-  protected void addTab(SketchFile file, String contents) throws IOException {
+  public synchronized void addTab(SketchFile file, String contents) throws IOException {
     EditorTab tab = new EditorTab(this, file, contents);
     tab.getTextArea().getDocument()
         .addDocumentListener(new DocumentTextChangeListener(
@@ -1658,7 +1685,7 @@ protected void addTab(SketchFile file, String contents) throws IOException {
     reorderTabs();
   }
 
-  protected void removeTab(SketchFile file) throws IOException {
+  public synchronized void removeTab(SketchFile file) throws IOException {
     int index = findTabIndex(file);
     tabs.remove(index);
   }
@@ -1927,6 +1954,25 @@ protected boolean handleOpenInternal(File sketchFile) {
     // Disable untitled setting from previous document, if any
     untitled = false;
 
+	// Add FS watcher for current Editor instance
+    Path dir = file.toPath().getParent();
+
+	Editor instance = this;
+
+    task = new Runnable() {
+      public void run() {
+        try {
+          new WatchDir(dir, true).processEvents(instance);
+        } catch (IOException x) {
+          System.err.println(x);
+        }
+      }
+    };
+
+    addFocusListener(this);
+    getTabs().forEach(tab -> tab.getScrollPane().addFocusListener(this));
+    getTabs().forEach(tab -> tab.getTextArea().addFocusListener(this));
+
     // opening was successful
     return true;
   }
diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java
index eab701d80a0..4ef3d7bf5ef 100644
--- a/app/src/processing/app/EditorTab.java
+++ b/app/src/processing/app/EditorTab.java
@@ -30,6 +30,8 @@
 import java.awt.Font;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
 import java.io.IOException;
 
 import javax.swing.Action;
@@ -47,6 +49,14 @@
 import javax.swing.text.DefaultCaret;
 import javax.swing.text.Document;
 
+import static java.nio.file.StandardWatchEventKinds.*;
+import java.nio.file.WatchService;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchEvent;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.io.File;
+
 import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
 import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
 import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
@@ -60,6 +70,7 @@
 import processing.app.syntax.SketchTextArea;
 import processing.app.syntax.SketchTextAreaEditorKit;
 import processing.app.tools.DiscourseFormat;
+import processing.app.tools.WatchDir;
 
 /**
  * Single tab, editing a single file, in the main window.
@@ -106,6 +117,8 @@ public EditorTab(Editor editor, SketchFile file, String contents)
     file.setStorage(this);
     applyPreferences();
     add(scrollPane, BorderLayout.CENTER);
+    setFocusable(true);
+    setRequestFocusEnabled(true);
   }
 
   private RSyntaxDocument createDocument(String contents) {
@@ -120,6 +133,7 @@ private RSyntaxDocument createDocument(String contents) {
     }
     document.addDocumentListener(new DocumentTextChangeListener(
         () -> setModified(true)));
+
     return document;
   }
   
@@ -456,7 +470,11 @@ public void setSelection(int start, int stop) {
   public int getScrollPosition() {
     return scrollPane.getVerticalScrollBar().getValue();
   }
-    
+
+  public RTextScrollPane getScrollPane() {
+    return scrollPane;
+  }
+
   public void setScrollPosition(int pos) {
     scrollPane.getVerticalScrollBar().setValue(pos);
   }
diff --git a/app/src/processing/app/tools/WatchDir.java b/app/src/processing/app/tools/WatchDir.java
new file mode 100644
index 00000000000..bc2f35e74c9
--- /dev/null
+++ b/app/src/processing/app/tools/WatchDir.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ *   - Neither the name of Oracle nor the names of its
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package processing.app.tools;
+
+import java.nio.file.*;
+import static java.nio.file.StandardWatchEventKinds.*;
+import static java.nio.file.LinkOption.*;
+import java.nio.file.attribute.*;
+import java.io.*;
+import java.util.*;
+import processing.app.Editor;
+import processing.app.EditorTab;
+import processing.app.Sketch;
+import processing.app.SketchFile;
+import processing.app.helpers.FileUtils;
+
+/**
+ * Example to watch a directory (or tree) for changes to files.
+ */
+
+public class WatchDir {
+
+    private final WatchService watcher;
+    private final Map<WatchKey,Path> keys;
+    private final boolean recursive;
+    private boolean trace = false;
+
+    @SuppressWarnings("unchecked")
+    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
+        return (WatchEvent<T>)event;
+    }
+
+    /**
+     * Register the given directory with the WatchService
+     */
+    private void register(Path dir) throws IOException {
+        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
+        if (trace) {
+            Path prev = keys.get(key);
+            if (prev == null) {
+            } else {
+                if (!dir.equals(prev)) {
+                }
+            }
+        }
+        keys.put(key, dir);
+    }
+
+    /**
+     * Register the given directory, and all its sub-directories, with the
+     * WatchService.
+     */
+    private void registerAll(final Path start) throws IOException {
+        // register directory and sub-directories
+        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+                throws IOException
+            {
+                register(dir);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    /**
+     * Creates a WatchService and registers the given directory
+     */
+    public WatchDir(Path dir, boolean recursive) throws IOException {
+        this.watcher = FileSystems.getDefault().newWatchService();
+        this.keys = new HashMap<WatchKey,Path>();
+        this.recursive = recursive;
+
+        if (recursive) {
+            registerAll(dir);
+        } else {
+            register(dir);
+        }
+
+        // enable trace after initial registration
+        this.trace = true;
+    }
+
+    /**
+     * Process all events for keys queued to the watcher
+     */
+    public void processEvents(Editor editor) {
+        for (;;) {
+
+            // wait for key to be signalled
+            WatchKey key;
+            try {
+                key = watcher.take();
+            } catch (InterruptedException x) {
+                return;
+            }
+
+            Path dir = keys.get(key);
+            if (dir == null) {
+                continue;
+            }
+
+            for (WatchEvent<?> event: key.pollEvents()) {
+                WatchEvent.Kind kind = event.kind();
+
+                // TBD - provide example of how OVERFLOW event is handled
+                if (kind == OVERFLOW) {
+                    continue;
+                }
+
+                // Context for directory entry event is the file name of entry
+                WatchEvent<Path> ev = cast(event);
+                Path name = ev.context();
+                Path child = dir.resolve(name);
+
+                // reload the tab content
+                if (kind == ENTRY_CREATE) {
+					try {
+						String filename = name.toString();
+						FileUtils.SplitFile split = FileUtils.splitFilename(filename);
+						if (Sketch.EXTENSIONS.contains(split.extension.toLowerCase())) {
+							SketchFile sketch = editor.getSketch().addFile(filename);
+							editor.addTab(sketch, null);
+						}
+					} catch (IOException e) {}
+                } else if (kind == ENTRY_DELETE) {
+					List<EditorTab> tabs = editor.getTabs();
+					Iterator<EditorTab> iter = tabs.iterator();
+					while (iter.hasNext()) {
+						EditorTab tab = iter.next();
+						if (name.getFileName().toString().equals(tab.getSketchFile().getFileName())) {
+							try {
+								editor.removeTab(tab.getSketchFile());
+							} catch (IOException e) {}
+						}
+					}
+				}
+				editor.getTabs().forEach(tab -> tab.reload());
+
+                // if directory is created, and watching recursively, then
+                // register it and its sub-directories
+                if (recursive && (kind == ENTRY_CREATE)) {
+                    try {
+                        if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
+                            registerAll(child);
+                        }
+                    } catch (IOException x) {
+                        // ignore to keep sample readbale
+                    }
+                }
+            }
+
+            // reset key and remove from set if directory no longer accessible
+            boolean valid = key.reset();
+            if (!valid) {
+                keys.remove(key);
+
+                // all directories are inaccessible
+                if (keys.isEmpty()) {
+                    break;
+                }
+            }
+        }
+    }
+}