Bug 1103346 - Add renaming support to webIDE. r=jryans
authorAbdelrhman Ahmed <a.ahmed1026@gmail.com>
Fri, 02 Jan 2015 13:59:00 -0500
changeset 222243 ff084cf799446340125a44ff0008d34d6b0e0568
parent 222242 7b4b861a642e79c1731945d75181252b592e6b0e
child 222244 11b650affda02eaed069e4615571154a210961aa
push id10671
push userryanvm@gmail.com
push dateTue, 06 Jan 2015 17:10:43 +0000
treeherderfx-team@7a8b80930bd1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1103346
milestone37.0a1
Bug 1103346 - Add renaming support to webIDE. r=jryans
browser/devtools/projecteditor/lib/plugins/rename/rename.js
browser/devtools/projecteditor/lib/projecteditor.js
browser/devtools/projecteditor/lib/stores/resource.js
browser/devtools/projecteditor/lib/tree.js
browser/devtools/projecteditor/moz.build
browser/devtools/projecteditor/test/browser.ini
browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js
browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/rename/rename.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const { getLocalizedString } = require("projecteditor/helpers/l10n");
+
+var RenamePlugin = Class({
+  extends: Plugin,
+
+  init: function(host) {
+    this.host.addCommand(this, {
+      id: "cmd-rename"
+    });
+    this.contextMenuItem = this.host.createMenuItem({
+      parent: this.host.contextMenuPopup,
+      label: getLocalizedString("projecteditor.renameLabel"),
+      command: "cmd-rename"
+    });
+  },
+
+  onCommand: function(cmd) {
+    if (cmd === "cmd-rename") {
+      let tree = this.host.projectTree;
+      let resource = tree.getSelectedResource();
+      let parent = resource.parent;
+      let oldName = resource.basename;
+
+      tree.promptEdit(oldName, resource).then(name => {
+        if (name === oldName) {
+          return resource;
+        }
+        if (resource.hasChild(parent, name)) {
+          let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
+          let template = matches[1] + "{1}" + matches[3] + matches[4];
+          name = this.suggestName(resource, template, parseInt(matches[2]) || 2);
+        }
+        return parent.rename(oldName,name);
+      }).then(resource => {
+        this.host.project.refresh();
+        tree.selectResource(resource);
+        if (!resource.isDir) {
+          this.host.currentEditor.focus();
+        }
+      }).then(null, console.error);
+    }
+  },
+
+  suggestName: function(resource, template, start=1) {
+    let i = start;
+    let name;
+    let parent = resource.parent;
+    do {
+      name = template.replace("\{1\}", i === 1 ? "" : i);
+      i++;
+    } while (resource.hasChild(parent, name));
+
+    return name;
+  }
+});
+
+exports.RenamePlugin = RenamePlugin;
+registerPlugin(RenamePlugin);
--- a/browser/devtools/projecteditor/lib/projecteditor.js
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -22,16 +22,17 @@ const { Services } = Cu.import("resource
 const ITCHPAD_URL = "chrome://browser/content/devtools/projecteditor.xul";
 const { confirm } = require("projecteditor/helpers/prompts");
 const { getLocalizedString } = require("projecteditor/helpers/l10n");
 
 // Enabled Plugins
 require("projecteditor/plugins/dirty/dirty");
 require("projecteditor/plugins/delete/delete");
 require("projecteditor/plugins/new/new");
+require("projecteditor/plugins/rename/rename");
 require("projecteditor/plugins/save/save");
 require("projecteditor/plugins/image-view/plugin");
 require("projecteditor/plugins/app-manager/plugin");
 require("projecteditor/plugins/status-bar/plugin");
 
 // Uncomment to enable logging.
 // require("projecteditor/plugins/logging/logging");
 
--- a/browser/devtools/projecteditor/lib/stores/resource.js
+++ b/browser/devtools/projecteditor/lib/stores/resource.js
@@ -122,16 +122,31 @@ var Resource = Class({
     resource.parent = this;
     this.children.add(resource);
     this.store.notifyAdd(resource);
     emit(this, "children-changed", this);
     return resource;
   },
 
   /**
+   * Checks a resource has child with specific name.
+   *
+   * @param Resource resource
+   * @param string name
+   */
+  hasChild: function(resource, name) {
+    for (let child of resource.children) {
+      if (child.basename === name) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  /**
    * Remove a resource to children set and notify of the change.
    *
    * @param Resource resource
    */
   removeChild: function(resource) {
     resource.parent = null;
     this.children.remove(resource);
     this.store.notifyRemove(resource);
@@ -299,16 +314,37 @@ var FileResource = Class({
       if (!resource) {
         throw new Error("Error creating " + newPath);
       }
       return resource;
     });
   },
 
   /**
+   * Rename the file from the filesystem
+   *
+   * @returns Promise
+   *          Resolves with the renamed FileResource.
+   */
+  rename: function(oldName, newName) {
+    let oldPath = OS.Path.join(this.path, oldName);
+    let newPath = OS.Path.join(this.path, newName);
+
+    return OS.File.move(oldPath, newPath).then(() => {
+      return this.store.refresh();
+    }).then(() => {
+      let resource = this.store.resources.get(newPath);
+      if (!resource) {
+        throw new Error("Error creating " + newPath);
+      }
+      return resource;
+    });
+  },
+
+  /**
    * Write a string to this file.
    *
    * @param string content
    * @returns Promise
    *          Resolves once it has been written to disk.
    *          Rejected if there is an error
    */
   save: function(content) {
--- a/browser/devtools/projecteditor/lib/tree.js
+++ b/browser/devtools/projecteditor/lib/tree.js
@@ -272,16 +272,49 @@ var TreeView = Class({
         item.parentNode.removeChild(item);
       },
     });
 
     return deferred.promise;
   },
 
   /**
+   * Prompt the user to rename file in the tree.
+   *
+   * @param string initial
+   *               The suggested starting file name
+   * @param resource
+   *
+   * @returns Promise
+   *          Resolves once the prompt has been successful,
+   *          Rejected if it is cancelled
+   */
+  promptEdit: function(initial, resource) {
+    let deferred = promise.defer();
+    let placeholder = this._containers.get(resource).elt;
+
+    new InplaceEditor({
+      element: placeholder,
+      initial: initial,
+      start: editor => {
+        editor.input.select();
+      },
+      done: function(val, commit) {
+        if (commit) {
+          deferred.resolve(val);
+        } else {
+          deferred.reject(val);
+        }
+      },
+    });
+
+    return deferred.promise;
+  },
+
+  /**
    * Add a new Store into the TreeView
    *
    * @param Store model
    */
   addModel: function(model) {
     if (this.models.has(model)) {
       // Requesting to add a model that already exists
       return;
--- a/browser/devtools/projecteditor/moz.build
+++ b/browser/devtools/projecteditor/moz.build
@@ -47,16 +47,20 @@ EXTRA_JS_MODULES.devtools.projecteditor.
 EXTRA_JS_MODULES.devtools.projecteditor.plugins.logging += [
     'lib/plugins/logging/logging.js',
 ]
 
 EXTRA_JS_MODULES.devtools.projecteditor.plugins.new += [
     'lib/plugins/new/new.js',
 ]
 
+EXTRA_JS_MODULES.devtools.projecteditor.plugins.rename += [
+    'lib/plugins/rename/rename.js',
+]
+
 EXTRA_JS_MODULES.devtools.projecteditor.plugins.save += [
     'lib/plugins/save/save.js',
 ]
 
 EXTRA_JS_MODULES.devtools.projecteditor.plugins['status-bar'] += [
     'lib/plugins/status-bar/plugin.js',
 ]
 
--- a/browser/devtools/projecteditor/test/browser.ini
+++ b/browser/devtools/projecteditor/test/browser.ini
@@ -7,16 +7,17 @@ support-files =
   helper_edits.js
 
 [browser_projecteditor_app_options.js]
 skip-if = buildapp == 'mulet'
 [browser_projecteditor_confirm_unsaved.js]
 [browser_projecteditor_contextmenu_01.js]
 [browser_projecteditor_contextmenu_02.js]
 [browser_projecteditor_delete_file.js]
+[browser_projecteditor_rename_file.js]
 skip-if = buildapp == 'mulet'
 [browser_projecteditor_editing_01.js]
 skip-if = buildapp == 'mulet'
 [browser_projecteditor_editors_image.js]
 [browser_projecteditor_external_change.js]
 [browser_projecteditor_immediate_destroy.js]
 [browser_projecteditor_init.js]
 [browser_projecteditor_menubar_01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js
@@ -0,0 +1,59 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test file rename functionality
+
+add_task(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  ok(true, "ProjectEditor has loaded");
+
+  let root = [...projecteditor.project.allStores()][0].root;
+  is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
+  for (let child of root.children) {
+    yield renameWithContextMenu(projecteditor, projecteditor.projectTree.getViewContainer(child));
+  }
+});
+
+function openContextMenuOn(node) {
+  EventUtils.synthesizeMouseAtCenter(
+    node,
+    {button: 2, type: "contextmenu"},
+    node.ownerDocument.defaultView
+  );
+}
+
+function renameWithContextMenu(projecteditor, container) {
+  let defer = promise.defer();
+  let popup = projecteditor.contextMenuPopup;
+  let resource = container.resource;
+  info ("Going to attempt renaming for: " + resource.path);
+
+  onPopupShow(popup).then(function () {
+    let renameCommand = popup.querySelector("[command=cmd-rename]");
+    ok (renameCommand, "Rename command exists in popup");
+    is (renameCommand.getAttribute("hidden"), "", "Rename command is visible");
+    is (renameCommand.getAttribute("disabled"), "", "Rename command is enabled");
+
+    projecteditor.project.on("refresh-complete", function refreshComplete() {
+      projecteditor.project.off("refresh-complete", refreshComplete);
+      OS.File.stat(resource.path + ".renamed").then(() => {
+        ok (true, "File is renamed");
+        defer.resolve();
+      }, (ex) => {
+        ok (false, "Failed to rename file");
+        defer.resolve();
+      });
+    });
+
+    renameCommand.click();
+    popup.hidePopup();
+    EventUtils.sendString(resource.basename + ".renamed", projecteditor.window);
+    EventUtils.synthesizeKey("VK_RETURN", {}, projecteditor.window);
+  });
+
+  openContextMenuOn(container.label);
+  return defer.promise;
+}
--- a/browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
@@ -41,16 +41,21 @@ projecteditor.deleteFolderPromptMessage=
 # to make sure if a file should be removed.
 projecteditor.deleteFilePromptMessage=Are you sure you want to delete this file?
 
 # LOCALIZATION NOTE (projecteditor.newLabel):
 # This string is displayed as a menu item for adding a new file to
 # the directory.
 projecteditor.newLabel=New…
 
+# LOCALIZATION NOTE (projecteditor.renameLabel):
+# This string is displayed as a menu item for renaming a file in
+# the directory.
+projecteditor.renameLabel=Rename
+
 # LOCALIZATION NOTE (projecteditor.saveLabel):
 # This string is displayed as a menu item for saving the current file.
 projecteditor.saveLabel=Save
 
 # LOCALIZATION NOTE (projecteditor.saveAsLabel):
 # This string is displayed as a menu item for saving the current file
 # with a new name.
 projecteditor.saveAsLabel=Save As…