summaryrefslogtreecommitdiffstats
path: root/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/gcli/source/lib/gcli/util/fileparser.js')
-rw-r--r--devtools/shared/gcli/source/lib/gcli/util/fileparser.js281
1 files changed, 281 insertions, 0 deletions
diff --git a/devtools/shared/gcli/source/lib/gcli/util/fileparser.js b/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
new file mode 100644
index 000000000..4c470e638
--- /dev/null
+++ b/devtools/shared/gcli/source/lib/gcli/util/fileparser.js
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+var util = require('./util');
+var l10n = require('./l10n');
+var spell = require('./spell');
+var filesystem = require('./filesystem');
+var Status = require('../types/types').Status;
+
+/*
+ * An implementation of the functions that call the filesystem, designed to
+ * support the file type.
+ */
+
+/**
+ * Helper for the parse() function from the file type.
+ * See gcli/util/filesystem.js for details
+ */
+exports.parse = function(context, typed, options) {
+ return filesystem.stat(typed).then(function(stats) {
+ // The 'save-as' case - the path should not exist but does
+ if (options.existing === 'no' && stats.exists) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrExists', [ typed ]),
+ predictor: undefined // No predictions that we can give here
+ };
+ }
+
+ if (stats.exists) {
+ // The path exists - check it's the correct file type ...
+ if (options.filetype === 'file' && !stats.isFile) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrIsNotFile', [ typed ]),
+ predictor: getPredictor(typed, options)
+ };
+ }
+
+ if (options.filetype === 'directory' && !stats.isDir) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrIsNotDirectory', [ typed ]),
+ predictor: getPredictor(typed, options)
+ };
+ }
+
+ // ... and that it matches any 'match' RegExp
+ if (options.matches != null && !options.matches.test(typed)) {
+ return {
+ value: undefined,
+ status: Status.INCOMPLETE,
+ message: l10n.lookupFormat('fileErrDoesntMatch',
+ [ typed, options.source ]),
+ predictor: getPredictor(typed, options)
+ };
+ }
+ }
+ else {
+ if (options.existing === 'yes') {
+ // We wanted something that exists, but it doesn't. But we don't know
+ // if the path so far is an ERROR or just INCOMPLETE
+ var parentName = filesystem.dirname(typed);
+ return filesystem.stat(parentName).then(function(stats) {
+ return {
+ value: undefined,
+ status: stats.isDir ? Status.INCOMPLETE : Status.ERROR,
+ message: l10n.lookupFormat('fileErrNotExists', [ typed ]),
+ predictor: getPredictor(typed, options)
+ };
+ });
+ }
+ }
+
+ // We found no problems
+ return {
+ value: typed,
+ status: Status.VALID,
+ message: undefined,
+ predictor: getPredictor(typed, options)
+ };
+ });
+};
+
+var RANK_OPTIONS = { noSort: true, prefixZero: true };
+
+/**
+ * We want to be able to turn predictions off in Firefox
+ */
+exports.supportsPredictions = false;
+
+/**
+ * Get a function which creates predictions of files that match the given
+ * path
+ */
+function getPredictor(typed, options) {
+ if (!exports.supportsPredictions) {
+ return undefined;
+ }
+
+ return function() {
+ var allowFile = (options.filetype !== 'directory');
+ var parts = filesystem.split(typed);
+
+ var absolute = (typed.indexOf('/') === 0);
+ var roots;
+ if (absolute) {
+ roots = [ { name: '/', dist: 0, original: '/' } ];
+ }
+ else {
+ roots = dirHistory.getCommonDirectories().map(function(root) {
+ return { name: root, dist: 0, original: root };
+ });
+ }
+
+ // Add each part of the typed pathname onto each of the roots in turn,
+ // Finding options from each of those paths, and using these options as
+ // our roots for the next part
+ var partsAdded = util.promiseEach(parts, function(part, index) {
+
+ var partsSoFar = filesystem.join.apply(filesystem, parts.slice(0, index + 1));
+
+ // We allow this file matches in this pass if we're allowed files at all
+ // (i.e this isn't 'cd') and if this is the last part of the path
+ var allowFileForPart = (allowFile && index >= parts.length - 1);
+
+ var rootsPromise = util.promiseEach(roots, function(root) {
+
+ // Extend each roots to a list of all the files in each of the roots
+ var matchFile = allowFileForPart ? options.matches : null;
+ var promise = filesystem.ls(root.name, matchFile);
+
+ var onSuccess = function(entries) {
+ // Unless this is the final part filter out the non-directories
+ if (!allowFileForPart) {
+ entries = entries.filter(function(entry) {
+ return entry.isDir;
+ });
+ }
+ var entryMap = {};
+ entries.forEach(function(entry) {
+ entryMap[entry.pathname] = entry;
+ });
+ return entryMap;
+ };
+
+ var onError = function(err) {
+ // We expect errors due to the path not being a directory, not being
+ // accessible, or removed since the call to 'readdir'
+ return {};
+ };
+
+ promise = promise.then(onSuccess, onError);
+
+ // We want to compare all the directory entries with the original root
+ // plus the partsSoFar
+ var compare = filesystem.join(root.original, partsSoFar);
+
+ return promise.then(function(entryMap) {
+
+ var ranks = spell.rank(compare, Object.keys(entryMap), RANK_OPTIONS);
+ // penalize each path by the distance of it's parent
+ ranks.forEach(function(rank) {
+ rank.original = root.original;
+ rank.stats = entryMap[rank.name];
+ });
+ return ranks;
+ });
+ });
+
+ return rootsPromise.then(function(data) {
+ // data is an array of arrays of ranking objects. Squash down.
+ data = data.reduce(function(prev, curr) {
+ return prev.concat(curr);
+ }, []);
+
+ data.sort(function(r1, r2) {
+ return r1.dist - r2.dist;
+ });
+
+ // Trim, but by how many?
+ // If this is the last run through, we want to present the user with
+ // a sensible set of predictions. Otherwise we want to trim the tree
+ // to a reasonable set of matches, so we're happy with 1
+ // We look through x +/- 3 roots, and find the one with the biggest
+ // distance delta, and cut below that
+ // x=5 for the last time through, and x=8 otherwise
+ var isLast = index >= parts.length - 1;
+ var start = isLast ? 1 : 5;
+ var end = isLast ? 7 : 10;
+
+ var maxDeltaAt = start;
+ var maxDelta = data[start].dist - data[start - 1].dist;
+
+ for (var i = start + 1; i < end; i++) {
+ var delta = data[i].dist - data[i - 1].dist;
+ if (delta >= maxDelta) {
+ maxDelta = delta;
+ maxDeltaAt = i;
+ }
+ }
+
+ // Update the list of roots for the next time round
+ roots = data.slice(0, maxDeltaAt);
+ });
+ });
+
+ return partsAdded.then(function() {
+ var predictions = roots.map(function(root) {
+ var isFile = root.stats && root.stats.isFile;
+ var isDir = root.stats && root.stats.isDir;
+
+ var name = root.name;
+ if (isDir && name.charAt(name.length) !== filesystem.sep) {
+ name += filesystem.sep;
+ }
+
+ return {
+ name: name,
+ incomplete: !(allowFile && isFile),
+ isFile: isFile, // Added for describe, below
+ dist: root.dist, // TODO: Remove - added for debug in describe
+ };
+ });
+
+ return util.promiseEach(predictions, function(prediction) {
+ if (!prediction.isFile) {
+ prediction.description = '(' + prediction.dist + ')';
+ prediction.dist = undefined;
+ prediction.isFile = undefined;
+ return prediction;
+ }
+
+ return filesystem.describe(prediction.name).then(function(description) {
+ prediction.description = description;
+ prediction.dist = undefined;
+ prediction.isFile = undefined;
+ return prediction;
+ });
+ });
+ });
+ };
+}
+
+// =============================================================================
+
+/*
+ * The idea is that we maintain a list of 'directories that the user is
+ * interested in'. We store directories in a most-frequently-used cache
+ * of some description.
+ * But for now we're just using / and ~/
+ */
+var dirHistory = {
+ getCommonDirectories: function() {
+ return [
+ filesystem.sep, // i.e. the root directory
+ filesystem.home // i.e. the users home directory
+ ];
+ },
+ addCommonDirectory: function(ignore) {
+ // Not implemented yet
+ }
+};