Caret/js/ui/projectManager.js

388 lines
No EOL
12 KiB
JavaScript

define([
"storage/settingsProvider",
"command",
"sessions",
"storage/file",
"util/manos",
"ui/dialog",
"ui/contextMenus",
"editor",
"util/dom2"
], function(Settings, command, sessions, File, M, dialog, context, editor) {
/*
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.
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.
*/
var guidCounter = 0;
//FSNodes are used to track filesystem state inside projects
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;
},
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() {
self.children = [];
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);
}
};
var ProjectManager = function(element) {
this.directories = [];
this.pathMap = {};
this.expanded = {};
this.project = null;
this.projectFile = null;
if (element) {
this.setElement(element)
}
var self = this;
chrome.storage.local.get("retainedProject", function(data) {
if (data.retainedProject) {
var file = new File();
file.onWrite = self.watchProjectFile.bind(self);
file.restore(data.retainedProject, function(err, f) {
if (err) {
return chrome.storage.local.remove("retainedProject");
}
file.read(function(err, data) {
if (err) return;
self.projectFile = file;
self.loadProject(JSON.parse(data));
});
});
}
})
};
ProjectManager.prototype = {
element: null,
addDirectory: function(c) {
var self = this;
chrome.fileSystem.chooseEntry({ type: "openDirectory" }, function(d) {
if (!d) return;
self.insertDirectory(d);
});
},
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));
},
removeDirectory: function(args) {
this.directories = this.directories.filter(function(node) {
return node.id != args.id;
});
this.render();
},
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();
}
}
this.directories.forEach(function(d) {
d.walk(check);
});
},
render: function() {
if (!this.element) return;
//Ace doesn't know about non-window resize events
//moving the panel will screw up its dimensions
setTimeout(function() {
editor.resize();
}, 500);
this.element.innerHTML = "";
this.pathMap = {};
if (this.directories.length == 0) {
this.element.removeClass("show");
return;
}
var self = this;
this.element.addClass("show");
var walker = function(node) {
var li = document.createElement("li");
var a = document.createElement("a");
a.setAttribute("tabindex", -1);
li.append(a);
if (node.isDirectory) {
a.innerHTML = node.label;
a.setAttribute("data-full-path", node.entry.fullPath);
a.addClass("directory");
if (self.directories.indexOf(node) != -1) {
a.href = context.makeURL("root/directory", node.id);
} else {
a.href = context.makeURL("directory", node.id);
}
a.setAttribute("command", null);
if (self.expanded[node.entry.fullPath]) {
li.addClass("expanded");
}
var ul = document.createElement("ul");
node.children.sort(function(a, b) {
if (a.isDirectory != b.isDirectory) {
//sneaky casting trick
return ~~b.isDirectory - ~~a.isDirectory;
}
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
});
for (var i = 0; i < node.children.length; i++) {
ul.append(walker(node.children[i]));
}
li.append(ul);
} else {
var path = node.entry.fullPath;
a.innerHTML = node.label;
a.href = context.makeURL("file", node.entry.fullPath.replace(/[\/\\]/g, "@"));
a.setAttribute("argument", path);
a.setAttribute("command", "project:open-file");
self.pathMap[path] = node;
}
return li;
};
var trees = this.directories.map(walker);
var list = document.createElement("ul");
trees.forEach(function(dir) {
dir.classList.add("root");
dir.classList.add("expanded");
list.appendChild(dir);
});
this.element.appendChild(list);
},
setElement: function(el) {
this.element = el;
this.bindEvents();
},
bindEvents: function() {
var self = this;
this.element.on("click", function(e) {
e.preventDefault();
var target = e.target;
if (target.hasClass("directory")) {
target.parentElement.toggle("expanded");
var path = target.getAttribute("data-full-path");
self.expanded[path] = !!!self.expanded[path];
}
});
},
openFile: function(path) {
var self = this;
var found = false;
var node = this.pathMap[path];
if (!node) return;
//walk through existing tabs to see if it's already open
var tabs = sessions.getAllTabs();
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);
}
tab.file.getPath(function(err, p) {
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);
})
}
);
});
},
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 {
var file = new File();
var watch = this.watchProjectFile.bind(this);
var self = this;
file.open("save", function() {
file.write(json);
var id = file.retain();
chrome.storage.local.set({retainedProject: id});
file.onWrite = watch;
self.projectFile = file;
});
}
return json;
},
openProjectFile: function() {
var file = new File();
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});
self.projectFile = file;
});
file.onWrite = self.watchProjectFile;
});
},
watchProjectFile: function() {
var self = this;
this.projectFile.read(function(err, data) {
if (err) return;
self.loadProject(data);
});
},
loadProject: function(project) {
var self = this;
//project is the JSON from a project file
if (typeof project == "string") {
project = JSON.parse(project);
}
this.project = project;
//assign settings
if (project.settings) {
Settings.setProject(project.settings);
}
//restore directory entries that can be restored
this.directories = [];
M.map(
project.folders,
function(folder, index, c) {
chrome.fileSystem.restoreEntry(folder.retained, function(entry) {
//remember, you can only restore project directories you'd previously opened
if (!entry) return c();
var node = new FSNode(entry);
self.directories.push(node);
node.walk(c);
});
},
function() {
self.render();
}
);
},
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);
});
},
clearProject: function(keepRetained) {
this.projectFile = null;
this.directories = [];
this.project = {};
Settings.clearProject();
if (!keepRetained) chrome.storage.local.remove("retainedProject");
this.render();
},
getPaths: function() {
return Object.keys(this.pathMap);
}
};
var pm = new ProjectManager(document.find(".project"));
command.on("project:add-dir", pm.addDirectory.bind(pm));
command.on("project:remove-all", pm.removeAllDirectories.bind(pm));
command.on("project:generate", pm.generateProject.bind(pm));
command.on("project:open-file", pm.openFile.bind(pm));
command.on("project:refresh-dir", pm.refresh.bind(pm));
command.on("project:open", pm.openProjectFile.bind(pm));
command.on("project:edit", pm.editProjectFile.bind(pm));
command.on("project:clear", pm.clearProject.bind(pm));
context.register("Remove from Project", "removeDirectory", "root/directory/:id", pm.removeDirectory.bind(pm));
return pm;
});