From 14375eb8776409648dab0cb539a7fbbd957bbeda Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Wed, 14 Dec 2011 15:33:07 -0800
Subject: [PATCH 01/14] Initial implementation of File > Save and dirty bit
 management

---
 src/Commands.js |  3 ++-
 src/brackets.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 59 insertions(+), 4 deletions(-)

diff --git a/src/Commands.js b/src/Commands.js
index f7927341a80..829552fd979 100644
--- a/src/Commands.js
+++ b/src/Commands.js
@@ -4,5 +4,6 @@
 
 // List of constants for global command IDs.
 var Commands = {
-    FILE_OPEN: "file.open"
+    FILE_OPEN: "file.open",
+    FILE_SAVE: "file.save"
 };
diff --git a/src/brackets.js b/src/brackets.js
index 4423ca4d2bb..814877100c6 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -28,10 +28,13 @@ $(document).ready(function() {
 		ProjectManager.openProject();
 	});
     
-    // Implements the "Open File" menu
+    // Implements the File menu items
     $("#menu-file-open").click(function() {
         CommandManager.execute(Commands.FILE_OPEN);
     });
+    $("#menu-file-save").click(function() {
+        CommandManager.execute(Commands.FILE_SAVE);
+    })
     
     // Implements the 'Run Tests' menu to bring up the Jasmine unit test window
     var testWindow = null;
@@ -49,8 +52,42 @@ $(document).ready(function() {
             testWindow.location.reload(); // if it was opened before, we need to reload because it will be cached
         }
     });
+    
+    // Application state
+    // TODO: factor this stuff out into a real app controller
+    var _currentFilePath = null;
+    var _currentTitlePath = null;
+    var _isDirty = false;
+    var _savedUndoPosition = 0;
+    
+    editor.setOption("onChange", function() {
+        updateDirty();
+    });
 
     // Utility functions
+    function updateDirty() {
+        // TODO: This doesn't currently work properly with undo because of brackets-app issue #9.
+        // So files get dirty, but they never get un-dirty.
+        
+        // If we've undone past the undo position at the last save, and there is no redo stack,
+        // then we can never get back to non-dirty state.
+        var historySize = editor.historySize();
+        var historyInfo = editor.historyInfo();
+        console.log(JSON.stringify(historyInfo));
+        if (historySize.undo < _savedUndoPosition && historySize.redo == 0) {
+            _savedUndoPosition = -1;
+        }
+        var newIsDirty = (editor.historySize().undo != _savedUndoPosition);
+        if (_isDirty != newIsDirty) {
+            _isDirty = newIsDirty;
+            updateTitle();
+        }        
+    }
+    
+    function updateTitle() {
+        $("#main-toolbar .title").text(_currentTitlePath + (_isDirty ? " \u2022" : ""));
+    }
+    
     function doOpen(fullPath) {          
         if (fullPath) {
             // TODO: use higher-level file API instead of raw API
@@ -59,6 +96,8 @@ $(document).ready(function() {
                     // TODO--this will change with the real file API implementation
                 }
                 else {
+                    _currentFilePath = _currentTitlePath = fullPath;
+                    
                     // TODO: have a real controller object for the editor
                     editor.setValue(content);
 
@@ -66,10 +105,12 @@ $(document).ready(function() {
                     if (fullPath.indexOf(projectRootPath) == 0) {
                         fullPath = fullPath.slice(projectRootPath.length);
                         if (fullPath.charAt(0) == '/') {
-                            fullPath = fullPath.slice(1);
+                            _currentTitlePath = fullPath.slice(1);
                         }                          
                     }
-                    $("#main-toolbar .title").text(fullPath);
+                    editor.clearHistory();
+                    _savedUndoPosition = editor.historySize().undo;
+                    updateDirty();
                 }
             });
         }
@@ -91,4 +132,17 @@ $(document).ready(function() {
         }
     });
 
+    CommandManager.register(Commands.FILE_SAVE, function() {
+        if (_currentFilePath && _isDirty) {
+            brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) {
+                if (err) {
+                    // TODO--this will change with the real file API implementation
+                }
+                else {
+                    _savedUndoPosition = editor.historySize().undo;
+                    updateDirty();
+                }
+            });
+        }
+    });
 });

From f048b02f1232898588cc5192ccc00bc925b26704 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Wed, 14 Dec 2011 16:17:55 -0800
Subject: [PATCH 02/14] Removed call to temp debug function

---
 src/brackets.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/brackets.js b/src/brackets.js
index 814877100c6..a705e8b1c79 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -72,8 +72,6 @@ $(document).ready(function() {
         // If we've undone past the undo position at the last save, and there is no redo stack,
         // then we can never get back to non-dirty state.
         var historySize = editor.historySize();
-        var historyInfo = editor.historyInfo();
-        console.log(JSON.stringify(historyInfo));
         if (historySize.undo < _savedUndoPosition && historySize.redo == 0) {
             _savedUndoPosition = -1;
         }

From c4493d7b42973438863554e68a63dd5dcb7510ed Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Thu, 15 Dec 2011 09:57:37 -0800
Subject: [PATCH 03/14] Added comments around our dirty bit management

---
 src/brackets.js | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/brackets.js b/src/brackets.js
index a705e8b1c79..cc8e12a16e2 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -66,11 +66,8 @@ $(document).ready(function() {
 
     // Utility functions
     function updateDirty() {
-        // TODO: This doesn't currently work properly with undo because of brackets-app issue #9.
-        // So files get dirty, but they never get un-dirty.
-        
         // If we've undone past the undo position at the last save, and there is no redo stack,
-        // then we can never get back to non-dirty state.
+        // then we can never get back to a non-dirty state.
         var historySize = editor.historySize();
         if (historySize.undo < _savedUndoPosition && historySize.redo == 0) {
             _savedUndoPosition = -1;
@@ -106,7 +103,11 @@ $(document).ready(function() {
                             _currentTitlePath = fullPath.slice(1);
                         }                          
                     }
+                    
+                    // Make sure we can't undo back to the previous content.
                     editor.clearHistory();
+                    
+                    // This should be 0, but just to be safe...
                     _savedUndoPosition = editor.historySize().undo;
                     updateDirty();
                 }
@@ -137,6 +138,8 @@ $(document).ready(function() {
                     // TODO--this will change with the real file API implementation
                 }
                 else {
+                    // Remember which position in the undo stack we're at as of the last save.
+                    // When we're exactly at that position again, we know we're not dirty.
                     _savedUndoPosition = editor.historySize().undo;
                     updateDirty();
                 }

From 8941cd9cc6583a0151f3b9f0e61ab946fb3babd0 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Thu, 15 Dec 2011 15:56:29 -0800
Subject: [PATCH 04/14] Beginning implementation of prompt when closing dirty
 file

---
 src/Commands.js | 3 ++-
 src/brackets.js | 9 +++++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/Commands.js b/src/Commands.js
index 829552fd979..b1d7c127eba 100644
--- a/src/Commands.js
+++ b/src/Commands.js
@@ -5,5 +5,6 @@
 // List of constants for global command IDs.
 var Commands = {
     FILE_OPEN: "file.open",
-    FILE_SAVE: "file.save"
+    FILE_SAVE: "file.save",
+    FILE_CLOSE: "file.close"
 };
diff --git a/src/brackets.js b/src/brackets.js
index cc8e12a16e2..36cfaa889a5 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -117,6 +117,15 @@ $(document).ready(function() {
     
     // Register global commands
     CommandManager.register(Commands.FILE_OPEN, function(fullPath) {
+        // TODO: In the future, when we implement multiple open files, we won't close the previous file when opening
+        // a new one. However, for now, since we only support a single open document, I'm pretending as if we're 
+        // closing the existing file first. This is so that I can put the code that checks for an unsaved file and 
+        // prompts the user to save it in the close command, where it belongs. When we implement multiple open files,
+        // we can remove this here.
+        if (_currentFilePath) {
+            CommandManager.execute(Commands.FILE_CLOSE);
+        }
+        
         if (!fullPath) {
             // Prompt the user with a dialog
             NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, 

From 0d169b91554859284949b91cf0a0b59897be64ee Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Thu, 15 Dec 2011 18:48:38 -0800
Subject: [PATCH 05/14] Implementing file close--prompting to save not yet
 implemented

---
 src/brackets.js | 23 +++++++++++++++++++++--
 src/index.html  |  1 +
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/brackets.js b/src/brackets.js
index ac991a431b9..4954d506980 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -59,9 +59,12 @@ $(document).ready(function() {
     $("#menu-file-open").click(function() {
         CommandManager.execute(Commands.FILE_OPEN);
     });
+    $("#menu-file-close").click(function() {
+        CommandManager.execute(Commands.FILE_CLOSE);
+    });
     $("#menu-file-save").click(function() {
         CommandManager.execute(Commands.FILE_SAVE);
-    })
+    });
     
     // Implements the 'Run Tests' menu to bring up the Jasmine unit test window
     var testWindow = null;
@@ -107,7 +110,7 @@ $(document).ready(function() {
     }
     
     function updateTitle() {
-        $("#main-toolbar .title").text(_currentTitlePath + (_isDirty ? " \u2022" : ""));
+        $("#main-toolbar .title").text(_currentTitlePath ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) : "Untitled");
     }
     
     function doOpen(fullPath) {          
@@ -200,4 +203,20 @@ $(document).ready(function() {
             });
         }
     });
+    
+    CommandManager.register(Commands.FILE_CLOSE, function() {
+        if (_currentFilePath && _isDirty) {
+            // *** TODO: prompt to save
+        }
+        
+        // TODO: When we implement multiple files being open, this will probably change to just
+        // dispose of the editor for the current file (and will later change again if we choose to
+        // limit the number of open editors).
+        editor.setValue("");
+        editor.clearHistory();
+        _currentFilePath = _currentTitlePath = null;
+        _savedUndoPosition = 0;
+        _isDirty = false;
+        updateTitle();
+    });
 });
diff --git a/src/index.html b/src/index.html
index 442fcd6858a..d2456289d3a 100644
--- a/src/index.html
+++ b/src/index.html
@@ -41,6 +41,7 @@
 				<ul class="dropdown-menu">
 					<li><a href="#" id="menu-file-new">New</a></li>
 					<li><a href="#" id="menu-file-open">Open</a></li>
+					<li><a href="#" id="menu-file-close">Close</a></li>
 					<li><a href="#" id="menu-file-save">Save</a></li>
 					<li><a href="#" id="menu-file-quit">Quit</a></li>
 				</ul>

From 33e8cfe0c083313604d6c8e47f631227cf99101d Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Thu, 15 Dec 2011 22:23:42 -0800
Subject: [PATCH 06/14] Added dialog when closing dirty file. Refactored dialog
 code to make it more generic, driven by HTML. Added basic keymapping
 functions.

---
 src/CommandManager.js    |   2 +
 src/KeyBindingManager.js | 112 +++++++++++++++++++++++++++++++++++++++
 src/ProjectManager.js    |  15 +++---
 src/brackets.js          |  95 ++++++++++++++++++++++++++-------
 src/index.html           |  22 ++++++--
 src/strings.js           |   7 ++-
 src/styles/brackets.less |   4 ++
 7 files changed, 227 insertions(+), 30 deletions(-)
 create mode 100644 src/KeyBindingManager.js

diff --git a/src/CommandManager.js b/src/CommandManager.js
index fa4e20f51e4..5849cc0cefc 100644
--- a/src/CommandManager.js
+++ b/src/CommandManager.js
@@ -26,6 +26,8 @@ CommandManager.register = function(id, command) {
  * @param {string} id The ID of the command to run.
  */
 CommandManager.execute = function(id) {
+    // TODO: are we guaranteed that all commands are synchronous? If not, we should
+    // take a callback, or (perhaps better) return a Deferred.
     var command = CommandManager._commands[id];
     if (command) {
         command.apply(null, Array.prototype.slice.call(arguments, 1));
diff --git a/src/KeyBindingManager.js b/src/KeyBindingManager.js
new file mode 100644
index 00000000000..a5333b4ac86
--- /dev/null
+++ b/src/KeyBindingManager.js
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
+ */
+
+var KeyBindingManager = {
+    /**
+     * The map of all registered keymaps.
+     */
+    _keymaps: {},
+
+    /**
+     * The map of currently active keymaps. As new keymaps come into effect, they are pushed onto the end of
+     * the array. Later keymaps take precedence over earlier ones.
+     */
+    _activeKeymaps: {},
+
+    /**
+     * The maximum possible keymap level.
+     */
+    _maxLevel: -1,
+
+    /**
+     * Registers the specified keymap based on its id.
+     *
+     * TODO: do Closure annotations support user-defined types?
+     * @param {KeyMap} keymap The keymap to register.
+     */
+    registerKeymap: function(keymap) {
+        KeyBindingManager._keymaps[keymap.id] = keymap;
+        if (keymap.level > KeyBindingManager._maxLevel) {
+            KeyBindingManager._maxLevel = keymap.level;
+        }
+    },
+
+    /**
+     * Activate the specified keymap. This installs it at the appropriate level, replacing any other keymap
+     * at that level.
+     *
+     * TODO: does the Closure annotation syntax support user-created types?
+     * @param {string} keymap The ID of the registered keymap to install.
+     */
+    activateKeymap: function(id) {
+        var keymap = KeyBindingManager._keymaps[id];
+        if (!keymap) {
+            throw new Error("Keymap " + id + " not registered");
+        }
+    
+        KeyBindingManager._activeKeymaps[keymap.level] = keymap;
+    },
+
+    /**
+     * Deactivate the keymap with the given ID. If it's not already active, does nothing.
+     *
+     * @param {string} id The ID of the keymap to deactivate.
+     */
+    deactivateKeymap: function(id) {
+        for (var i = 0; i < KeyBindingManager._maxLevel; i++) {
+            if (KeyBindingManager._activeKeymaps[i] && KeyBindingManager._activeKeymaps[i].id === id) {
+                delete KeyBindingManager._activeKeymaps[i];
+            }
+        }
+    },
+
+    /**
+     * Process the keybinding for the current key.
+     *
+     * @param {string} A key-description string.
+     * @return {boolean} true if the key was processed, false otherwise
+     */
+    handleKey: function(key) {
+        for (var i = KeyBindingManager._maxLevel; i >= 0; i--) {
+            var keymap = KeyBindingManager._activeKeymaps[i];
+            if (keymap && keymap.map[key]) {
+                CommandManager.execute(keymap.map[key]);
+                return true;
+            }
+        }        
+        return false;
+    }
+};
+
+/** class Keymap
+ *
+ * A keymap specifies how keys are mapped to commands. The KeyBindingManager allows for a hierarchy of
+ * keymaps, specified by a numeric level. It does not dictate the semantics of these levels, but in
+ * practice, level 0 is a global keymap, level 1 is a file-type-specific keymap, and deeper levels can
+ * be used for more specific contexts. When a new keymap is activated, it replaces any other keymap at
+ * its same level.
+ *
+ * Keys are described by strings of the form "[modifier-modifier-...-]key", where modifier is one of
+ * Ctrl, Alt, or Shift. If multiple modifiers are specified, they must be specified in that order
+ * (i.e. "Ctrl-Alt-Shift-P" is legal, "Alt-Ctrl-Shift-P" is not). 
+ * (TODO: the above restriction is to simplify mapping--is it too onerous?)
+ *    -- Ctrl maps to Cmd on Mac. (This means that you can't specifically bind to the Ctrl key on Mac.)
+ *    -- Alt maps to the Option key on Mac.
+ *    -- Letters must be uppercase, but do not require Shift by default. To indicate that Shift must be held
+ *       down, you must specifically include Shift.
+ *
+ * @constructor
+ * @param {string} id A unique ID for this keymap.
+ * @param {number} level The level of this keymap. Must be an integer >= 0.
+ * @param {map} map An object mapping key-description strings to command IDs.
+ */
+var KeyMap = function(id, level, map) {
+    if (id === undefined || level === undefined || map === undefined) {
+        throw new Error("All parameters to the KeyMap constructor must be specified");
+    }
+    
+    this.id = id;
+    this.level = level;
+    this.map = map;
+};
diff --git a/src/ProjectManager.js b/src/ProjectManager.js
index f717a66797e..ac0dfe6d951 100644
--- a/src/ProjectManager.js
+++ b/src/ProjectManager.js
@@ -34,8 +34,9 @@ ProjectManager.openProject = function() {
                     ProjectManager.loadProject( files[0] );
             },
             function(error) {
-                brackets.showErrorDialog(
-                      brackets.strings.ERROR_LOADING_PROJECT
+                brackets.showDialog(
+                      brackets.DIALOG_ID_ERROR
+                    , brackets.strings.ERROR_LOADING_PROJECT
                     , brackets.strings.format(brackets.strings.OPEN_DIALOG_ERROR, error.code)
                 );
             }
@@ -89,8 +90,9 @@ ProjectManager.loadProject = function(rootPath) {
                 ProjectManager._renderTree(ProjectManager._treeDataProvider);
             },
             function(error) {
-                brackets.showErrorDialog(
-                      brackets.strings.ERROR_LOADING_PROJECT
+                brackets.showDialog(
+                      brackets.DIALOG_ID_ERROR
+                    , brackets.strings.ERROR_LOADING_PROJECT
                     , brackets.strings.format(brackets.strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, rootPath, error.code)
                 );
             }
@@ -126,8 +128,9 @@ ProjectManager._treeDataProvider = function(treeNode, jsTreeCallback) {
             jsTreeCallback(subtreeJSON);
         },
         function(error) {
-            brackets.showErrorDialog(
-                  brackets.strings.ERROR_LOADING_PROJECT
+            brackets.showDialog(
+                  brackets.DIALOG_ID_ERROR
+                , brackets.strings.ERROR_LOADING_PROJECT
                 , brackets.strings.format(brackets.strings.READ_DIRECTORY_ENTRIES_ERROR, dirEntry.fullPath, error.code)
             );
         }
diff --git a/src/brackets.js b/src/brackets.js
index 4954d506980..84829412991 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -9,21 +9,36 @@ brackets = window.brackets || {};
 
 brackets.inBrowser = !brackets.hasOwnProperty("fs");
 
+brackets.DIALOG_BTN_CANCEL = "cancel";
+brackets.DIALOG_BTN_OK = "ok";
+brackets.DIALOG_BTN_DONTSAVE = "dontsave";
+
+brackets.DIALOG_ID_ERROR = "error-dialog";
+brackets.DIALOG_ID_SAVE_CLOSE = "save-close-dialog";
+
 /**
- * General purpose modal error dialog. 
+ * General purpose modal dialog. Assumes that the HTML for the dialog contains elements with "title"
+ * and "message" classes, as well as a number of elements with "dialog-button" class, each of which has
+ * a "data-button-id".
  *
+ * @param {string} id The ID of the dialog node in the HTML.
  * @param {string} title The title of the error dialog. Can contain HTML markup.
  * @param {string} message The message to display in the error dialog. Can contain HTML markup.
+ * @param {function=} callback The optional callback to be called when the dialog is dismissed. Called with
+ *     the value of the data-button-id of the clicked button.
  */
-brackets.showErrorDialog = function(title, message) {
-    var dlg = $("#error-dialog");
+brackets.showDialog = function(id, title, message, callback) {
+    var dlg = $("#" + id);
     
     // Set title and message
-    $("#error-dialog-title").html(title);
-    $("#error-dialog-message").html(message);
+    $(".dialog-title", dlg).html(title);
+    $(".dialog-message", dlg).html(message);
     
-    // Click handler for OK button
-    dlg.delegate("#error-dialog-ok", "click", function(e) {
+    // Click handler for buttons
+    dlg.on("click", ".dialog-button", function(e) {
+        if (callback) {
+            callback($(this).attr("data-button-id"));
+        }
         dlg.modal(true).hide();
     });
     
@@ -33,7 +48,7 @@ brackets.showErrorDialog = function(title, message) {
         , show: true
         }
     );
-}
+};
 
 $(document).ready(function() {
 
@@ -149,6 +164,8 @@ $(document).ready(function() {
                     // This should be 0, but just to be safe...
                     _savedUndoPosition = editor.historySize().undo;
                     updateDirty();
+
+                    editor.focus();
                 };
                 
                 reader.onerror = function(event) {
@@ -200,23 +217,63 @@ $(document).ready(function() {
                     _savedUndoPosition = editor.historySize().undo;
                     updateDirty();
                 }
+                editor.focus();
             });
         }
     });
     
     CommandManager.register(Commands.FILE_CLOSE, function() {
         if (_currentFilePath && _isDirty) {
-            // *** TODO: prompt to save
+            brackets.showDialog(
+                  brackets.DIALOG_ID_SAVE_CLOSE
+                , brackets.strings.SAVE_CLOSE_TITLE
+                , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath)
+                , function(id) {
+                    if (id !== brackets.DIALOG_BTN_CANCEL) {
+                        if (id === brackets.DIALOG_BTN_OK) {
+                            CommandManager.execute(Commands.FILE_SAVE);
+                        }   
+                            
+                        // TODO: When we implement multiple files being open, this will probably change to just
+                        // dispose of the editor for the current file (and will later change again if we choose to
+                        // limit the number of open editors).
+                        editor.setValue("");
+                        editor.clearHistory();
+                        _currentFilePath = _currentTitlePath = null;
+                        _savedUndoPosition = 0;
+                        _isDirty = false;
+                        updateTitle();
+                        editor.focus();
+                    }
+                }
+            );
+        }
+    });
+    
+    // Register keymaps and install the keyboard handler
+    // TODO: show keyboard equivalents in the menus
+    var KEYMAP_GLOBAL = "global";
+    KeyBindingManager.registerKeymap(new KeyMap(KEYMAP_GLOBAL, 0, 
+        { "Ctrl-O": Commands.FILE_OPEN
+        , "Ctrl-S": Commands.FILE_SAVE
+        , "Ctrl-W": Commands.FILE_CLOSE
+        }));
+    KeyBindingManager.activateKeymap(KEYMAP_GLOBAL);
+    
+    $(document.body).keydown(function(event) {
+        var keyDescriptor = [];
+        if (event.metaKey || event.ctrlKey) {
+            keyDescriptor.push("Ctrl");
+        }
+        if (event.altKey) {
+            keyDescriptor.push("Alt");
+        }
+        if (event.shiftKey) {
+            keyDescriptor.push("Shift");
+        }
+        keyDescriptor.push(String.fromCharCode(event.keyCode).toUpperCase());
+        if (KeyBindingManager.handleKey(keyDescriptor.join("-"))) {
+            event.preventDefault();
         }
-        
-        // TODO: When we implement multiple files being open, this will probably change to just
-        // dispose of the editor for the current file (and will later change again if we choose to
-        // limit the number of open editors).
-        editor.setValue("");
-        editor.clearHistory();
-        _currentFilePath = _currentTitlePath = null;
-        _savedUndoPosition = 0;
-        _isDirty = false;
-        updateTitle();
     });
 });
diff --git a/src/index.html b/src/index.html
index d2456289d3a..560764cb0e0 100644
--- a/src/index.html
+++ b/src/index.html
@@ -23,6 +23,7 @@
 	<script src="NativeFileSystem.js"></script>	
 	<script src="CommandManager.js"></script>
 	<script src="Commands.js"></script>
+	<script src="KeyBindingManager.js"></script>
 	<script src="ProjectManager.js"></script>
 	<script src="brackets.js"></script>
 	<script src="strings.js"></script>
@@ -86,13 +87,28 @@
 	<div id="error-dialog" class="modal hide fade">
 		<div class="modal-header">
 			<a href="#" class="close">&times;</a>
-			<h3 id="error-dialog-title">Error</h3>
+			<h3 class="dialog-title">Error</h3>
 		</div>
 		<div class="modal-body">
-			<p id="error-dialog-message">Message goes here</p>
+			<p class="dialog-message">Message goes here</p>
 		</div>
 		<div class="modal-footer">
-			<a id="error-dialog-ok" href="#" class="btn primary">OK</a>
+			<a href="#" class="dialog-button btn primary" data-button-id="ok">OK</a>
+		</div>		
+	</div>
+	<div id="save-close-dialog" class="modal hide fade">
+		<div class="modal-header">
+			<a href="#" class="close">&times;</a>
+			<h3 class="dialog-title">Save Changes</h3>
+		</div>
+		<div class="modal-body">
+			<p class="dialog-message">Message goes here</p>
+		</div>
+		<div class="modal-footer">
+			<a href="#" class="dialog-button btn left" data-button-id="dontsave">Don't Save</a>
+			<!-- TODO: switch buttons on Windows -->
+			<a href="#" class="dialog-button btn" data-button-id="cancel">Cancel</a>
+			<a href="#" class="dialog-button btn primary" data-button-id="ok">Save</a>
 		</div>		
 	</div>
 </body>
diff --git a/src/strings.js b/src/strings.js
index a45d57ce935..102bf9c8117 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -33,6 +33,9 @@ if (!brackets.strings)
     // Project error strings
     s.ERROR_LOADING_PROJECT = "Error loading project";
     s.OPEN_DIALOG_ERROR = "An error occurred when showing the open file dialog. (error {0})";
-    s.REQUEST_NATIVE_FILE_SYSTEM_ERROR = "An error occurred when trying to load the directory '{0}'. (error {1})";
-    s.READ_DIRECTORY_ENTRIES_ERROR = "An error occurred when reading the contents of the directory '{0}'. (error {1})";
+    s.REQUEST_NATIVE_FILE_SYSTEM_ERROR = "An error occurred when trying to load the directory \"{0}\". (error {1})";
+    s.READ_DIRECTORY_ENTRIES_ERROR = "An error occurred when reading the contents of the directory \"{0}\". (error {1})";
+    
+    s.SAVE_CLOSE_TITLE = "Save Changes";
+    s.SAVE_CLOSE_MESSAGE = "Do you want to save the changes you made in the document \"{0}\"?";
 })();
\ No newline at end of file
diff --git a/src/styles/brackets.less b/src/styles/brackets.less
index 6ec96e6b059..d835b912e4b 100644
--- a/src/styles/brackets.less
+++ b/src/styles/brackets.less
@@ -73,6 +73,10 @@
 	.box-shadow(0 1px 3px 0 rgba(0, 0, 0, 0.3));
 }
 
+.modal-footer .btn.left {
+    float: left;
+}
+
 /* Overall layout */
 
 html, body {

From 72d998c8efb338f1d98e4b910b830a4ca4a6328a Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Thu, 15 Dec 2011 22:52:34 -0800
Subject: [PATCH 07/14] Made commands rely on $.Deferred() to handle
 asynchronicity, and updated logic of the open/save/close commands to take
 advantage of this.

---
 src/CommandManager.js | 16 ++++++--
 src/brackets.js       | 96 ++++++++++++++++++++++++++++++++-----------
 2 files changed, 83 insertions(+), 29 deletions(-)

diff --git a/src/CommandManager.js b/src/CommandManager.js
index 5849cc0cefc..ae34211e04c 100644
--- a/src/CommandManager.js
+++ b/src/CommandManager.js
@@ -11,7 +11,9 @@ CommandManager._commands = {};
  *
  * @param {string} id The ID of the command.
  * @param {function} command The function to call when the command is executed. Any arguments passed to
- *     execute() (after the id) are passed as arguments to the function.
+ *     execute() (after the id) are passed as arguments to the function. If the function is asynchronous,
+ *     it must return a jQuery Deferred that is resolved when the command completes. Otherwise, the
+ *     CommandManager will assume it is synchronous, and return a Deferred that is already resolved.
  */
 CommandManager.register = function(id, command) {
     if (CommandManager._commands[id]) {
@@ -24,15 +26,21 @@ CommandManager.register = function(id, command) {
  * Runs a global command. Additional arguments are passed to the command.
  *
  * @param {string} id The ID of the command to run.
+ * @return {Deferred} a jQuery Deferred that will be resolved when the command completes.
  */
 CommandManager.execute = function(id) {
-    // TODO: are we guaranteed that all commands are synchronous? If not, we should
-    // take a callback, or (perhaps better) return a Deferred.
     var command = CommandManager._commands[id];
     if (command) {
-        command.apply(null, Array.prototype.slice.call(arguments, 1));
+        var result = command.apply(null, Array.prototype.slice.call(arguments, 1));
+        if (result === undefined) {
+            return $.Deferred().resolve();
+        }
+        else {
+            return result;
+        }
     }
     else {
         console.log("Attempted to call unregistered command: " + id);
+        return $.Deferred().reject();
     }
 }
diff --git a/src/brackets.js b/src/brackets.js
index 84829412991..45f8dab5e71 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -128,7 +128,24 @@ $(document).ready(function() {
         $("#main-toolbar .title").text(_currentTitlePath ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) : "Untitled");
     }
     
-    function doOpen(fullPath) {          
+    function doOpenWithOptionalPath(fullPath) {     
+        if (!fullPath) {
+            // Prompt the user with a dialog
+            // TODO: we're relying on this to not be asynchronous--is that safe?
+            NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, 
+                ["htm", "html", "js", "css"], function(files) {
+                    if (files.length > 0) {
+                        return doOpen(files[0]);
+                    }
+                });
+        }
+        else {
+            return doOpen(fullPath);
+        }
+    }
+    
+    function doOpen(fullPath) { 
+        var result = $.Deferred();         
         if (fullPath) {
             var reader = new NativeFileSystem.FileReader();
 
@@ -166,10 +183,12 @@ $(document).ready(function() {
                     updateDirty();
 
                     editor.focus();
+                    result.resolve();
                 };
                 
                 reader.onerror = function(event) {
                     // TODO: display meaningful error
+                    result.reject();
                 }
                 
                 reader.readAsText(file, "utf8");
@@ -178,6 +197,20 @@ $(document).ready(function() {
                 // TODO: display meaningful error
             });
         }
+        return result;
+    }
+    
+    function doClose() {
+        // TODO: When we implement multiple files being open, this will probably change to just
+        // dispose of the editor for the current file (and will later change again if we choose to
+        // limit the number of open editors).
+        editor.setValue("");
+        editor.clearHistory();
+        _currentFilePath = _currentTitlePath = null;
+        _savedUndoPosition = 0;
+        _isDirty = false;
+        updateTitle();
+        editor.focus();
     }
     
     // Register global commands
@@ -188,65 +221,78 @@ $(document).ready(function() {
         // prompts the user to save it in the close command, where it belongs. When we implement multiple open files,
         // we can remove this here.
         if (_currentFilePath) {
-            CommandManager.execute(Commands.FILE_CLOSE);
-        }
-        
-        if (!fullPath) {
-            // Prompt the user with a dialog
-            NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, 
-                ["htm", "html", "js", "css"], function(files) {
-                    if (files.length > 0) {
-                        doOpen(files[0]);
-                    }
+            var result = $.Deferred();
+            CommandManager.execute(Commands.FILE_CLOSE).done(function() {
+                doOpenWithOptionalPath(fullPath).done(function() {
+                    result.resolve();
+                })
+                .fail(function() {
+                    result.reject();
                 });
+            })
+            .fail(function() {
+                result.reject();
+            });
+            return result;
         }
         else {
-            doOpen(fullPath);
+            return doOpenWithOptionalPath(fullPath);
         }
     });
 
     CommandManager.register(Commands.FILE_SAVE, function() {
         if (_currentFilePath && _isDirty) {
+            var result = $.Deferred();
             brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) {
                 if (err) {
                     // TODO--this will change with the real file API implementation
+                    result.reject();
                 }
                 else {
                     // Remember which position in the undo stack we're at as of the last save.
                     // When we're exactly at that position again, we know we're not dirty.
                     _savedUndoPosition = editor.historySize().undo;
                     updateDirty();
+                    result.resolve();
                 }
                 editor.focus();
             });
+            return result;
         }
     });
     
     CommandManager.register(Commands.FILE_CLOSE, function() {
         if (_currentFilePath && _isDirty) {
+            var result = $.Deferred();
             brackets.showDialog(
                   brackets.DIALOG_ID_SAVE_CLOSE
                 , brackets.strings.SAVE_CLOSE_TITLE
                 , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath)
                 , function(id) {
-                    if (id !== brackets.DIALOG_BTN_CANCEL) {
+                    if (id === brackets.DIALOG_BTN_CANCEL) {
+                        result.reject();
+                    }
+                    else {
                         if (id === brackets.DIALOG_BTN_OK) {
-                            CommandManager.execute(Commands.FILE_SAVE);
+                            CommandManager.execute(Commands.FILE_SAVE).done(function() {
+                                doClose();
+                                result.resolve();
+                            })
+                            .fail(function() {
+                                result.reject();
+                            });
                         }   
-                            
-                        // TODO: When we implement multiple files being open, this will probably change to just
-                        // dispose of the editor for the current file (and will later change again if we choose to
-                        // limit the number of open editors).
-                        editor.setValue("");
-                        editor.clearHistory();
-                        _currentFilePath = _currentTitlePath = null;
-                        _savedUndoPosition = 0;
-                        _isDirty = false;
-                        updateTitle();
-                        editor.focus();
+                        else {
+                            doClose();
+                            result.resolve();
+                        }
                     }
                 }
             );
+            return result;
+        }
+        else {
+            doClose();
         }
     });
     

From ef8d96c6ab884a07a38efa53d672271713ceeac6 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Thu, 15 Dec 2011 23:12:03 -0800
Subject: [PATCH 08/14] Added clarifying comment in FILE_CLOSE command

---
 src/brackets.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/brackets.js b/src/brackets.js
index 45f8dab5e71..d539871d315 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -283,6 +283,7 @@ $(document).ready(function() {
                             });
                         }   
                         else {
+                            // This is the "Don't Save" case--we can just go ahead and close the file.
                             doClose();
                             result.resolve();
                         }

From b91566c24735f5b705a9aaf8e996e867ed94ab4d Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Fri, 16 Dec 2011 09:45:03 -0800
Subject: [PATCH 09/14] Trying different indentation style for Deferred
 callbacks

---
 src/brackets.js | 58 ++++++++++++++++++++++++++-----------------------
 1 file changed, 31 insertions(+), 27 deletions(-)

diff --git a/src/brackets.js b/src/brackets.js
index d539871d315..6d33d0308cb 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -24,10 +24,11 @@ brackets.DIALOG_ID_SAVE_CLOSE = "save-close-dialog";
  * @param {string} id The ID of the dialog node in the HTML.
  * @param {string} title The title of the error dialog. Can contain HTML markup.
  * @param {string} message The message to display in the error dialog. Can contain HTML markup.
- * @param {function=} callback The optional callback to be called when the dialog is dismissed. Called with
- *     the value of the data-button-id of the clicked button.
+ * @return {Deferred} a $.Deferred() that will be resolved with the ID of the clicked button when the dialog
+ *     is dismissed.
  */
 brackets.showDialog = function(id, title, message, callback) {
+    var result = $.Deferred();
     var dlg = $("#" + id);
     
     // Set title and message
@@ -36,9 +37,7 @@ brackets.showDialog = function(id, title, message, callback) {
     
     // Click handler for buttons
     dlg.on("click", ".dialog-button", function(e) {
-        if (callback) {
-            callback($(this).attr("data-button-id"));
-        }
+        result.resolve($(this).attr("data-button-id"));
         dlg.modal(true).hide();
     });
     
@@ -48,6 +47,7 @@ brackets.showDialog = function(id, title, message, callback) {
         , show: true
         }
     );
+    return result;
 };
 
 $(document).ready(function() {
@@ -222,17 +222,20 @@ $(document).ready(function() {
         // we can remove this here.
         if (_currentFilePath) {
             var result = $.Deferred();
-            CommandManager.execute(Commands.FILE_CLOSE).done(function() {
-                doOpenWithOptionalPath(fullPath).done(function() {
-                    result.resolve();
+            CommandManager
+                .execute(Commands.FILE_CLOSE)
+                .done(function() {
+                    doOpenWithOptionalPath(fullPath)
+                        .done(function() {
+                            result.resolve();
+                        })
+                        .fail(function() {
+                            result.reject();
+                        });
                 })
                 .fail(function() {
                     result.reject();
                 });
-            })
-            .fail(function() {
-                result.reject();
-            });
             return result;
         }
         else {
@@ -245,7 +248,7 @@ $(document).ready(function() {
             var result = $.Deferred();
             brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) {
                 if (err) {
-                    // TODO--this will change with the real file API implementation
+                    // TODO: display meaningful error
                     result.reject();
                 }
                 else {
@@ -268,28 +271,29 @@ $(document).ready(function() {
                   brackets.DIALOG_ID_SAVE_CLOSE
                 , brackets.strings.SAVE_CLOSE_TITLE
                 , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath)
-                , function(id) {
-                    if (id === brackets.DIALOG_BTN_CANCEL) {
-                        result.reject();
-                    }
-                    else {
-                        if (id === brackets.DIALOG_BTN_OK) {
-                            CommandManager.execute(Commands.FILE_SAVE).done(function() {
+            ).done(function(id) {
+                if (id === brackets.DIALOG_BTN_CANCEL) {
+                    result.reject();
+                }
+                else {
+                    if (id === brackets.DIALOG_BTN_OK) {
+                        CommandManager
+                            .execute(Commands.FILE_SAVE)
+                            .done(function() {
                                 doClose();
                                 result.resolve();
                             })
                             .fail(function() {
                                 result.reject();
                             });
-                        }   
-                        else {
-                            // This is the "Don't Save" case--we can just go ahead and close the file.
-                            doClose();
-                            result.resolve();
-                        }
+                    }   
+                    else {
+                        // This is the "Don't Save" case--we can just go ahead and close the file.
+                        doClose();
+                        result.resolve();
                     }
                 }
-            );
+            });
             return result;
         }
         else {

From 43a0b989c716266282caa2bfef7d801abc3ed129 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Fri, 16 Dec 2011 09:49:47 -0800
Subject: [PATCH 10/14] Added another reject() case to doOpen()

---
 src/brackets.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/brackets.js b/src/brackets.js
index 6d33d0308cb..c0454110c79 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -195,6 +195,7 @@ $(document).ready(function() {
             },
             function (error) {
                 // TODO: display meaningful error
+                result.reject();
             });
         }
         return result;
@@ -246,6 +247,7 @@ $(document).ready(function() {
     CommandManager.register(Commands.FILE_SAVE, function() {
         if (_currentFilePath && _isDirty) {
             var result = $.Deferred();
+            var writer = 
             brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) {
                 if (err) {
                     // TODO: display meaningful error

From adb37d0d72919f8d39ec094ea7ff5ab9ec634fe6 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Fri, 16 Dec 2011 10:13:17 -0800
Subject: [PATCH 11/14] Hooked up to HTML file save API

---
 src/brackets.js | 34 ++++++++++++++++++++++------------
 1 file changed, 22 insertions(+), 12 deletions(-)

diff --git a/src/brackets.js b/src/brackets.js
index c0454110c79..093d9a7d4b0 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -245,25 +245,35 @@ $(document).ready(function() {
     });
 
     CommandManager.register(Commands.FILE_SAVE, function() {
+        var result = $.Deferred();
         if (_currentFilePath && _isDirty) {
-            var result = $.Deferred();
-            var writer = 
-            brackets.fs.writeFile(_currentFilePath, editor.getValue(), "utf8", function(err) {
-                if (err) {
-                    // TODO: display meaningful error
-                    result.reject();
-                }
-                else {
-                    // Remember which position in the undo stack we're at as of the last save.
-                    // When we're exactly at that position again, we know we're not dirty.
+            // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar
+            // to what's in the standard file API) to get a FileEntry, rather than manually constructing it
+            var fileEntry = new NativeFileSystem.FileEntry(_currentFilePath);
+            
+            fileEntry.createWriter(function(writer) {
+                writer.onwrite = function() {
                     _savedUndoPosition = editor.historySize().undo;
                     updateDirty();
                     result.resolve();
                 }
-                editor.focus();
+                writer.onerror = function() {
+                    result.reject();
+                }
+                writer.write(editor.getValue());
+            },
+            function(error) {
+                // TODO: display meaningful error
+                result.reject();
             });
-            return result;
         }
+        else {
+            result.resolve();
+        }
+        result.always(function() { 
+            editor.focus(); 
+        });
+        return result;
     });
     
     CommandManager.register(Commands.FILE_CLOSE, function() {

From 3e7c89a5dd71e3ceb1a5e9ee2034960d89b34194 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Fri, 16 Dec 2011 12:55:16 -0800
Subject: [PATCH 12/14] Renamed showDialog() to showModalDialog()

---
 src/ProjectManager.js | 6 +++---
 src/brackets.js       | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/ProjectManager.js b/src/ProjectManager.js
index ac0dfe6d951..5d52364be8e 100644
--- a/src/ProjectManager.js
+++ b/src/ProjectManager.js
@@ -34,7 +34,7 @@ ProjectManager.openProject = function() {
                     ProjectManager.loadProject( files[0] );
             },
             function(error) {
-                brackets.showDialog(
+                brackets.showModalDialog(
                       brackets.DIALOG_ID_ERROR
                     , brackets.strings.ERROR_LOADING_PROJECT
                     , brackets.strings.format(brackets.strings.OPEN_DIALOG_ERROR, error.code)
@@ -90,7 +90,7 @@ ProjectManager.loadProject = function(rootPath) {
                 ProjectManager._renderTree(ProjectManager._treeDataProvider);
             },
             function(error) {
-                brackets.showDialog(
+                brackets.showModalDialog(
                       brackets.DIALOG_ID_ERROR
                     , brackets.strings.ERROR_LOADING_PROJECT
                     , brackets.strings.format(brackets.strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, rootPath, error.code)
@@ -128,7 +128,7 @@ ProjectManager._treeDataProvider = function(treeNode, jsTreeCallback) {
             jsTreeCallback(subtreeJSON);
         },
         function(error) {
-            brackets.showDialog(
+            brackets.showModalDialog(
                   brackets.DIALOG_ID_ERROR
                 , brackets.strings.ERROR_LOADING_PROJECT
                 , brackets.strings.format(brackets.strings.READ_DIRECTORY_ENTRIES_ERROR, dirEntry.fullPath, error.code)
diff --git a/src/brackets.js b/src/brackets.js
index 093d9a7d4b0..dc7c47f8c12 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -27,7 +27,7 @@ brackets.DIALOG_ID_SAVE_CLOSE = "save-close-dialog";
  * @return {Deferred} a $.Deferred() that will be resolved with the ID of the clicked button when the dialog
  *     is dismissed.
  */
-brackets.showDialog = function(id, title, message, callback) {
+brackets.showModalDialog = function(id, title, message, callback) {
     var result = $.Deferred();
     var dlg = $("#" + id);
     
@@ -279,7 +279,7 @@ $(document).ready(function() {
     CommandManager.register(Commands.FILE_CLOSE, function() {
         if (_currentFilePath && _isDirty) {
             var result = $.Deferred();
-            brackets.showDialog(
+            brackets.showModalDialog(
                   brackets.DIALOG_ID_SAVE_CLOSE
                 , brackets.strings.SAVE_CLOSE_TITLE
                 , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath)

From b01a80b685f0fbc4d6b07ef7490879e32ffb9819 Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Fri, 16 Dec 2011 14:17:16 -0800
Subject: [PATCH 13/14] Refactored brackets.js to make the boot sequence
 cleaner, and pulled out the file handling code into a separate file.
 Simplified KeyBindingManager. Changed $.Deferred() to new $.Deferred().

---
 src/CommandManager.js      |   8 +-
 src/FileCommandHandlers.js | 248 ++++++++++++++++++++++++++
 src/KeyBindingManager.js   |  86 ++-------
 src/NativeFileSystem.js    |  11 +-
 src/brackets.js            | 356 ++++++++-----------------------------
 src/index.html             |   1 +
 6 files changed, 356 insertions(+), 354 deletions(-)
 create mode 100644 src/FileCommandHandlers.js

diff --git a/src/CommandManager.js b/src/CommandManager.js
index ae34211e04c..cc6e6763648 100644
--- a/src/CommandManager.js
+++ b/src/CommandManager.js
@@ -2,6 +2,10 @@
  * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
  */
 
+/**
+ * Manages global application commands that can be called from menu items, key bindings, or subparts
+ * of the application.
+ */
 var CommandManager = {};
 
 CommandManager._commands = {};
@@ -33,7 +37,7 @@ CommandManager.execute = function(id) {
     if (command) {
         var result = command.apply(null, Array.prototype.slice.call(arguments, 1));
         if (result === undefined) {
-            return $.Deferred().resolve();
+            return (new $.Deferred()).resolve();
         }
         else {
             return result;
@@ -41,6 +45,6 @@ CommandManager.execute = function(id) {
     }
     else {
         console.log("Attempted to call unregistered command: " + id);
-        return $.Deferred().reject();
+        return (new $.Deferred()).reject();
     }
 }
diff --git a/src/FileCommandHandlers.js b/src/FileCommandHandlers.js
new file mode 100644
index 00000000000..e78647397c9
--- /dev/null
+++ b/src/FileCommandHandlers.js
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
+ */
+
+/**
+ * Handlers for commands related to file handling (opening, saving, etc.)
+ */
+var FileCommandHandlers = (function() {
+    // TODO: remove this and use the real exports variable when we switch to modules.
+    var exports = {};
+    
+    var _editor, _title, _currentFilePath, _currentTitlePath,
+        _isDirty = false,
+        _savedUndoPosition = 0;
+    
+    exports.init = function init(editor, title) {
+        _editor = editor;
+        _title = title;
+        
+        _editor.setOption("onChange", function() {
+            updateDirty();
+        });
+    
+        // Register global commands
+        CommandManager.register(Commands.FILE_OPEN, handleFileOpen);
+        CommandManager.register(Commands.FILE_SAVE, handleFileSave);
+        CommandManager.register(Commands.FILE_CLOSE, handleFileClose);
+    };
+
+    function updateDirty() {
+        // If we've undone past the undo position at the last save, and there is no redo stack,
+        // then we can never get back to a non-dirty state.
+        var historySize = _editor.historySize();
+        if (historySize.undo < _savedUndoPosition && historySize.redo == 0) {
+            _savedUndoPosition = -1;
+        }
+        var newIsDirty = (_editor.historySize().undo != _savedUndoPosition);
+        if (_isDirty != newIsDirty) {
+            _isDirty = newIsDirty;
+            updateTitle();
+        }        
+    }
+    
+    function updateTitle() {
+        _title.text(
+            _currentTitlePath 
+                ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) 
+                : "Untitled"
+        );
+    }
+    
+    function handleFileOpen(fullPath) {
+        // TODO: In the future, when we implement multiple open files, we won't close the previous file when opening
+        // a new one. However, for now, since we only support a single open document, I'm pretending as if we're 
+        // closing the existing file first. This is so that I can put the code that checks for an unsaved file and 
+        // prompts the user to save it in the close command, where it belongs. When we implement multiple open files,
+        // we can remove this here.
+        var result; 
+        if (_currentFilePath) {
+            result = new $.Deferred();
+            CommandManager
+                .execute(Commands.FILE_CLOSE)
+                .done(function() {
+                    doOpenWithOptionalPath(fullPath)
+                        .done(function() {
+                            result.resolve();
+                        })
+                        .fail(function() {
+                            result.reject();
+                        });
+                })
+                .fail(function() {
+                    result.reject();
+                });
+        }
+        else {
+            result = doOpenWithOptionalPath(fullPath);
+        }
+        result.always(function() { 
+            _editor.focus(); 
+        });
+        return result;
+    }
+    
+    function doOpenWithOptionalPath(fullPath) {  
+        if (!fullPath) {
+            // Prompt the user with a dialog
+            // TODO: we're relying on this to not be asynchronous--is that safe?
+            NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, 
+                ["htm", "html", "js", "css"], function(files) {
+                    if (files.length > 0) {
+                        return doOpen(files[0]);
+                    }
+                });
+        }
+        else {
+            return doOpen(fullPath);
+        }
+    }
+    
+    function doOpen(fullPath) { 
+        var result = new $.Deferred();         
+        if (fullPath) {
+            var reader = new NativeFileSystem.FileReader();
+
+            // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar
+            // to what's in the standard file API) to get a FileEntry, rather than manually constructing it
+            var fileEntry = new NativeFileSystem.FileEntry(fullPath);
+            
+            // TODO: it's weird to have to construct a FileEntry just to get a File.
+            fileEntry.file(function(file) {                
+                reader.onload = function(event) {
+                    _currentFilePath = _currentTitlePath = fullPath;
+                    
+                    // TODO: have a real controller object for the editor
+                    _editor.setValue(event.target.result);
+                    _editor.clearHistory();
+
+                    // In the main toolbar, show the project-relative path (if the file is inside the current project)
+                    // or the full absolute path (if it's not in the project).
+                    var projectRootPath = ProjectManager.getProjectRoot().fullPath;
+                    if (projectRootPath.length > 0 && projectRootPath.charAt(projectRootPath.length - 1) != "/") {
+                        projectRootPath += "/";
+                    }
+                    if (fullPath.indexOf(projectRootPath) == 0) {
+                        _currentTitlePath = fullPath.slice(projectRootPath.length);
+                        if (_currentTitlePath.charAt(0) == '/') {
+                            _currentTitlePath = _currentTitlePath.slice(1);
+                        }                          
+                    }
+                    
+                    // Make sure we can't undo back to the previous content.
+                    _editor.clearHistory();
+                    
+                    // This should be 0, but just to be safe...
+                    _savedUndoPosition = _editor.historySize().undo;
+                    updateDirty();
+
+                    _editor.focus();
+                    result.resolve();
+                };
+                
+                reader.onerror = function(event) {
+                    // TODO: display meaningful error
+                    result.reject();
+                }
+                
+                reader.readAsText(file, "utf8");
+            },
+            function (error) {
+                // TODO: display meaningful error
+                result.reject();
+            });
+        }
+        return result;
+    }
+    
+    function handleFileSave() {
+        var result = new $.Deferred();
+        if (_currentFilePath && _isDirty) {
+            // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar
+            // to what's in the standard file API) to get a FileEntry, rather than manually constructing it
+            var fileEntry = new NativeFileSystem.FileEntry(_currentFilePath);
+        
+            fileEntry.createWriter(
+                function(writer) {
+                    writer.onwrite = function() {
+                        _savedUndoPosition = _editor.historySize().undo;
+                        updateDirty();
+                        result.resolve();
+                    }
+                    writer.onerror = function() {
+                        result.reject();
+                    }
+                    writer.write(_editor.getValue());
+                },
+                function(error) {
+                    // TODO: display meaningful error
+                    result.reject();
+                }
+            );
+        }
+        else {
+            result.resolve();
+        }
+        result.always(function() { 
+            _editor.focus(); 
+        });
+        return result;
+    }
+    
+    function handleFileClose() {
+        if (_currentFilePath && _isDirty) {
+            var result = new $.Deferred();
+            brackets.showModalDialog(
+                  brackets.DIALOG_ID_SAVE_CLOSE
+                , brackets.strings.SAVE_CLOSE_TITLE
+                , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath)
+            ).done(function(id) {
+                if (id === brackets.DIALOG_BTN_CANCEL) {
+                    result.reject();
+                }
+                else {
+                    if (id === brackets.DIALOG_BTN_OK) {
+                        CommandManager
+                            .execute(Commands.FILE_SAVE)
+                            .done(function() {
+                                doClose();
+                                result.resolve();
+                            })
+                            .fail(function() {
+                                result.reject();
+                            });
+                    }   
+                    else {
+                        // This is the "Don't Save" case--we can just go ahead and close the file.
+                        doClose();
+                        result.resolve();
+                    }
+                }
+            });
+            result.always(function() { 
+                _editor.focus(); 
+            });
+            return result;
+        }
+        else {
+            doClose();
+            _editor.focus();
+        }
+    }
+    
+    function doClose() {
+        // TODO: When we implement multiple files being open, this will probably change to just
+        // dispose of the editor for the current file (and will later change again if we choose to
+        // limit the number of open editors).
+        _editor.setValue("");
+        _editor.clearHistory();
+        _currentFilePath = _currentTitlePath = null;
+        _savedUndoPosition = 0;
+        _isDirty = false;
+        updateTitle();
+        _editor.focus();
+    }
+    
+    return exports;
+})();
+
diff --git a/src/KeyBindingManager.js b/src/KeyBindingManager.js
index a5333b4ac86..fec89b639c5 100644
--- a/src/KeyBindingManager.js
+++ b/src/KeyBindingManager.js
@@ -2,63 +2,22 @@
  * Copyright 2011 Adobe Systems Incorporated. All Rights Reserved.
  */
 
+/**
+ * Manages the mapping of keyboard inputs to commands.
+ */
 var KeyBindingManager = {
     /**
-     * The map of all registered keymaps.
-     */
-    _keymaps: {},
-
-    /**
-     * The map of currently active keymaps. As new keymaps come into effect, they are pushed onto the end of
-     * the array. Later keymaps take precedence over earlier ones.
-     */
-    _activeKeymaps: {},
-
-    /**
-     * The maximum possible keymap level.
-     */
-    _maxLevel: -1,
-
-    /**
-     * Registers the specified keymap based on its id.
-     *
-     * TODO: do Closure annotations support user-defined types?
-     * @param {KeyMap} keymap The keymap to register.
+     * The currently installed keymap.
      */
-    registerKeymap: function(keymap) {
-        KeyBindingManager._keymaps[keymap.id] = keymap;
-        if (keymap.level > KeyBindingManager._maxLevel) {
-            KeyBindingManager._maxLevel = keymap.level;
-        }
-    },
+    _keymap: null,
 
     /**
-     * Activate the specified keymap. This installs it at the appropriate level, replacing any other keymap
-     * at that level.
+     * Install the specified keymap as the current keymap, overwriting the existing keymap.
      *
-     * TODO: does the Closure annotation syntax support user-created types?
-     * @param {string} keymap The ID of the registered keymap to install.
+     * @param {KeyMap} keymap The keymap to install.
      */
-    activateKeymap: function(id) {
-        var keymap = KeyBindingManager._keymaps[id];
-        if (!keymap) {
-            throw new Error("Keymap " + id + " not registered");
-        }
-    
-        KeyBindingManager._activeKeymaps[keymap.level] = keymap;
-    },
-
-    /**
-     * Deactivate the keymap with the given ID. If it's not already active, does nothing.
-     *
-     * @param {string} id The ID of the keymap to deactivate.
-     */
-    deactivateKeymap: function(id) {
-        for (var i = 0; i < KeyBindingManager._maxLevel; i++) {
-            if (KeyBindingManager._activeKeymaps[i] && KeyBindingManager._activeKeymaps[i].id === id) {
-                delete KeyBindingManager._activeKeymaps[i];
-            }
-        }
+    installKeymap: function(keymap) {
+        this._keymap = keymap;
     },
 
     /**
@@ -68,12 +27,9 @@ var KeyBindingManager = {
      * @return {boolean} true if the key was processed, false otherwise
      */
     handleKey: function(key) {
-        for (var i = KeyBindingManager._maxLevel; i >= 0; i--) {
-            var keymap = KeyBindingManager._activeKeymaps[i];
-            if (keymap && keymap.map[key]) {
-                CommandManager.execute(keymap.map[key]);
-                return true;
-            }
+        if (this._keymap && this._keymap.map[key]) {
+            CommandManager.execute(this._keymap.map[key]);
+            return true;
         }        
         return false;
     }
@@ -81,11 +37,8 @@ var KeyBindingManager = {
 
 /** class Keymap
  *
- * A keymap specifies how keys are mapped to commands. The KeyBindingManager allows for a hierarchy of
- * keymaps, specified by a numeric level. It does not dictate the semantics of these levels, but in
- * practice, level 0 is a global keymap, level 1 is a file-type-specific keymap, and deeper levels can
- * be used for more specific contexts. When a new keymap is activated, it replaces any other keymap at
- * its same level.
+ * A keymap specifies how keys are mapped to commands. This currently just holds the map, but in future
+ * it will likely be extended to include other metadata about the keymap.
  *
  * Keys are described by strings of the form "[modifier-modifier-...-]key", where modifier is one of
  * Ctrl, Alt, or Shift. If multiple modifiers are specified, they must be specified in that order
@@ -97,16 +50,11 @@ var KeyBindingManager = {
  *       down, you must specifically include Shift.
  *
  * @constructor
- * @param {string} id A unique ID for this keymap.
- * @param {number} level The level of this keymap. Must be an integer >= 0.
  * @param {map} map An object mapping key-description strings to command IDs.
  */
-var KeyMap = function(id, level, map) {
-    if (id === undefined || level === undefined || map === undefined) {
+var KeyMap = function(map) {
+    if (map === undefined) {
         throw new Error("All parameters to the KeyMap constructor must be specified");
-    }
-    
-    this.id = id;
-    this.level = level;
+    }    
     this.map = map;
 };
diff --git a/src/NativeFileSystem.js b/src/NativeFileSystem.js
index 0ec1411166e..1ea2f33b962 100644
--- a/src/NativeFileSystem.js
+++ b/src/NativeFileSystem.js
@@ -186,12 +186,13 @@ NativeFileSystem.FileEntry.prototype.createWriter = function( successCallback, e
         var self = this;
 
         brackets.fs.writeFile( fileEntry.fullPath, data, "utf8", function( err ) {
-            if ( self.onerror ) {
-                self.onerror ( NativeFileSystem._nativeToFileError( err ) );
-
-                // TODO (jasonsj): partial write, update length and position
+            if ( err ) {
+                if ( self.onerror ) {
+                    self.onerror ( NativeFileSystem._nativeToFileError( err ) );
+                }
             }
             else {
+                // TODO (jasonsj): partial write, update length and position
                 // successful completetion of a write
                 self.position += data.size;
             }
@@ -204,7 +205,7 @@ NativeFileSystem.FileEntry.prototype.createWriter = function( successCallback, e
                 self.onwrite();
             }
 
-            if ( this.onwriteend ) {
+            if ( self.onwriteend ) {
                 // TODO (jasonsj): progressevent
                 self.onwriteend();
             }
diff --git a/src/brackets.js b/src/brackets.js
index dc7c47f8c12..254e73ce207 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -53,290 +53,90 @@ brackets.showModalDialog = function(id, title, message, callback) {
 $(document).ready(function() {
 
     var editor = CodeMirror($('#editor').get(0));
-
-    // Load a default project into the tree
-    if (brackets.inBrowser) {
-        // In browser: dummy folder tree (hardcoded in ProjectManager)
-        ProjectManager.loadProject("DummyProject");
-    } else {
-        // In app shell: load Brackets itself
-        var loadedPath = window.location.pathname;
-        var bracketsSrc = loadedPath.substr(0, loadedPath.lastIndexOf("/"));
-        ProjectManager.loadProject(bracketsSrc);
-    }
-    
-    // Open project button
-    $("#btn-open-project").click(function() {
-        ProjectManager.openProject();
-    });
-    
-    // Implements the File menu items
-    $("#menu-file-open").click(function() {
-        CommandManager.execute(Commands.FILE_OPEN);
-    });
-    $("#menu-file-close").click(function() {
-        CommandManager.execute(Commands.FILE_CLOSE);
-    });
-    $("#menu-file-save").click(function() {
-        CommandManager.execute(Commands.FILE_SAVE);
-    });
-    
-    // Implements the 'Run Tests' menu to bring up the Jasmine unit test window
-    var testWindow = null;
-    $("#menu-runtests").click(function(){
-        if (!(testWindow === null)) {
-            try {
-                testWindow.location.reload();
-            } catch(e) {
-                testWindow = null;  // the window was probably closed
-            } 
-        }
-        
-        if (testWindow === null) {
-            testWindow = window.open("../test/SpecRunner.html");
-            testWindow.location.reload(); // if it was opened before, we need to reload because it will be cached
-        }
-    });
     
-    // Application state
-    // TODO: factor this stuff out into a real app controller
-    var _currentFilePath = null;
-    var _currentTitlePath = null;
-    var _isDirty = false;
-    var _savedUndoPosition = 0;
-    
-    editor.setOption("onChange", function() {
-        updateDirty();
-    });
-
-    // Utility functions
-    function updateDirty() {
-        // If we've undone past the undo position at the last save, and there is no redo stack,
-        // then we can never get back to a non-dirty state.
-        var historySize = editor.historySize();
-        if (historySize.undo < _savedUndoPosition && historySize.redo == 0) {
-            _savedUndoPosition = -1;
-        }
-        var newIsDirty = (editor.historySize().undo != _savedUndoPosition);
-        if (_isDirty != newIsDirty) {
-            _isDirty = newIsDirty;
-            updateTitle();
-        }        
-    }
-    
-    function updateTitle() {
-        $("#main-toolbar .title").text(_currentTitlePath ? (_currentTitlePath + (_isDirty ? " \u2022" : "")) : "Untitled");
-    }
-    
-    function doOpenWithOptionalPath(fullPath) {     
-        if (!fullPath) {
-            // Prompt the user with a dialog
-            // TODO: we're relying on this to not be asynchronous--is that safe?
-            NativeFileSystem.showOpenDialog(false, false, "Open File", ProjectManager.getProjectRoot().fullPath, 
-                ["htm", "html", "js", "css"], function(files) {
-                    if (files.length > 0) {
-                        return doOpen(files[0]);
-                    }
-                });
-        }
-        else {
-            return doOpen(fullPath);
-        }
+    initProject();
+    initMenus();
+    initCommandHandlers();
+    initKeyBindings();
+    
+    function initProject() {    
+        // Load a default project into the tree
+        if (brackets.inBrowser) {
+            // In browser: dummy folder tree (hardcoded in ProjectManager)
+            ProjectManager.loadProject("DummyProject");
+        } else {
+            // In app shell: load Brackets itself
+            var loadedPath = window.location.pathname;
+            var bracketsSrc = loadedPath.substr(0, loadedPath.lastIndexOf("/"));
+            ProjectManager.loadProject(bracketsSrc);
+        }
+    
+        // Open project button
+        $("#btn-open-project").click(function() {
+            ProjectManager.openProject();
+        });
     }
+ 
+    function initMenus() {
+        // Implements the File menu items
+        $("#menu-file-open").click(function() {
+            CommandManager.execute(Commands.FILE_OPEN);
+        });
+        $("#menu-file-close").click(function() {
+            CommandManager.execute(Commands.FILE_CLOSE);
+        });
+        $("#menu-file-save").click(function() {
+            CommandManager.execute(Commands.FILE_SAVE);
+        });
     
-    function doOpen(fullPath) { 
-        var result = $.Deferred();         
-        if (fullPath) {
-            var reader = new NativeFileSystem.FileReader();
-
-            // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar
-            // to what's in the standard file API) to get a FileEntry, rather than manually constructing it
-            var fileEntry = new NativeFileSystem.FileEntry(fullPath);
-            
-            // TODO: it's weird to have to construct a FileEntry just to get a File.
-            fileEntry.file(function(file) {                
-                reader.onload = function(event) {
-                    _currentFilePath = _currentTitlePath = fullPath;
-                    
-                    // TODO: have a real controller object for the editor
-                    editor.setValue(event.target.result);
-                    editor.clearHistory();
-
-                    // In the main toolbar, show the project-relative path (if the file is inside the current project)
-                    // or the full absolute path (if it's not in the project).
-                    var projectRootPath = ProjectManager.getProjectRoot().fullPath;
-                    if (projectRootPath.length > 0 && projectRootPath.charAt(projectRootPath.length - 1) != "/") {
-                        projectRootPath += "/";
-                    }
-                    if (fullPath.indexOf(projectRootPath) == 0) {
-                        _currentTitlePath = fullPath.slice(projectRootPath.length);
-                        if (_currentTitlePath.charAt(0) == '/') {
-                            _currentTitlePath = _currentTitlePath.slice(1);
-                        }                          
-                    }
-                    
-                    // Make sure we can't undo back to the previous content.
-                    editor.clearHistory();
-                    
-                    // This should be 0, but just to be safe...
-                    _savedUndoPosition = editor.historySize().undo;
-                    updateDirty();
-
-                    editor.focus();
-                    result.resolve();
-                };
-                
-                reader.onerror = function(event) {
-                    // TODO: display meaningful error
-                    result.reject();
-                }
-                
-                reader.readAsText(file, "utf8");
-            },
-            function (error) {
-                // TODO: display meaningful error
-                result.reject();
-            });
-        }
-        return result;
+        // Implements the 'Run Tests' menu to bring up the Jasmine unit test window
+        var testWindow = null;
+        $("#menu-runtests").click(function(){
+            if (!(testWindow === null)) {
+                try {
+                    testWindow.location.reload();
+                } catch(e) {
+                    testWindow = null;  // the window was probably closed
+                } 
+            }
+        
+            if (testWindow === null) {
+                testWindow = window.open("../test/SpecRunner.html");
+                testWindow.location.reload(); // if it was opened before, we need to reload because it will be cached
+            }
+        });
     }
     
-    function doClose() {
-        // TODO: When we implement multiple files being open, this will probably change to just
-        // dispose of the editor for the current file (and will later change again if we choose to
-        // limit the number of open editors).
-        editor.setValue("");
-        editor.clearHistory();
-        _currentFilePath = _currentTitlePath = null;
-        _savedUndoPosition = 0;
-        _isDirty = false;
-        updateTitle();
-        editor.focus();
+    function initCommandHandlers() {    
+        FileCommandHandlers.init(editor, $("#main-toolbar .title"));
     }
     
-    // Register global commands
-    CommandManager.register(Commands.FILE_OPEN, function(fullPath) {
-        // TODO: In the future, when we implement multiple open files, we won't close the previous file when opening
-        // a new one. However, for now, since we only support a single open document, I'm pretending as if we're 
-        // closing the existing file first. This is so that I can put the code that checks for an unsaved file and 
-        // prompts the user to save it in the close command, where it belongs. When we implement multiple open files,
-        // we can remove this here.
-        if (_currentFilePath) {
-            var result = $.Deferred();
-            CommandManager
-                .execute(Commands.FILE_CLOSE)
-                .done(function() {
-                    doOpenWithOptionalPath(fullPath)
-                        .done(function() {
-                            result.resolve();
-                        })
-                        .fail(function() {
-                            result.reject();
-                        });
-                })
-                .fail(function() {
-                    result.reject();
-                });
-            return result;
-        }
-        else {
-            return doOpenWithOptionalPath(fullPath);
-        }
-    });
-
-    CommandManager.register(Commands.FILE_SAVE, function() {
-        var result = $.Deferred();
-        if (_currentFilePath && _isDirty) {
-            // TODO: we should implement something like NativeFileSystem.resolveNativeFileSystemURL() (similar
-            // to what's in the standard file API) to get a FileEntry, rather than manually constructing it
-            var fileEntry = new NativeFileSystem.FileEntry(_currentFilePath);
-            
-            fileEntry.createWriter(function(writer) {
-                writer.onwrite = function() {
-                    _savedUndoPosition = editor.historySize().undo;
-                    updateDirty();
-                    result.resolve();
-                }
-                writer.onerror = function() {
-                    result.reject();
-                }
-                writer.write(editor.getValue());
-            },
-            function(error) {
-                // TODO: display meaningful error
-                result.reject();
-            });
-        }
-        else {
-            result.resolve();
-        }
-        result.always(function() { 
-            editor.focus(); 
+    function initKeyBindings() {
+        // Register keymaps and install the keyboard handler
+        // TODO: show keyboard equivalents in the menus
+        var _globalKeymap = new KeyMap(
+            { "Ctrl-O": Commands.FILE_OPEN
+            , "Ctrl-S": Commands.FILE_SAVE
+            , "Ctrl-W": Commands.FILE_CLOSE
+            }
+        );
+        KeyBindingManager.installKeymap(_globalKeymap);
+    
+        $(document.body).keydown(function(event) {
+            var keyDescriptor = [];
+            if (event.metaKey || event.ctrlKey) {
+                keyDescriptor.push("Ctrl");
+            }
+            if (event.altKey) {
+                keyDescriptor.push("Alt");
+            }
+            if (event.shiftKey) {
+                keyDescriptor.push("Shift");
+            }
+            keyDescriptor.push(String.fromCharCode(event.keyCode).toUpperCase());
+            if (KeyBindingManager.handleKey(keyDescriptor.join("-"))) {
+                event.preventDefault();
+            }
         });
-        return result;
-    });
-    
-    CommandManager.register(Commands.FILE_CLOSE, function() {
-        if (_currentFilePath && _isDirty) {
-            var result = $.Deferred();
-            brackets.showModalDialog(
-                  brackets.DIALOG_ID_SAVE_CLOSE
-                , brackets.strings.SAVE_CLOSE_TITLE
-                , brackets.strings.format(brackets.strings.SAVE_CLOSE_MESSAGE, _currentTitlePath)
-            ).done(function(id) {
-                if (id === brackets.DIALOG_BTN_CANCEL) {
-                    result.reject();
-                }
-                else {
-                    if (id === brackets.DIALOG_BTN_OK) {
-                        CommandManager
-                            .execute(Commands.FILE_SAVE)
-                            .done(function() {
-                                doClose();
-                                result.resolve();
-                            })
-                            .fail(function() {
-                                result.reject();
-                            });
-                    }   
-                    else {
-                        // This is the "Don't Save" case--we can just go ahead and close the file.
-                        doClose();
-                        result.resolve();
-                    }
-                }
-            });
-            return result;
-        }
-        else {
-            doClose();
-        }
-    });
-    
-    // Register keymaps and install the keyboard handler
-    // TODO: show keyboard equivalents in the menus
-    var KEYMAP_GLOBAL = "global";
-    KeyBindingManager.registerKeymap(new KeyMap(KEYMAP_GLOBAL, 0, 
-        { "Ctrl-O": Commands.FILE_OPEN
-        , "Ctrl-S": Commands.FILE_SAVE
-        , "Ctrl-W": Commands.FILE_CLOSE
-        }));
-    KeyBindingManager.activateKeymap(KEYMAP_GLOBAL);
-    
-    $(document.body).keydown(function(event) {
-        var keyDescriptor = [];
-        if (event.metaKey || event.ctrlKey) {
-            keyDescriptor.push("Ctrl");
-        }
-        if (event.altKey) {
-            keyDescriptor.push("Alt");
-        }
-        if (event.shiftKey) {
-            keyDescriptor.push("Shift");
-        }
-        keyDescriptor.push(String.fromCharCode(event.keyCode).toUpperCase());
-        if (KeyBindingManager.handleKey(keyDescriptor.join("-"))) {
-            event.preventDefault();
-        }
-    });
+    }
 });
diff --git a/src/index.html b/src/index.html
index 560764cb0e0..48e786a8652 100644
--- a/src/index.html
+++ b/src/index.html
@@ -23,6 +23,7 @@
 	<script src="NativeFileSystem.js"></script>	
 	<script src="CommandManager.js"></script>
 	<script src="Commands.js"></script>
+	<script src="FileCommandHandlers.js"></script>
 	<script src="KeyBindingManager.js"></script>
 	<script src="ProjectManager.js"></script>
 	<script src="brackets.js"></script>

From dbf5170ffea53a168a304be21d124e45250646cc Mon Sep 17 00:00:00 2001
From: njadbe <njaramil@adobe.com>
Date: Fri, 16 Dec 2011 16:39:34 -0800
Subject: [PATCH 14/14] Code review cleanup

---
 src/FileCommandHandlers.js | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/src/FileCommandHandlers.js b/src/FileCommandHandlers.js
index e78647397c9..2a0c9978380 100644
--- a/src/FileCommandHandlers.js
+++ b/src/FileCommandHandlers.js
@@ -112,10 +112,6 @@ var FileCommandHandlers = (function() {
                 reader.onload = function(event) {
                     _currentFilePath = _currentTitlePath = fullPath;
                     
-                    // TODO: have a real controller object for the editor
-                    _editor.setValue(event.target.result);
-                    _editor.clearHistory();
-
                     // In the main toolbar, show the project-relative path (if the file is inside the current project)
                     // or the full absolute path (if it's not in the project).
                     var projectRootPath = ProjectManager.getProjectRoot().fullPath;
@@ -129,6 +125,9 @@ var FileCommandHandlers = (function() {
                         }                          
                     }
                     
+                    // TODO: have a real controller object for the editor
+                    _editor.setValue(event.target.result);
+ 
                     // Make sure we can't undo back to the previous content.
                     _editor.clearHistory();
                     
@@ -136,7 +135,6 @@ var FileCommandHandlers = (function() {
                     _savedUndoPosition = _editor.historySize().undo;
                     updateDirty();
 
-                    _editor.focus();
                     result.resolve();
                 };
                 
@@ -190,8 +188,8 @@ var FileCommandHandlers = (function() {
     }
     
     function handleFileClose() {
+        var result = new $.Deferred();
         if (_currentFilePath && _isDirty) {
-            var result = new $.Deferred();
             brackets.showModalDialog(
                   brackets.DIALOG_ID_SAVE_CLOSE
                 , brackets.strings.SAVE_CLOSE_TITLE
@@ -222,12 +220,13 @@ var FileCommandHandlers = (function() {
             result.always(function() { 
                 _editor.focus(); 
             });
-            return result;
         }
         else {
             doClose();
             _editor.focus();
+            result.resolve();
         }
+        return result;
     }
     
     function doClose() {