mirror of
https://github.com/gryf/pentadactyl-pm.git
synced 2025-12-20 23:57:59 +01:00
Document and clean up some of the trickier sections of CompletionContext.
This commit is contained in:
@@ -160,10 +160,10 @@ const CompletionContext = Class("CompletionContext", {
|
||||
* @property {CompletionContext} The top-level completion context.
|
||||
*/
|
||||
this.top = this;
|
||||
this.__defineGetter__("incomplete", function () this.contextList.some(function (c) c.parent && c.incomplete));
|
||||
this.__defineGetter__("waitingForTab", function () this.contextList.some(function (c) c.parent && c.waitingForTab));
|
||||
this.__defineSetter__("incomplete", function (val) {});
|
||||
this.__defineSetter__("waitingForTab", function (val) {});
|
||||
this.__defineGetter__("incomplete", function () this._incomplete || this.contextList.some(function (c) c.parent && c.incomplete));
|
||||
this.__defineGetter__("waitingForTab", function () this._waitingForTab || this.contextList.some(function (c) c.parent && c.waitingForTab));
|
||||
this.__defineSetter__("incomplete", function (val) { this._incomplete = val; });
|
||||
this.__defineSetter__("waitingForTab", function (val) { this._waitingForTab = val; });
|
||||
this.reset();
|
||||
}
|
||||
/**
|
||||
@@ -315,9 +315,17 @@ const CompletionContext = Class("CompletionContext", {
|
||||
this.process = format.process || this.process;
|
||||
},
|
||||
|
||||
/**
|
||||
* @property {string | xml | null}
|
||||
* The message displayed at the head of the completions for the
|
||||
* current context.
|
||||
*/
|
||||
get message() this._message || (this.waitingForTab ? "Waiting for <Tab>" : null),
|
||||
set message(val) this._message = val,
|
||||
|
||||
/**
|
||||
* The prototype object for items returned by {@link items}.
|
||||
*/
|
||||
get itemPrototype() {
|
||||
let res = {};
|
||||
function result(quote) {
|
||||
@@ -339,18 +347,39 @@ const CompletionContext = Class("CompletionContext", {
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true when the completions generated by {@link #generate}
|
||||
* must be regenerated. May be set to true to invalidate the current
|
||||
* completions.
|
||||
*/
|
||||
get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this.cache.offset != this.offset),
|
||||
set regenerate(val) { if (val) delete this.itemCache[this.key]; },
|
||||
|
||||
get generate() !this._generate ? null : function () {
|
||||
/**
|
||||
* A property which may be set to a function to generate the value
|
||||
* of {@link completions} only when necessary. The generated
|
||||
* completions are linked to the value in {@link #key} and may be
|
||||
* invalidated by setting the {@link #regenerate} property.
|
||||
*/
|
||||
get generate() this._generate || null,
|
||||
set generate(arg) {
|
||||
this.hasItems = true;
|
||||
this._generate = arg;
|
||||
},
|
||||
/**
|
||||
* Generates the item list in {@link #completions} via the
|
||||
* {@link #generate} method if the previously generated value is no
|
||||
* longer valid.
|
||||
*/
|
||||
generateCompletions: function generateCompletions() {
|
||||
if (this.offset != this.cache.offset || this.lastActivated != this.top.runCount) {
|
||||
this.itemCache = {};
|
||||
this.cache.offset = this.offset;
|
||||
this.lastActivated = this.top.runCount;
|
||||
}
|
||||
if (!this.itemCache[this.key]) {
|
||||
if (!this.itemCache[this.key])
|
||||
try {
|
||||
let res = this._generate.call(this);
|
||||
let res = this._generate();
|
||||
if (res != null)
|
||||
this.itemCache[this.key] = res;
|
||||
}
|
||||
@@ -358,37 +387,42 @@ const CompletionContext = Class("CompletionContext", {
|
||||
dactyl.reportError(e);
|
||||
this.message = "Error: " + e;
|
||||
}
|
||||
}
|
||||
return this.itemCache[this.key];
|
||||
},
|
||||
set generate(arg) {
|
||||
this.hasItems = true;
|
||||
this._generate = arg;
|
||||
// XXX
|
||||
this.noUpdate = true;
|
||||
this.completions = this.itemCache[this.key];
|
||||
this.noUpdate = false;
|
||||
},
|
||||
|
||||
get ignoreCase() {
|
||||
if ("_ignoreCase" in this)
|
||||
return this._ignoreCase;
|
||||
let mode = this.wildcase;
|
||||
if (mode == "match")
|
||||
return this._ignoreCase = false;
|
||||
if (mode == "ignore")
|
||||
return this._ignoreCase = true;
|
||||
return this._ignoreCase = !/[A-Z]/.test(this.filter);
|
||||
if (this._ignoreCase == null) {
|
||||
let mode = this.wildcase;
|
||||
if (mode == "match")
|
||||
this._ignoreCase = false;
|
||||
if (mode == "ignore")
|
||||
this._ignoreCase = true;
|
||||
this._ignoreCase = !/[A-Z]/.test(this.filter);
|
||||
}
|
||||
return this._ignoreCase;
|
||||
},
|
||||
set ignoreCase(val) this._ignoreCase = val,
|
||||
|
||||
/**
|
||||
* Returns a list of all completion items which match the current
|
||||
* filter. The items returned are objects containing one property
|
||||
* for each corresponding property in {@link keys}. The returned
|
||||
* list is generated on-demand from the item list in {@link completions}
|
||||
* or generated by {@link generate}, and is cached as long as no
|
||||
* properties which would invalidate the result are changed.
|
||||
*/
|
||||
get items() {
|
||||
// Don't return any items if completions or generator haven't
|
||||
// been set during this completion cycle.
|
||||
if (!this.hasItems)
|
||||
return [];
|
||||
|
||||
// Regenerate completions if we must
|
||||
if (this.generate) {
|
||||
// XXX
|
||||
this.noUpdate = true;
|
||||
this.completions = this.generate();
|
||||
this.noUpdate = false;
|
||||
}
|
||||
if (this.generate)
|
||||
this.generateCompletions();
|
||||
let items = this.completions;
|
||||
|
||||
// Check for cache miss
|
||||
@@ -404,7 +438,7 @@ const CompletionContext = Class("CompletionContext", {
|
||||
this.cache.rows = [];
|
||||
this.cache.filter = this.filter;
|
||||
if (items == null)
|
||||
return items;
|
||||
return null;
|
||||
|
||||
let self = this;
|
||||
delete this._substrings;
|
||||
@@ -430,9 +464,10 @@ const CompletionContext = Class("CompletionContext", {
|
||||
|
||||
try {
|
||||
// Item prototypes
|
||||
let proto = this.itemPrototype;
|
||||
if (!this.cache.constructed)
|
||||
if (!this.cache.constructed) {
|
||||
let proto = this.itemPrototype;
|
||||
this.cache.constructed = items.map(function (item) ({ __proto__: proto, item: item }));
|
||||
}
|
||||
|
||||
// Filters
|
||||
let filtered = this.filterFunc(this.cache.constructed);
|
||||
@@ -452,6 +487,10 @@ const CompletionContext = Class("CompletionContext", {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of all substrings common to all items which
|
||||
* include the current filter.
|
||||
*/
|
||||
get substrings() {
|
||||
let items = this.items;
|
||||
if (items.length == 0 || !this.hasItems)
|
||||
@@ -460,11 +499,13 @@ const CompletionContext = Class("CompletionContext", {
|
||||
return this._substrings;
|
||||
|
||||
let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
|
||||
let text = fixCase(items[0].text);
|
||||
let text = fixCase(items[0].text);
|
||||
let filter = fixCase(this.filter);
|
||||
|
||||
// Exceedingly long substrings cause Gecko to go into convulsions
|
||||
if (text.length > 100)
|
||||
text = text.substr(0, 100);
|
||||
let filter = fixCase(this.filter);
|
||||
|
||||
if (this.anchored) {
|
||||
var compare = function compare(text, s) text.substr(0, s.length) == s;
|
||||
var substrings = [text];
|
||||
@@ -480,15 +521,18 @@ const CompletionContext = Class("CompletionContext", {
|
||||
start = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
substrings = items.reduce(function (res, item)
|
||||
res.map(function (list) {
|
||||
var m, len = list.length;
|
||||
var n = list.length;
|
||||
res.map(function (substring) {
|
||||
// A simple binary search to find the longest substring
|
||||
// of the given string which also matches the current
|
||||
// item's text.
|
||||
var m, len = substring.length;
|
||||
var n = substring.length;
|
||||
var i = 0;
|
||||
while (n) {
|
||||
m = Math.floor(n / 2);
|
||||
let s = list[i + m];
|
||||
let keep = compare(fixCase(item.text), list.substring(0, i + m));
|
||||
let keep = compare(fixCase(item.text), substring.substring(0, i + m));
|
||||
if (!keep)
|
||||
len = i + m - 1;
|
||||
if (!keep || m == 0)
|
||||
@@ -498,9 +542,10 @@ const CompletionContext = Class("CompletionContext", {
|
||||
n = n - m;
|
||||
}
|
||||
}
|
||||
return len == list.length ? list : list.substr(0, Math.max(len, 0));
|
||||
return len == substring.length ? substring : substring.substr(0, Math.max(len, 0));
|
||||
}),
|
||||
substrings);
|
||||
|
||||
let quote = this.quote;
|
||||
if (quote)
|
||||
substrings = substrings.map(function (str) quote[0] + quote[1](str));
|
||||
@@ -532,6 +577,10 @@ const CompletionContext = Class("CompletionContext", {
|
||||
this._filter = this._filter.substr(count);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls the {@link #cancel} method of all currently active
|
||||
* sub-contexts.
|
||||
*/
|
||||
cancelAll: function () {
|
||||
for (let [, context] in Iterator(this.contextList)) {
|
||||
if (context.cancel)
|
||||
@@ -571,6 +620,25 @@ const CompletionContext = Class("CompletionContext", {
|
||||
yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
|
||||
},
|
||||
|
||||
/**
|
||||
* Forks this completion context to create a new sub-context named
|
||||
* as {this.name}/{name}. The new context is automatically advanced
|
||||
* *offset* characters. If *completer* is provided, it is called
|
||||
* with *self* as its 'this' object, the new context as its first
|
||||
* argument, and any subsequent arguments after *completer* as its
|
||||
* following arguments.
|
||||
*
|
||||
* If *completer* is provided, this function returns its return
|
||||
* value, otherwise it returns the new completion context.
|
||||
*
|
||||
* @param {string} name The name of the new context.
|
||||
* @param {number} offset The offset of the new context relative to
|
||||
* the current context's offset.
|
||||
* @param {object} self *completer*'s 'this' object. @optional
|
||||
* @param {function|string} completer A completer function to call
|
||||
* for the new context. If a string is provided, it is
|
||||
* interpreted as a method to access on *self*.
|
||||
*/
|
||||
fork: function fork(name, offset, self, completer) {
|
||||
if (typeof completer == "string")
|
||||
completer = self[completer];
|
||||
@@ -591,6 +659,21 @@ const CompletionContext = Class("CompletionContext", {
|
||||
return context;
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlights text in the nsIEditor associated with this completion
|
||||
* context. *length* characters are highlighted from the position
|
||||
* *start*, relative to the current context's offset, with the
|
||||
* selection type *type* as defined in nsISelectionController.
|
||||
*
|
||||
* When called with no arguments, all highlights are removed. When
|
||||
* called with a 0 length, all highlights of type *type* are
|
||||
* removed.
|
||||
*
|
||||
* @param {number} start The position at which to start
|
||||
* highlighting.
|
||||
* @param {number} length The length of the substring to highlight.
|
||||
* @param {string} type The selection type to highlight with.
|
||||
*/
|
||||
highlight: function highlight(start, length, type) {
|
||||
if (arguments.length == 0) {
|
||||
for (let type in this.selectionTypes)
|
||||
@@ -614,15 +697,35 @@ const CompletionContext = Class("CompletionContext", {
|
||||
catch (e) {}
|
||||
},
|
||||
|
||||
match: function match(str) {
|
||||
return this.matchString(this.filter, str);
|
||||
},
|
||||
|
||||
pushProcessor: function pushProcess(i, fn) {
|
||||
let next = this.process[i];
|
||||
this.process[i] = function (item, text) fn(item, text, next);
|
||||
/**
|
||||
* Tests the given string for a match against the current filter,
|
||||
* taking into account anchoring and case sensitivity rules.
|
||||
*
|
||||
* @param {string} str The string to match.
|
||||
* @returns {boolean} True if the string matches, false otherwise.
|
||||
*/
|
||||
match: function match(str) this.matchString(this.filter, str),
|
||||
|
||||
/**
|
||||
* Pushes a new output processor onto the processor chain of
|
||||
* {@link #process}. The provided function is called with the item
|
||||
* and text to process along with a reference to the processor
|
||||
* previously installed in the given *index* of {@link #process}.
|
||||
*
|
||||
* @param {number} index The index into {@link #process}.
|
||||
* @param {function(object, string, function)} func The new
|
||||
* processor.
|
||||
*/
|
||||
pushProcessor: function pushProcess(index, func) {
|
||||
let next = this.process[index];
|
||||
this.process[index] = function (item, text) func(item, text, next);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets this completion context and all sub-contexts for use in a
|
||||
* new completion cycle. May only be called on the top-level
|
||||
* context.
|
||||
*/
|
||||
reset: function reset() {
|
||||
let self = this;
|
||||
if (this.parent)
|
||||
@@ -649,9 +752,9 @@ const CompletionContext = Class("CompletionContext", {
|
||||
// delete this.contexts[key];
|
||||
for each (let context in this.contexts) {
|
||||
context.hasItems = false;
|
||||
if (context != context.top)
|
||||
context.incomplete = false;
|
||||
context.incomplete = false;
|
||||
}
|
||||
this.waitingForTab = false;
|
||||
this.runCount++;
|
||||
for each (let context in this.contextList)
|
||||
context.lastActivated = this.runCount;
|
||||
|
||||
Reference in New Issue
Block a user