Compare commits
13 commits
master
...
project-re
Author | SHA1 | Date | |
---|---|---|---|
|
20fe5b64d8 | ||
|
65e3a26cfa | ||
|
0ad9330ddc | ||
|
7e2c77d572 | ||
|
90d07c4ffa | ||
|
a28955c011 | ||
|
56701f58fd | ||
|
36d875c251 | ||
|
1fe74fa2fc | ||
|
a7b6e1f455 | ||
|
e3005d25af | ||
|
f4b54839e3 | ||
|
09ae51eab9 |
11 changed files with 456 additions and 531 deletions
|
@ -100,6 +100,7 @@
|
||||||
"dialogUnsaved": { "message": "$1 has unsaved work." },
|
"dialogUnsaved": { "message": "$1 has unsaved work." },
|
||||||
|
|
||||||
"projectNoCurrentProject": { "message": "No project currently open." },
|
"projectNoCurrentProject": { "message": "No project currently open." },
|
||||||
|
"projectRemoveDirectory": { "message": "Remove from project" },
|
||||||
|
|
||||||
"paletteExecuting": { "message": "Executing $1..." },
|
"paletteExecuting": { "message": "Executing $1..." },
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ require([
|
||||||
"sessions",
|
"sessions",
|
||||||
"util/manos",
|
"util/manos",
|
||||||
"util/i18n",
|
"util/i18n",
|
||||||
"ui/projectManager",
|
"project/tree",
|
||||||
|
"project/file",
|
||||||
"ui/keys",
|
"ui/keys",
|
||||||
"fileManager",
|
"fileManager",
|
||||||
"ui/menus",
|
"ui/menus",
|
||||||
|
|
120
js/project/file.js
Normal file
120
js/project/file.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
define([
|
||||||
|
"project/tree",
|
||||||
|
"command",
|
||||||
|
"storage/file",
|
||||||
|
"storage/settingsProvider",
|
||||||
|
"sessions",
|
||||||
|
"ui/dialog",
|
||||||
|
"util/manos"
|
||||||
|
], function(projectTree, command, File, Settings, session, dialog, M) {
|
||||||
|
var projectConfig;
|
||||||
|
var projectFile;
|
||||||
|
|
||||||
|
var generateProject = function() {
|
||||||
|
var project = projectConfig || {};
|
||||||
|
//everything but "folders" is left as-is
|
||||||
|
//run through all directories, retain them, and add to the structure
|
||||||
|
project.folders = projectTree.getDirectories().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 (projectFile) {
|
||||||
|
projectFile.write(json);
|
||||||
|
} else {
|
||||||
|
var file = new File();
|
||||||
|
var watch = watchProjectFile;
|
||||||
|
file.open("save", function() {
|
||||||
|
file.write(json);
|
||||||
|
var id = file.retain();
|
||||||
|
chrome.storage.local.set({retainedProject: id});
|
||||||
|
file.onWrite = watch;
|
||||||
|
projectFile = file;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
};
|
||||||
|
|
||||||
|
var openProjectFile = function() {
|
||||||
|
var file = new File();
|
||||||
|
file.open(function() {
|
||||||
|
file.read(function(err, data) {
|
||||||
|
loadProject(data);
|
||||||
|
var retained = file.retain();
|
||||||
|
chrome.storage.local.set({retainedProject: retained});
|
||||||
|
projectFile = file;
|
||||||
|
file.onWrite = watchProjectFile;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var watchProjectFile = function() {
|
||||||
|
projectFile.read(function(err, data) {
|
||||||
|
loadProject(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var loadProject = function(project) {
|
||||||
|
//project is the JSON from a project file
|
||||||
|
if (typeof project == "string") {
|
||||||
|
project = JSON.parse(project);
|
||||||
|
}
|
||||||
|
projectConfig = project;
|
||||||
|
//assign settings
|
||||||
|
if (project.settings) {
|
||||||
|
Settings.setProject(project.settings);
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.element.addClass("loading");
|
||||||
|
//restore directory entries that can be restored
|
||||||
|
this.directories = [];
|
||||||
|
blacklist = blacklistRegExp();
|
||||||
|
//TODO: untangle this from tree view
|
||||||
|
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);
|
||||||
|
//if this is the first, go ahead and start the slideout
|
||||||
|
if (!self.directories.length) {
|
||||||
|
self.element.addClass("show");
|
||||||
|
}
|
||||||
|
self.directories.push(node);
|
||||||
|
node.walk(blacklist, c);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
self.loading = false;
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
var editProjectFile = function() {
|
||||||
|
if (!this.projectFile) {
|
||||||
|
return dialog(i18n.get("projectNoCurrentProject"));
|
||||||
|
}
|
||||||
|
projectFile.read(function(err, data) {
|
||||||
|
sessions.addFile(data, projectFile);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var clearProject = function(keepRetained) {
|
||||||
|
projectFile = null;
|
||||||
|
projectConfig = {};
|
||||||
|
projectTree.clear();
|
||||||
|
Settings.clearProject();
|
||||||
|
if (!keepRetained) chrome.storage.local.remove("retainedProject");
|
||||||
|
};
|
||||||
|
|
||||||
|
command.on("project:generate", generateProject);
|
||||||
|
command.on("project:open", openProjectFile);
|
||||||
|
command.on("project:edit", editProjectFile);
|
||||||
|
command.on("project:clear", clearProject);
|
||||||
|
|
||||||
|
});
|
204
js/project/node.js
Normal file
204
js/project/node.js
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
define([
|
||||||
|
"util/manos",
|
||||||
|
"util/elementData",
|
||||||
|
"sessions",
|
||||||
|
"storage/file",
|
||||||
|
"util/template!templates/projectDir.html,templates/projectFile.html",
|
||||||
|
"ui/contextMenus",
|
||||||
|
"settings!user",
|
||||||
|
"util/dom2"
|
||||||
|
], function(M, elementData, sessions, File, inflate, context, Settings) {
|
||||||
|
|
||||||
|
//TODO: implement a polling-based watch for directories
|
||||||
|
//TODO: pull the blacklist and use it during readdir()
|
||||||
|
|
||||||
|
var fileListSort = function(a, b) {
|
||||||
|
if (a.isDir != b.isDir) {
|
||||||
|
return ~~b.isDir - ~~a.isDir;
|
||||||
|
}
|
||||||
|
if (a.name < b.name) return -1;
|
||||||
|
if (a.name > b.name) return 1;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var noop = function() {};
|
||||||
|
var guid = 0;
|
||||||
|
|
||||||
|
var Node = function(entry) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.name = entry.name
|
||||||
|
this.isDirty = true;
|
||||||
|
this.isDir = entry.isDirectory;
|
||||||
|
this.children = [];
|
||||||
|
this.id = guid++;
|
||||||
|
};
|
||||||
|
|
||||||
|
Node.prototype = {
|
||||||
|
id: null,
|
||||||
|
entry: null,
|
||||||
|
isOpen: false,
|
||||||
|
isDirty: false,
|
||||||
|
isDir: false,
|
||||||
|
isRoot: false,
|
||||||
|
path: null,
|
||||||
|
name: null,
|
||||||
|
parent: null,
|
||||||
|
children: null,
|
||||||
|
element: null,
|
||||||
|
toggle: function(done) {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
this.render(done);
|
||||||
|
},
|
||||||
|
setElement: function(element) {
|
||||||
|
this.element = element;
|
||||||
|
elementData.set(element, this);
|
||||||
|
},
|
||||||
|
render: function(done) {
|
||||||
|
var self = this;
|
||||||
|
done = done || noop;
|
||||||
|
if (!this.element) return done();
|
||||||
|
//render the label
|
||||||
|
var template;
|
||||||
|
var menu;
|
||||||
|
if (this.isDir) {
|
||||||
|
template = "templates/projectDir.html";
|
||||||
|
menu = context.makeURL(this.isRoot ? "root/directory" : "directory", this.id);
|
||||||
|
} else {
|
||||||
|
template = "templates/projectFile.html";
|
||||||
|
menu = context.makeURL("file", this.entry.fullPath.replace(/[\/\\]/g, "@"));
|
||||||
|
}
|
||||||
|
var a = this.element.find("a.label");
|
||||||
|
if (!a) {
|
||||||
|
a = document.createElement("a");
|
||||||
|
this.element.append(a);
|
||||||
|
}
|
||||||
|
a.outerHTML = inflate.getHTML(template, {
|
||||||
|
label: this.name,
|
||||||
|
path: this.entry.fullPath,
|
||||||
|
contextMenu: menu
|
||||||
|
});
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.element.removeClass("expanded");
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
//only render children if open
|
||||||
|
this.element.addClass("expanded");
|
||||||
|
if (this.isDirty && this.isDir) {
|
||||||
|
this.readdir(function() {
|
||||||
|
self.renderChildren(function() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.renderChildren(done);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderChildren: function(done) {
|
||||||
|
var ul = this.element.find("ul.children");
|
||||||
|
if (!ul) {
|
||||||
|
ul = document.createElement("ul");
|
||||||
|
ul.className = "children";
|
||||||
|
this.element.append(ul);
|
||||||
|
}
|
||||||
|
if (!this.children.length) return done();
|
||||||
|
this.children.sort(fileListSort);
|
||||||
|
M.map(this.children, function(item, i, c) {
|
||||||
|
if (!item.element) {
|
||||||
|
var li = document.createElement("li");
|
||||||
|
item.setElement(li);
|
||||||
|
ul.append(li);
|
||||||
|
}
|
||||||
|
item.render(c); //recurses on its own if new
|
||||||
|
}, done)
|
||||||
|
},
|
||||||
|
readdir: function(done) {
|
||||||
|
if (!this.isDir) return done();
|
||||||
|
var self = this;
|
||||||
|
var reader = this.entry.createReader();
|
||||||
|
var entries = [];
|
||||||
|
var existing = {};
|
||||||
|
this.children.forEach(function(child) {
|
||||||
|
existing[child.name] = child;
|
||||||
|
});
|
||||||
|
var collect = function(list) {
|
||||||
|
if (!list.length) return complete();
|
||||||
|
entries.push.apply(entries, list);
|
||||||
|
reader.readEntries(collect);
|
||||||
|
};
|
||||||
|
var complete = function() {
|
||||||
|
var matched = [];
|
||||||
|
var added = [];
|
||||||
|
var oldChildren = self.children;
|
||||||
|
//filter out the blacklist
|
||||||
|
try {
|
||||||
|
var filter = Settings.get("user").ignoreFiles;
|
||||||
|
filter = new RegExp(filter);
|
||||||
|
entries = entries.filter(function(entry) {
|
||||||
|
//reject .directories
|
||||||
|
if (entry.name[0] == "." && entry.isDirectory) return false;
|
||||||
|
return !filter.test(entry.name);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error applying blacklist", e, filter);
|
||||||
|
}
|
||||||
|
self.children = entries.map(function(entry) {
|
||||||
|
if (existing[entry.name]) {
|
||||||
|
return existing[entry.name];
|
||||||
|
}
|
||||||
|
return new Node(entry);
|
||||||
|
});
|
||||||
|
//cull files that disappeared
|
||||||
|
oldChildren.forEach(function(child) {
|
||||||
|
if (self.children.indexOf(child) == -1) {
|
||||||
|
if (child.element) child.element.remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.isDirty = false;
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
reader.readEntries(collect);
|
||||||
|
},
|
||||||
|
walk: function(f, done) {
|
||||||
|
M.map(this.children, function(node, i, c) {
|
||||||
|
f(node, function() {
|
||||||
|
node.walk(f, c);
|
||||||
|
});
|
||||||
|
}, done);
|
||||||
|
},
|
||||||
|
openFile: function() {
|
||||||
|
var self = this;
|
||||||
|
var tabs = sessions.getAllTabs();
|
||||||
|
var found = false;
|
||||||
|
chrome.fileSystem.getDisplayPath(this.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(self.entry);
|
||||||
|
file.read(function(err, data) {
|
||||||
|
sessions.addFile(data, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Node;
|
||||||
|
|
||||||
|
});
|
121
js/project/tree.js
Normal file
121
js/project/tree.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
define([
|
||||||
|
"project/node",
|
||||||
|
"command",
|
||||||
|
"util/elementData",
|
||||||
|
"util/i18n",
|
||||||
|
"ui/contextMenus",
|
||||||
|
"util/manos",
|
||||||
|
"util/dom2"
|
||||||
|
], function(Node, command, elementData, i18n, context, M) {
|
||||||
|
|
||||||
|
var directories = [];
|
||||||
|
var pathMap = {};
|
||||||
|
var container = document.find(".project");
|
||||||
|
var tree = container.find(".tree");
|
||||||
|
|
||||||
|
var setVisible = function() {
|
||||||
|
if (directories.length) {
|
||||||
|
container.addClass("show");
|
||||||
|
} else {
|
||||||
|
container.removeClass("show");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var addDirectory = function() {
|
||||||
|
chrome.fileSystem.chooseEntry({ type: "openDirectory" }, function(entry) {
|
||||||
|
if (!entry) return;
|
||||||
|
var root = new Node(entry);
|
||||||
|
directories.push(root);
|
||||||
|
var element = document.createElement("ul");
|
||||||
|
var rootElement = document.createElement("li");
|
||||||
|
tree.append(element);
|
||||||
|
element.append(rootElement);
|
||||||
|
root.setElement(rootElement);
|
||||||
|
root.isOpen = true;
|
||||||
|
root.isRoot = true;
|
||||||
|
root.render(function() {
|
||||||
|
//after initial render, do the walk for path lookup
|
||||||
|
root.walk(function(node, c) {
|
||||||
|
pathMap[node.entry.fullPath] = node;
|
||||||
|
//make sure the children are read and populated, then continue
|
||||||
|
if (node.isDir) {
|
||||||
|
node.readdir(c);
|
||||||
|
} else c();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setVisible();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var removeDirectory = function(id) {
|
||||||
|
directories = directories.filter(function(node) {
|
||||||
|
if (node.id == id) {
|
||||||
|
node.element.remove();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
setVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
var removeAll = function() {
|
||||||
|
tree.innerHTML = "";
|
||||||
|
directories = [];
|
||||||
|
pathmap = {};
|
||||||
|
setVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
//toggle directories, or open files directly
|
||||||
|
tree.on("click", function(e) {
|
||||||
|
var li = e.target.findUp("li");
|
||||||
|
var node = elementData.get(li);
|
||||||
|
if (!li || !node) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target.hasClass("directory")) {
|
||||||
|
node.toggle();
|
||||||
|
} else {
|
||||||
|
node.openFile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
command.on("project:refresh-dir", function() {
|
||||||
|
pathMap = {};
|
||||||
|
directories.forEach(function(dir) {
|
||||||
|
M.chain(
|
||||||
|
dir.readdir.bind(dir),
|
||||||
|
dir.render.bind(dir),
|
||||||
|
function() {
|
||||||
|
dir.walk(function(node, c) {
|
||||||
|
pathMap[node.entry.fullPath] = node;
|
||||||
|
if (node.isDir) {
|
||||||
|
node.readdir(c);
|
||||||
|
} else c();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
command.on("project:add-dir", addDirectory);
|
||||||
|
command.on("project:open-file", function(path) {
|
||||||
|
var node = pathMap[path];
|
||||||
|
if (node) node.openFile();
|
||||||
|
});
|
||||||
|
command.on("project:remove-all", removeAll);
|
||||||
|
|
||||||
|
context.register(
|
||||||
|
i18n.get("projectRemoveDirectory"),
|
||||||
|
"removeDirectory",
|
||||||
|
"root/directory/:id",
|
||||||
|
function(args) {
|
||||||
|
removeDirectory(args.id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPaths: function() { return Object.keys(pathMap) },
|
||||||
|
getDirectories: function() { return directories },
|
||||||
|
insertDirectory: addDirectory,
|
||||||
|
clear: removeAll
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
2
js/sessions/dragdrop.js
vendored
2
js/sessions/dragdrop.js
vendored
|
@ -1,7 +1,7 @@
|
||||||
define([
|
define([
|
||||||
"command",
|
"command",
|
||||||
"sessions/addRemove",
|
"sessions/addRemove",
|
||||||
"ui/projectManager",
|
"project/tree",
|
||||||
"storage/file"
|
"storage/file"
|
||||||
], function(command, addRemove, projectManager, File) {
|
], function(command, addRemove, projectManager, File) {
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ define([
|
||||||
"editor",
|
"editor",
|
||||||
"settings!menus,user",
|
"settings!menus,user",
|
||||||
"ui/statusbar",
|
"ui/statusbar",
|
||||||
"ui/projectManager",
|
"project/tree",
|
||||||
"util/template!templates/paletteItem.html",
|
"util/template!templates/paletteItem.html",
|
||||||
"util/i18n",
|
"util/i18n",
|
||||||
"util/dom2"
|
"util/dom2"
|
||||||
|
|
|
@ -1,521 +0,0 @@
|
||||||
define([
|
|
||||||
"settings!user",
|
|
||||||
"command",
|
|
||||||
"sessions",
|
|
||||||
"storage/file",
|
|
||||||
"util/manos",
|
|
||||||
"ui/dialog",
|
|
||||||
"ui/contextMenus",
|
|
||||||
"editor",
|
|
||||||
"util/template!templates/projectDir.html,templates/projectFile.html",
|
|
||||||
"util/i18n",
|
|
||||||
"util/dom2"
|
|
||||||
], function(Settings, command, sessions, File, M, dialog, context, editor, inflate, i18n) {
|
|
||||||
|
|
||||||
/*
|
|
||||||
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;
|
|
||||||
|
|
||||||
//pseudo-worker to let the UI thread breathe
|
|
||||||
var queue = [];
|
|
||||||
var working = false;
|
|
||||||
var tick = function(fn) {
|
|
||||||
if (fn) queue.push(fn);
|
|
||||||
if (fn && working) return;
|
|
||||||
working = true;
|
|
||||||
//start work on the next frame
|
|
||||||
var process = function() {
|
|
||||||
var then = Date.now();
|
|
||||||
while (queue.length) {
|
|
||||||
var now = Date.now();
|
|
||||||
if (now - then > 10) {
|
|
||||||
return setTimeout(process);
|
|
||||||
}
|
|
||||||
var next = queue.shift();
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
working = false;
|
|
||||||
};
|
|
||||||
setTimeout(process);
|
|
||||||
};
|
|
||||||
|
|
||||||
//FSNodes are used to track filesystem state inside projects
|
|
||||||
//We don't use the typical File object, because we're not really reading them
|
|
||||||
//Nodes form a tree starting at the root directory
|
|
||||||
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 will asynchronously collect the file tree
|
|
||||||
walk: function(blacklist, 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 = [];
|
|
||||||
entries.forEach(function(entry) {
|
|
||||||
//skip dot dirs, but not files
|
|
||||||
if (entry.name[0] == "." && entry.isDirectory) return;
|
|
||||||
//skip ignored files
|
|
||||||
if (blacklist) {
|
|
||||||
if (blacklist.test(entry.name)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var node = new FSNode(entry);
|
|
||||||
self.children.push(node);
|
|
||||||
if (node.isDirectory) {
|
|
||||||
inc++;
|
|
||||||
//give the UI thread a chance to breathe
|
|
||||||
tick(function() { node.walk(blacklist, check); });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
check();
|
|
||||||
};
|
|
||||||
reader.readEntries(collect);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
var ProjectManager = function(element) {
|
|
||||||
this.directories = [];
|
|
||||||
this.pathMap = {};
|
|
||||||
this.expanded = {};
|
|
||||||
this.project = null;
|
|
||||||
this.projectFile = null;
|
|
||||||
if (element) {
|
|
||||||
this.setElement(element);
|
|
||||||
}
|
|
||||||
this.loading = false;
|
|
||||||
var self = this;
|
|
||||||
chrome.storage.local.get("retainedProject", function(data) {
|
|
||||||
if (data.retainedProject) {
|
|
||||||
var retained = data.retainedProject;
|
|
||||||
if (typeof retained == "string") {
|
|
||||||
retained = {
|
|
||||||
id: retained
|
|
||||||
};
|
|
||||||
}
|
|
||||||
self.loading = true;
|
|
||||||
self.render();
|
|
||||||
var file = new File();
|
|
||||||
var onFail = function() {
|
|
||||||
self.loading = false;
|
|
||||||
self.render();
|
|
||||||
chrome.storage.local.remove("retainedProject");
|
|
||||||
}
|
|
||||||
file.onWrite = self.watchProjectFile.bind(self);
|
|
||||||
file.restore(retained.id, function(err, f) {
|
|
||||||
if (err) {
|
|
||||||
return onFail();
|
|
||||||
}
|
|
||||||
file.read(function(err, data) {
|
|
||||||
if (err) {
|
|
||||||
return onFail();
|
|
||||||
}
|
|
||||||
self.projectFile = file;
|
|
||||||
self.loadProject(JSON.parse(data));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var blacklistRegExp = function() {
|
|
||||||
var blacklist = Settings.get("user").ignoreFiles;
|
|
||||||
if (blacklist) {
|
|
||||||
return new RegExp(blacklist);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
this.element.addClass("loading");
|
|
||||||
//ensure we aren't duplicating
|
|
||||||
this.directories.forEach(function(directoryNode){
|
|
||||||
if (directoryNode.entry.fullPath === entry.fullPath) {
|
|
||||||
root = directoryNode;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//if this is the first, go ahead and start the slideout
|
|
||||||
if (!this.directories.length) {
|
|
||||||
this.element.addClass("show");
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
var self = this;
|
|
||||||
tick(function() {
|
|
||||||
root.walk(blacklistRegExp(), function() {
|
|
||||||
self.render()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
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;
|
|
||||||
this.element.addClass("loading");
|
|
||||||
var check = function() {
|
|
||||||
counter++;
|
|
||||||
if (counter == self.directories.length) {
|
|
||||||
//render() should get rid of the class, but let's be sure
|
|
||||||
self.element.removeClass("loading");
|
|
||||||
self.render();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
blacklist = blacklistRegExp();
|
|
||||||
this.directories.forEach(function(d) {
|
|
||||||
d.walk(blacklist, 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);
|
|
||||||
|
|
||||||
var tree = this.element.find(".tree");
|
|
||||||
this.pathMap = {};
|
|
||||||
if (this.directories.length == 0 && !this.loading) {
|
|
||||||
this.element.removeClass("show");
|
|
||||||
tree.innerHTML = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var self = this;
|
|
||||||
this.element.addClass("show");
|
|
||||||
if (this.loading) {
|
|
||||||
this.element.addClass("loading");
|
|
||||||
}
|
|
||||||
|
|
||||||
var walker = function(node) {
|
|
||||||
var li = document.createElement("li");
|
|
||||||
if (node.isDirectory) {
|
|
||||||
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);
|
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
return li;
|
|
||||||
};
|
|
||||||
|
|
||||||
//we give the load bar a chance to display before rendering
|
|
||||||
tick(function() {
|
|
||||||
var trees = self.directories.map(walker);
|
|
||||||
var list = document.createElement("ul");
|
|
||||||
trees.forEach(function(dir) {
|
|
||||||
dir.classList.add("root");
|
|
||||||
dir.classList.add("expanded");
|
|
||||||
list.appendChild(dir);
|
|
||||||
});
|
|
||||||
|
|
||||||
tree.innerHTML = "";
|
|
||||||
tree.appendChild(list);
|
|
||||||
if (!self.loading) {
|
|
||||||
self.element.removeClass("loading");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
editor.focus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
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) {
|
|
||||||
self.loadProject(data);
|
|
||||||
var retained = file.retain();
|
|
||||||
chrome.storage.local.set({retainedProject: retained});
|
|
||||||
self.projectFile = file;
|
|
||||||
file.onWrite = self.watchProjectFile.bind(self);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
watchProjectFile: function() {
|
|
||||||
var self = this;
|
|
||||||
this.projectFile.read(function(err, data) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
this.loading = true;
|
|
||||||
this.element.addClass("loading");
|
|
||||||
//restore directory entries that can be restored
|
|
||||||
this.directories = [];
|
|
||||||
blacklist = blacklistRegExp();
|
|
||||||
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);
|
|
||||||
//if this is the first, go ahead and start the slideout
|
|
||||||
if (!self.directories.length) {
|
|
||||||
self.element.addClass("show");
|
|
||||||
}
|
|
||||||
self.directories.push(node);
|
|
||||||
node.walk(blacklist, c);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
self.loading = false;
|
|
||||||
self.render();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
editProjectFile: function() {
|
|
||||||
if (!this.projectFile) {
|
|
||||||
return dialog(i18n.get("projectNoCurrentProject"));
|
|
||||||
}
|
|
||||||
var self = this;
|
|
||||||
this.projectFile.read(function(err, data) {
|
|
||||||
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));
|
|
||||||
|
|
||||||
var setAutoHide = function() {
|
|
||||||
var hide = Settings.get("user").autoHideProject;
|
|
||||||
if (hide) {
|
|
||||||
pm.element.classList.add("autohide");
|
|
||||||
} else {
|
|
||||||
pm.element.classList.remove("autohide");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
command.on("init:startup", setAutoHide);
|
|
||||||
command.on("init:restart", setAutoHide);
|
|
||||||
|
|
||||||
return pm;
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Caret",
|
"name": "Caret",
|
||||||
"description": "Professional text editing for Chrome and Chrome OS",
|
"description": "Professional text editing for Chrome and Chrome OS",
|
||||||
"version": "1.5.13",
|
"version": "1.5.16",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<a
|
<a
|
||||||
tabindex="-1"
|
class="directory label"
|
||||||
class="directory"
|
|
||||||
href="{{contextMenu}}"
|
|
||||||
command="null"
|
command="null"
|
||||||
data-full-path="{{path}}"
|
data-full-path="{{path}}"
|
||||||
|
href="{{contextMenu}}"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
{{label}}
|
{{label}}
|
||||||
</a>
|
</a>
|
|
@ -1,7 +1,6 @@
|
||||||
<a
|
<a
|
||||||
|
class="file label"
|
||||||
href="{{contextMenu}}"
|
href="{{contextMenu}}"
|
||||||
command="project:open-file"
|
|
||||||
argument="{{path}}"
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
{{label}}
|
{{label}}
|
||||||
|
|
Loading…
Reference in a new issue