2013-11-03 00:51:28 +00:00
|
|
|
define([
|
2014-01-21 06:10:06 +00:00
|
|
|
"storage/settingsProvider",
|
2013-12-04 17:24:04 +00:00
|
|
|
"command",
|
|
|
|
"sessions",
|
|
|
|
"storage/file",
|
|
|
|
"util/manos",
|
|
|
|
"ui/dialog",
|
|
|
|
"ui/contextMenus",
|
|
|
|
"editor",
|
2014-02-26 16:57:15 +00:00
|
|
|
"util/template!templates/projectDir.html,templates/projectFile.html",
|
2013-12-04 17:24:04 +00:00
|
|
|
"util/dom2"
|
2014-02-26 16:57:15 +00:00
|
|
|
], function(Settings, command, sessions, File, M, dialog, context, editor, inflate) {
|
2013-11-04 01:15:34 +00:00
|
|
|
|
|
|
|
/*
|
2013-12-10 22:46:28 +00:00
|
|
|
It's tempting to store projects in local storage, similar to the way that we
|
|
|
|
retain files for tabs, but this would be a mistake. Reading from storage is a
|
|
|
|
pain, because it wants to store a single level deep, and we'll want to alter
|
|
|
|
parts of the setup individually.
|
2013-11-04 01:15:34 +00:00
|
|
|
|
2013-12-10 22:46:28 +00:00
|
|
|
Instead, we'll retain a single file handle to the project file, which (as
|
|
|
|
JSON) will store the IDs of individual directories, the project-specific
|
|
|
|
settings, and (hopefully, one day) build systems. This also gets us around
|
|
|
|
the issues of restored directory order and constantly updating the retained
|
|
|
|
file list--we'll just update it when the project file is saved.
|
2013-11-04 01:15:34 +00:00
|
|
|
*/
|
2013-11-03 00:51:28 +00:00
|
|
|
|
|
|
|
var guidCounter = 0;
|
|
|
|
|
2013-11-04 16:41:22 +00:00
|
|
|
//FSNodes are used to track filesystem state inside projects
|
2014-02-26 16:57:15 +00:00
|
|
|
//We don't use the typical File object, because we're not really reading them
|
|
|
|
//Nodes form a tree starting at the root directory
|
2013-11-03 00:51:28 +00:00
|
|
|
var FSNode = function(entry) {
|
|
|
|
this.children = [];
|
|
|
|
this.id = guidCounter++;
|
|
|
|
if (entry) this.setEntry(entry);
|
|
|
|
};
|
|
|
|
FSNode.prototype = {
|
|
|
|
isDirectory: false,
|
|
|
|
entry: null,
|
|
|
|
tab: null,
|
|
|
|
id: null,
|
|
|
|
label: null,
|
|
|
|
setEntry: function(entry, c) {
|
|
|
|
this.entry = entry;
|
|
|
|
this.label = entry.name;
|
|
|
|
this.isDirectory = entry.isDirectory;
|
|
|
|
},
|
2014-02-26 16:57:15 +00:00
|
|
|
//walk will asynchronously collect the file tree
|
2013-11-03 00:51:28 +00:00
|
|
|
walk: function(done) {
|
|
|
|
var self = this;
|
|
|
|
var entries = [];
|
|
|
|
var reader = this.entry.createReader();
|
|
|
|
var inc = 1;
|
|
|
|
var check = function() {
|
|
|
|
inc--;
|
|
|
|
if (inc == 0) {
|
|
|
|
return done(self);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
var collect = function(list) {
|
|
|
|
if (list.length == 0) return complete();
|
|
|
|
entries.push.apply(entries, list);
|
|
|
|
reader.readEntries(collect);
|
|
|
|
};
|
|
|
|
var complete = function() {
|
2013-11-07 05:56:12 +00:00
|
|
|
self.children = [];
|
2013-11-03 00:51:28 +00:00
|
|
|
for (var i = 0; i < entries.length; i++) {
|
|
|
|
var entry = entries[i];
|
|
|
|
if (entry.name[0] == "." && entry.isDirectory) continue;
|
|
|
|
var node = new FSNode(entry);
|
|
|
|
self.children.push(node);
|
|
|
|
if (node.isDirectory) {
|
|
|
|
inc++;
|
|
|
|
node.walk(check);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
check();
|
|
|
|
};
|
|
|
|
reader.readEntries(collect);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-02-27 02:56:05 +00:00
|
|
|
// The Project Manager actually handles rendering and interfacing with the rest
|
|
|
|
// of the code. Commands are bound to a singleton instance, but it's technically
|
|
|
|
// not picky about being the only one.
|
|
|
|
|
2013-11-06 01:44:19 +00:00
|
|
|
var ProjectManager = function(element) {
|
2013-11-03 00:51:28 +00:00
|
|
|
this.directories = [];
|
2013-11-06 01:44:19 +00:00
|
|
|
this.pathMap = {};
|
2013-11-03 02:29:10 +00:00
|
|
|
this.expanded = {};
|
2013-11-06 02:46:16 +00:00
|
|
|
this.project = null;
|
2013-11-06 01:44:19 +00:00
|
|
|
this.projectFile = null;
|
|
|
|
if (element) {
|
2014-02-27 06:34:02 +00:00
|
|
|
this.setElement(element);
|
2013-11-06 01:44:19 +00:00
|
|
|
}
|
2013-11-06 02:46:16 +00:00
|
|
|
var self = this;
|
|
|
|
chrome.storage.local.get("retainedProject", function(data) {
|
|
|
|
if (data.retainedProject) {
|
2013-11-07 18:24:50 +00:00
|
|
|
var file = new File();
|
|
|
|
file.onWrite = self.watchProjectFile.bind(self);
|
|
|
|
file.restore(data.retainedProject, function(err, f) {
|
2013-11-06 02:46:16 +00:00
|
|
|
if (err) {
|
2013-11-06 02:52:00 +00:00
|
|
|
return chrome.storage.local.remove("retainedProject");
|
2013-11-06 02:46:16 +00:00
|
|
|
}
|
|
|
|
file.read(function(err, data) {
|
|
|
|
if (err) return;
|
2013-11-07 18:24:50 +00:00
|
|
|
self.projectFile = file;
|
2013-11-06 02:46:16 +00:00
|
|
|
self.loadProject(JSON.parse(data));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2014-02-27 06:34:02 +00:00
|
|
|
});
|
2013-11-03 00:51:28 +00:00
|
|
|
};
|
|
|
|
ProjectManager.prototype = {
|
|
|
|
element: null,
|
|
|
|
addDirectory: function(c) {
|
|
|
|
var self = this;
|
|
|
|
chrome.fileSystem.chooseEntry({ type: "openDirectory" }, function(d) {
|
2013-11-21 21:42:54 +00:00
|
|
|
if (!d) return;
|
2014-01-12 09:12:14 +00:00
|
|
|
self.insertDirectory(d);
|
2013-11-03 00:51:28 +00:00
|
|
|
});
|
|
|
|
},
|
2014-01-12 09:12:14 +00:00
|
|
|
insertDirectory: function(entry) {
|
|
|
|
var root;
|
|
|
|
//ensure we aren't duplicating
|
|
|
|
this.directories.forEach(function(directoryNode){
|
|
|
|
if (directoryNode.entry.fullPath === entry.fullPath) {
|
|
|
|
root = directoryNode;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (!root) {
|
|
|
|
root = new FSNode(entry);
|
|
|
|
this.directories.push(root);
|
|
|
|
}
|
|
|
|
|
|
|
|
//if the directory was there, we still want
|
|
|
|
//to refresh it, in response to the users
|
|
|
|
//interaction
|
|
|
|
root.walk(this.render.bind(this));
|
|
|
|
},
|
2013-11-07 05:56:12 +00:00
|
|
|
removeDirectory: function(args) {
|
|
|
|
this.directories = this.directories.filter(function(node) {
|
|
|
|
return node.id != args.id;
|
|
|
|
});
|
|
|
|
this.render();
|
|
|
|
},
|
2013-11-04 16:41:22 +00:00
|
|
|
removeAllDirectories: function() {
|
|
|
|
this.directories = [];
|
|
|
|
this.render();
|
|
|
|
},
|
|
|
|
refresh: function() {
|
|
|
|
var counter = 0;
|
|
|
|
var self = this;
|
|
|
|
var check = function() {
|
|
|
|
counter++;
|
|
|
|
if (counter = self.directories.length) {
|
|
|
|
self.render();
|
|
|
|
}
|
2014-02-27 06:34:02 +00:00
|
|
|
};
|
2013-11-04 16:41:22 +00:00
|
|
|
this.directories.forEach(function(d) {
|
|
|
|
d.walk(check);
|
|
|
|
});
|
|
|
|
},
|
2013-11-03 00:51:28 +00:00
|
|
|
render: function() {
|
2013-11-03 02:29:10 +00:00
|
|
|
if (!this.element) return;
|
2013-11-08 02:06:44 +00:00
|
|
|
//Ace doesn't know about non-window resize events
|
|
|
|
//moving the panel will screw up its dimensions
|
|
|
|
setTimeout(function() {
|
|
|
|
editor.resize();
|
|
|
|
}, 500);
|
2013-11-03 02:29:10 +00:00
|
|
|
this.element.innerHTML = "";
|
2013-12-10 22:46:28 +00:00
|
|
|
this.pathMap = {};
|
2013-11-04 00:28:17 +00:00
|
|
|
if (this.directories.length == 0) {
|
2013-11-04 16:53:15 +00:00
|
|
|
this.element.removeClass("show");
|
2013-11-04 00:28:17 +00:00
|
|
|
return;
|
|
|
|
}
|
2013-11-04 16:53:15 +00:00
|
|
|
var self = this;
|
|
|
|
this.element.addClass("show");
|
2013-11-08 01:04:50 +00:00
|
|
|
var walker = function(node) {
|
2013-11-04 00:28:17 +00:00
|
|
|
var li = document.createElement("li");
|
|
|
|
if (node.isDirectory) {
|
2014-02-26 16:57:15 +00:00
|
|
|
var isRoot = self.directories.indexOf(node) != -1;
|
|
|
|
var nodeData = {
|
|
|
|
label: node.label,
|
|
|
|
path: node.entry.fullPath,
|
|
|
|
contextMenu: context.makeURL(isRoot ? "root/directory" : "directory", node.id)
|
|
|
|
};
|
|
|
|
var a = inflate.get("templates/projectDir.html", nodeData);
|
|
|
|
li.append(a);
|
2013-11-04 16:53:15 +00:00
|
|
|
if (self.expanded[node.entry.fullPath]) {
|
|
|
|
li.addClass("expanded");
|
|
|
|
}
|
2013-11-04 00:28:17 +00:00
|
|
|
var ul = document.createElement("ul");
|
2013-11-06 01:44:19 +00:00
|
|
|
node.children.sort(function(a, b) {
|
|
|
|
if (a.isDirectory != b.isDirectory) {
|
|
|
|
//sneaky casting trick
|
2013-11-08 01:25:31 +00:00
|
|
|
return ~~b.isDirectory - ~~a.isDirectory;
|
2013-11-06 01:44:19 +00:00
|
|
|
}
|
|
|
|
if (a.label < b.label) return -1;
|
|
|
|
if (a.label > b.label) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
2013-11-04 00:28:17 +00:00
|
|
|
for (var i = 0; i < node.children.length; i++) {
|
2013-11-08 01:04:50 +00:00
|
|
|
ul.append(walker(node.children[i]));
|
2013-11-04 00:28:17 +00:00
|
|
|
}
|
2013-11-04 16:53:15 +00:00
|
|
|
li.append(ul);
|
2013-11-04 16:41:22 +00:00
|
|
|
} else {
|
2014-02-26 16:57:15 +00:00
|
|
|
var nodeData = {
|
|
|
|
path: node.entry.fullPath,
|
|
|
|
contextMenu: context.makeURL("file", node.entry.fullPath.replace(/[\/\\]/g, "@")),
|
|
|
|
label: node.label
|
|
|
|
};
|
|
|
|
var a = inflate.get("templates/projectFile.html", nodeData)
|
|
|
|
li.append(a);
|
|
|
|
self.pathMap[node.entry.fullPath] = node;
|
2013-11-04 00:28:17 +00:00
|
|
|
}
|
|
|
|
return li;
|
|
|
|
};
|
|
|
|
var trees = this.directories.map(walker);
|
|
|
|
var list = document.createElement("ul");
|
|
|
|
trees.forEach(function(dir) {
|
2013-11-21 18:18:43 +00:00
|
|
|
dir.classList.add("root");
|
2013-11-04 00:28:17 +00:00
|
|
|
dir.classList.add("expanded");
|
|
|
|
list.appendChild(dir);
|
|
|
|
});
|
|
|
|
this.element.appendChild(list);
|
2013-11-03 01:30:37 +00:00
|
|
|
},
|
2013-11-04 16:41:22 +00:00
|
|
|
setElement: function(el) {
|
|
|
|
this.element = el;
|
|
|
|
this.bindEvents();
|
|
|
|
},
|
2013-11-03 01:30:37 +00:00
|
|
|
bindEvents: function() {
|
2013-11-04 16:53:15 +00:00
|
|
|
var self = this;
|
2013-11-04 16:41:22 +00:00
|
|
|
this.element.on("click", function(e) {
|
2014-01-12 18:47:08 +00:00
|
|
|
e.preventDefault();
|
2013-11-04 16:41:22 +00:00
|
|
|
var target = e.target;
|
2013-11-04 16:53:15 +00:00
|
|
|
if (target.hasClass("directory")) {
|
2013-11-06 17:17:05 +00:00
|
|
|
target.parentElement.toggle("expanded");
|
2013-11-04 16:53:15 +00:00
|
|
|
var path = target.getAttribute("data-full-path");
|
|
|
|
self.expanded[path] = !!!self.expanded[path];
|
|
|
|
}
|
2013-11-04 16:41:22 +00:00
|
|
|
});
|
2013-11-03 01:30:37 +00:00
|
|
|
},
|
2013-11-06 01:44:19 +00:00
|
|
|
openFile: function(path) {
|
|
|
|
var self = this;
|
2013-11-06 16:05:13 +00:00
|
|
|
var found = false;
|
|
|
|
var node = this.pathMap[path];
|
|
|
|
if (!node) return;
|
2013-11-06 01:44:19 +00:00
|
|
|
//walk through existing tabs to see if it's already open
|
|
|
|
var tabs = sessions.getAllTabs();
|
2013-11-06 16:05:13 +00:00
|
|
|
chrome.fileSystem.getDisplayPath(node.entry, function(path) {
|
|
|
|
//look through the tabs for matching display paths
|
|
|
|
M.map(
|
|
|
|
tabs,
|
|
|
|
function(tab, i, c) {
|
|
|
|
if (!tab.file || tab.file.virtual) {
|
|
|
|
return c(false);
|
|
|
|
}
|
2014-01-12 08:43:02 +00:00
|
|
|
tab.file.getPath(function(err, p) {
|
2013-11-06 16:05:13 +00:00
|
|
|
if (p == path) {
|
|
|
|
sessions.setCurrent(tab);
|
|
|
|
found = true;
|
|
|
|
}
|
|
|
|
//we don't actually use the result
|
|
|
|
c();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
//if no match found, create a tab
|
|
|
|
function() {
|
|
|
|
if (found) return;
|
|
|
|
var file = new File(node.entry);
|
|
|
|
file.read(function(err, data) {
|
|
|
|
sessions.addFile(data, file);
|
2014-02-27 06:34:02 +00:00
|
|
|
});
|
2013-11-06 16:05:13 +00:00
|
|
|
}
|
|
|
|
);
|
2013-11-06 01:44:19 +00:00
|
|
|
});
|
2013-11-04 00:28:17 +00:00
|
|
|
},
|
2013-11-06 02:46:16 +00:00
|
|
|
generateProject: function() {
|
|
|
|
var project = this.project || {};
|
|
|
|
//everything but "folders" is left as-is
|
|
|
|
//run through all directories, retain them, and add to the structure
|
|
|
|
project.folders = this.directories.map(function(node) {
|
|
|
|
var id = chrome.fileSystem.retainEntry(node.entry);
|
|
|
|
return {
|
|
|
|
retained: id,
|
|
|
|
path: node.entry.fullPath
|
|
|
|
};
|
|
|
|
});
|
|
|
|
var json = JSON.stringify(project, null, 2);
|
|
|
|
if (this.projectFile) {
|
|
|
|
this.projectFile.write(json);
|
|
|
|
} else {
|
2013-11-07 18:24:50 +00:00
|
|
|
var file = new File();
|
2013-11-06 20:02:20 +00:00
|
|
|
var watch = this.watchProjectFile.bind(this);
|
2013-11-07 18:24:50 +00:00
|
|
|
var self = this;
|
2013-11-06 02:46:16 +00:00
|
|
|
file.open("save", function() {
|
|
|
|
file.write(json);
|
2013-11-06 16:12:00 +00:00
|
|
|
var id = file.retain();
|
2013-11-06 02:46:16 +00:00
|
|
|
chrome.storage.local.set({retainedProject: id});
|
2013-11-06 20:02:20 +00:00
|
|
|
file.onWrite = watch;
|
2013-11-07 18:24:50 +00:00
|
|
|
self.projectFile = file;
|
2013-11-06 02:46:16 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return json;
|
2013-11-04 16:41:22 +00:00
|
|
|
},
|
2013-11-06 02:46:16 +00:00
|
|
|
openProjectFile: function() {
|
2013-11-07 18:24:50 +00:00
|
|
|
var file = new File();
|
2013-11-06 16:12:00 +00:00
|
|
|
var self = this;
|
|
|
|
file.open(function() {
|
|
|
|
file.read(function(err, data) {
|
|
|
|
if (err) return;
|
|
|
|
self.loadProject(data);
|
|
|
|
var id = file.retain();
|
|
|
|
chrome.storage.local.set({retainedProject: id});
|
2013-11-07 18:24:50 +00:00
|
|
|
self.projectFile = file;
|
2013-11-06 16:12:00 +00:00
|
|
|
});
|
2013-11-06 20:02:20 +00:00
|
|
|
file.onWrite = self.watchProjectFile;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
watchProjectFile: function() {
|
|
|
|
var self = this;
|
|
|
|
this.projectFile.read(function(err, data) {
|
|
|
|
if (err) return;
|
|
|
|
self.loadProject(data);
|
2013-11-06 16:12:00 +00:00
|
|
|
});
|
2013-11-04 16:41:22 +00:00
|
|
|
},
|
2013-11-06 02:46:16 +00:00
|
|
|
loadProject: function(project) {
|
|
|
|
var self = this;
|
2013-11-04 16:41:22 +00:00
|
|
|
//project is the JSON from a project file
|
2013-11-06 02:46:16 +00:00
|
|
|
if (typeof project == "string") {
|
|
|
|
project = JSON.parse(project);
|
|
|
|
}
|
2013-11-06 20:02:20 +00:00
|
|
|
this.project = project;
|
2013-11-04 16:41:22 +00:00
|
|
|
//assign settings
|
2013-11-06 16:35:57 +00:00
|
|
|
if (project.settings) {
|
|
|
|
Settings.setProject(project.settings);
|
|
|
|
}
|
2013-11-04 16:41:22 +00:00
|
|
|
//restore directory entries that can be restored
|
2013-11-06 20:02:20 +00:00
|
|
|
this.directories = [];
|
2013-11-06 02:46:16 +00:00
|
|
|
M.map(
|
|
|
|
project.folders,
|
|
|
|
function(folder, index, c) {
|
|
|
|
chrome.fileSystem.restoreEntry(folder.retained, function(entry) {
|
2013-11-06 20:18:50 +00:00
|
|
|
//remember, you can only restore project directories you'd previously opened
|
|
|
|
if (!entry) return c();
|
2013-11-06 02:46:16 +00:00
|
|
|
var node = new FSNode(entry);
|
|
|
|
self.directories.push(node);
|
|
|
|
node.walk(c);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function() {
|
|
|
|
self.render();
|
|
|
|
}
|
|
|
|
);
|
2013-11-06 02:52:00 +00:00
|
|
|
},
|
2013-11-06 20:02:20 +00:00
|
|
|
editProjectFile: function() {
|
|
|
|
if (!this.projectFile) {
|
|
|
|
return dialog("No project opened.");
|
|
|
|
}
|
|
|
|
var self = this;
|
|
|
|
this.projectFile.read(function(err, data) {
|
|
|
|
if (err) return;
|
|
|
|
sessions.addFile(data, self.projectFile);
|
|
|
|
});
|
|
|
|
},
|
2013-11-06 17:17:05 +00:00
|
|
|
clearProject: function(keepRetained) {
|
2013-11-06 02:52:00 +00:00
|
|
|
this.projectFile = null;
|
|
|
|
this.directories = [];
|
|
|
|
this.project = {};
|
2013-11-06 16:35:57 +00:00
|
|
|
Settings.clearProject();
|
2013-11-06 17:17:05 +00:00
|
|
|
if (!keepRetained) chrome.storage.local.remove("retainedProject");
|
2013-11-06 02:52:00 +00:00
|
|
|
this.render();
|
2013-12-10 22:46:28 +00:00
|
|
|
},
|
|
|
|
getPaths: function() {
|
|
|
|
return Object.keys(this.pathMap);
|
2013-11-03 00:51:28 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-11-06 01:44:19 +00:00
|
|
|
var pm = new ProjectManager(document.find(".project"));
|
2013-11-03 02:29:10 +00:00
|
|
|
command.on("project:add-dir", pm.addDirectory.bind(pm));
|
2013-11-04 00:28:17 +00:00
|
|
|
command.on("project:remove-all", pm.removeAllDirectories.bind(pm));
|
2013-11-06 02:46:16 +00:00
|
|
|
command.on("project:generate", pm.generateProject.bind(pm));
|
2013-11-03 01:30:37 +00:00
|
|
|
command.on("project:open-file", pm.openFile.bind(pm));
|
2013-11-04 16:41:22 +00:00
|
|
|
command.on("project:refresh-dir", pm.refresh.bind(pm));
|
2013-11-06 02:52:00 +00:00
|
|
|
command.on("project:open", pm.openProjectFile.bind(pm));
|
2013-11-06 20:02:20 +00:00
|
|
|
command.on("project:edit", pm.editProjectFile.bind(pm));
|
2013-11-06 02:52:00 +00:00
|
|
|
command.on("project:clear", pm.clearProject.bind(pm));
|
2013-11-07 05:56:12 +00:00
|
|
|
|
2013-11-08 01:25:31 +00:00
|
|
|
context.register("Remove from Project", "removeDirectory", "root/directory/:id", pm.removeDirectory.bind(pm));
|
2013-12-10 22:46:28 +00:00
|
|
|
|
|
|
|
return pm;
|
2013-11-03 00:51:28 +00:00
|
|
|
|
|
|
|
});
|