Bug 1103346 - Add renaming support to webIDE. r=jryans
authorAbdelrhman Ahmed <a.ahmed1026@gmail.com>
Fri, 02 Jan 2015 13:59:00 -0500
changeset 248215 ff084cf799446340125a44ff0008d34d6b0e0568
parent 248214 7b4b861a642e79c1731945d75181252b592e6b0e
child 248216 11b650affda02eaed069e4615571154a210961aa
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1103346
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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…